Commit: Initial Update

This commit is contained in:
2026-10-13 20:35:22 +07:00
parent f1c253b028
commit 43b8bfa253
24 changed files with 2440 additions and 0 deletions
+302
View File
@@ -0,0 +1,302 @@
package components
import (
"fmt"
"strings"
"github.com/gdamore/tcell/v2"
"github.com/nebara/nebara-os/internal/model"
"github.com/nebara/nebara-os/internal/ui/sixel"
"github.com/nebara/nebara-os/internal/ui/theme"
"github.com/rivo/tview"
)
// HeaderWidget renders the official Nebara OS logo and version on the same row.
// When SIXEL is enabled, it renders the official high-fidelity PNG logo directly
// into the terminal via SIXEL escape sequences with synchronized tcell cell locking.
// Otherwise, it falls back to the colored text logo.
type HeaderWidget struct {
*tview.TextView
version string
marginH int
sixelRenderer *sixel.LogoRenderer
}
// NewHeaderWidget creates the header with the given version string, margin, and SIXEL setting.
func NewHeaderWidget(version string, marginH int, enableSixel bool) *HeaderWidget {
tv := tview.NewTextView().
SetDynamicColors(true).
SetScrollable(false)
tv.SetBackgroundColor(theme.ColorBackground)
var sr *sixel.LogoRenderer
if enableSixel {
if r, err := sixel.NewLogoRenderer(); err == nil {
sr = r
}
}
h := &HeaderWidget{
TextView: tv,
version: version,
marginH: marginH,
sixelRenderer: sr,
}
h.refresh(80) // initial width estimate; recalculated on draw
return h
}
// SetVersion updates the version string and redraws.
func (h *HeaderWidget) SetVersion(v string) {
h.version = v
}
// SetMarginH updates the horizontal margin.
func (h *HeaderWidget) SetMarginH(m int) {
h.marginH = m
}
// InvalidateSixel marks the SIXEL graphic dirty so it re-renders on next draw.
func (h *HeaderWidget) InvalidateSixel() {
if h.sixelRenderer != nil {
h.sixelRenderer.Invalidate()
}
}
// SetSixelEnabled toggles SIXEL rendering.
func (h *HeaderWidget) SetSixelEnabled(enabled bool) {
if h.sixelRenderer != nil {
h.sixelRenderer.SetEnabled(enabled)
}
}
// Refresh recalculates the header text for the current terminal width.
func (h *HeaderWidget) Refresh(termWidth int) {
h.refresh(termWidth)
}
// Draw renders the header text and synchronizes the SIXEL logo graphic with tcell.
func (h *HeaderWidget) Draw(screen tcell.Screen) {
h.TextView.Draw(screen)
if h.sixelRenderer != nil && h.sixelRenderer.IsEnabled() {
x, y, _, _ := h.TextView.GetInnerRect()
_ = h.sixelRenderer.Draw(screen, x, y)
}
}
func (h *HeaderWidget) refresh(termWidth int) {
versionText := "Version " + h.version
contentWidth := termWidth - h.marginH*2
versionVisibleLen := len(versionText)
// If SIXEL is active and content width is sufficient, reserve space for the image
if h.sixelRenderer != nil && h.sixelRenderer.IsEnabled() && contentWidth >= h.sixelRenderer.CellCols()+versionVisibleLen+2 {
logoCols := h.sixelRenderer.CellCols()
spaces := contentWidth - logoCols - versionVisibleLen
if spaces < 1 {
spaces = 1
}
line := strings.Repeat(" ", logoCols) + strings.Repeat(" ", spaces) + versionText
h.TextView.SetText(line)
return
}
// Fallback to text logo: "NEBARA - OS" with "O" in accent red.
logoPlain := "NEBARA - "
logoColored := fmt.Sprintf(
"[%s]%s[%s]O[%s]S",
theme.HexPrimary,
logoPlain,
theme.HexLogoAccent,
theme.HexPrimary,
)
logoVisibleLen := len("NEBARA - OS")
spaces := contentWidth - logoVisibleLen - versionVisibleLen
if spaces < 1 {
spaces = 1
}
line := logoColored + strings.Repeat(" ", spaces) + versionText
h.TextView.SetText(line)
}
// ApplianceInfoWidget renders the appliance metadata block.
type ApplianceInfoWidget struct {
*tview.TextView
}
// NewApplianceInfoWidget creates the appliance info display.
func NewApplianceInfoWidget(info model.ApplianceInfo) *ApplianceInfoWidget {
tv := tview.NewTextView().
SetDynamicColors(true).
SetScrollable(false)
tv.SetBackgroundColor(theme.ColorBackground)
w := &ApplianceInfoWidget{TextView: tv}
w.SetInfo(info)
return w
}
// SetInfo refreshes the appliance information display.
func (w *ApplianceInfoWidget) SetInfo(info model.ApplianceInfo) {
// Product title is bold; labels and values use regular weight.
// tview bold tag: [::b] ; reset: [::-]
text := fmt.Sprintf(
"[%s][::b]%s[::-]\n\n[%s]Hostname: %s\n[%s]Address : %s\n[%s]Uptime : %s\n[%s]Status : %s",
theme.HexPrimary, info.ProductName,
theme.HexPrimary, info.Hostname,
theme.HexPrimary, info.Address,
theme.HexPrimary, info.Uptime,
theme.HexPrimary, info.Status,
)
w.TextView.SetText(text)
}
// MessageWidget renders an optional severity-classified system message.
type MessageWidget struct {
*tview.TextView
}
// NewMessageWidget creates the message widget (initially empty).
func NewMessageWidget() *MessageWidget {
tv := tview.NewTextView().
SetDynamicColors(true).
SetScrollable(false)
tv.SetBackgroundColor(theme.ColorBackground)
return &MessageWidget{TextView: tv}
}
// SetMessage updates the displayed message. Pass nil to clear.
func (w *MessageWidget) SetMessage(msg *model.SystemMessage) {
if msg == nil || !msg.IsVisible() {
w.TextView.SetText("")
return
}
var hexColor string
switch msg.Severity {
case model.SeverityWarning:
hexColor = theme.HexWarning
case model.SeverityError:
hexColor = theme.HexError
case model.SeverityInfo:
hexColor = theme.HexInfo
default:
hexColor = theme.HexPrimary
}
// "Message : " label in primary color, then the tagged text in severity color.
text := fmt.Sprintf("[%s]Message : [%s]%s",
theme.HexPrimary, hexColor, tview.Escape(msg.Text))
w.TextView.SetText(text)
}
// DescriptionWidget shows the contextual description for the selected menu item.
type DescriptionWidget struct {
*tview.TextView
}
// NewDescriptionWidget creates the description area.
func NewDescriptionWidget() *DescriptionWidget {
tv := tview.NewTextView().
SetDynamicColors(true).
SetScrollable(false).
SetWrap(true)
tv.SetBackgroundColor(theme.ColorBackground)
return &DescriptionWidget{TextView: tv}
}
// SetDescription updates the displayed contextual description.
func (w *DescriptionWidget) SetDescription(text string) {
w.TextView.SetText(fmt.Sprintf("[%s]%s", theme.HexSecondary, tview.Escape(text)))
}
// FooterWidget renders the keyboard shortcut hint row.
type FooterWidget struct {
*tview.TextView
}
// NewFooterWidget creates the keyboard hint footer.
func NewFooterWidget() *FooterWidget {
tv := tview.NewTextView().
SetDynamicColors(true).
SetScrollable(false)
tv.SetBackgroundColor(theme.ColorBackground)
f := &FooterWidget{TextView: tv}
f.Refresh(120)
return f
}
// Refresh redraws the footer at the appropriate density for the terminal width.
func (f *FooterWidget) Refresh(termWidth int) {
if termWidth < 80 {
// Compact form.
f.TextView.SetText(fmt.Sprintf("[%s][::b]↑↓[-] Nav [::b]↵[-] Sel [::b]Esc[-] Back [::b]F1[-] Help",
theme.HexPrimary))
} else {
f.TextView.SetText(fmt.Sprintf("[%s][::b]↑↓[-] Navigate [::b]Enter[-] [::b]Select[-] [::b]Esc[-] [::b]Back[-] [::b]F1[-] [::b]Help[-]",
theme.HexPrimary))
}
}
// ModalWidget is a simple centered dialog box for "not implemented" notices.
type ModalWidget struct {
*tview.Modal
}
// NewNotImplementedModal creates a minimal "Not Implemented" modal.
func NewNotImplementedModal(itemLabel string, onDismiss func()) *tview.Modal {
msg := fmt.Sprintf("[ %s ]\n\nThis function is not available in the\nNebara OS TUI Prototype v0.1.\n\nPress Enter or Escape to return.", itemLabel)
modal := tview.NewModal().
SetText(msg).
AddButtons([]string{"OK"}).
SetDoneFunc(func(_ int, _ string) {
if onDismiss != nil {
onDismiss()
}
})
modal.SetBackgroundColor(theme.ColorBackground)
modal.SetBorderColor(theme.ColorBorder)
modal.SetTextColor(theme.ColorSecondary)
return modal
}
// HelpModal returns a modal displaying keyboard shortcuts.
func HelpModal(onDismiss func()) *tview.Modal {
text := "NEBARA OS — KEYBOARD SHORTCUTS\n\n" +
"↑ / ↓ Navigate menu items\n" +
"Enter Open submenu / confirm\n" +
"Esc Return to previous menu\n" +
"Home Jump to first item\n" +
"End Jump to last item\n" +
"F1 Show this help\n" +
"Ctrl+C Quit application"
modal := tview.NewModal().
SetText(text).
AddButtons([]string{"Close"}).
SetDoneFunc(func(_ int, _ string) {
if onDismiss != nil {
onDismiss()
}
})
modal.SetBackgroundColor(theme.ColorBackground)
modal.SetBorderColor(theme.ColorBorder)
modal.SetTextColor(theme.ColorSecondary)
return modal
}
// MinSizeWarning renders a compact message when the terminal is too small.
func MinSizeWarning(screen tcell.Screen, w, h int) {
msg := " Terminal too small. Minimum: 60×20 "
style := tcell.StyleDefault.Background(theme.ColorError).Foreground(theme.ColorBackground)
x := (w - len(msg)) / 2
y := h / 2
if x < 0 {
x = 0
}
for i, ch := range msg {
screen.SetContent(x+i, y, ch, nil, style)
}
}
+211
View File
@@ -0,0 +1,211 @@
// Package components provides custom tview primitives for the Nebara OS TUI.
// Each component is self-contained and themed using the centralized design system.
package components
import (
"github.com/gdamore/tcell/v2"
"github.com/nebara/nebara-os/internal/model"
"github.com/nebara/nebara-os/internal/ui/theme"
"github.com/rivo/tview"
)
// MenuWidget is a custom tview.Primitive that renders the navigation menu
// with full-width row highlighting, a single-cell border, and inner padding.
// It implements the exact Figma selection design: full-width BBBBBB background.
type MenuWidget struct {
*tview.Box
items []model.MenuItem
selectedIdx int
innerPad int // horizontal cells of padding inside the border
// onSelectionChanged is called whenever the selected index changes.
onSelectionChanged func(idx int)
// onEnter is called when the user presses Enter on an item.
onEnter func(idx int)
// onBack is called when the user presses Escape.
onBack func()
}
// NewMenuWidget creates a MenuWidget with the given items and padding.
func NewMenuWidget(items []model.MenuItem, innerPad int) *MenuWidget {
m := &MenuWidget{
Box: tview.NewBox(),
items: items,
innerPad: innerPad,
}
m.Box.SetBackgroundColor(theme.ColorBackground)
m.Box.SetBorderColor(theme.ColorBorder)
m.Box.SetBorder(true)
return m
}
// SetInnerPad updates the horizontal padding inside the border.
func (m *MenuWidget) SetInnerPad(pad int) {
m.innerPad = pad
}
// SetItems replaces the current menu items.
func (m *MenuWidget) SetItems(items []model.MenuItem) {
m.items = items
if m.selectedIdx >= len(items) {
m.selectedIdx = 0
}
}
// SetSelectedIndex sets the highlighted row.
func (m *MenuWidget) SetSelectedIndex(idx int) {
if idx >= 0 && idx < len(m.items) {
m.selectedIdx = idx
}
}
// SelectedIndex returns the current highlighted row index.
func (m *MenuWidget) SelectedIndex() int {
return m.selectedIdx
}
// SetOnSelectionChanged registers a callback invoked on selection movement.
func (m *MenuWidget) SetOnSelectionChanged(fn func(int)) {
m.onSelectionChanged = fn
}
// SetOnEnter registers a callback invoked when Enter is pressed.
func (m *MenuWidget) SetOnEnter(fn func(int)) {
m.onEnter = fn
}
// SetOnBack registers a callback invoked when Escape is pressed.
func (m *MenuWidget) SetOnBack(fn func()) {
m.onBack = fn
}
// Draw renders the menu widget.
// The border is drawn by tview.Box; we draw the items in the inner area.
func (m *MenuWidget) Draw(screen tcell.Screen) {
// Draw the box border first.
m.Box.Draw(screen)
// Get the inner area (inside the border).
x, y, w, h := m.Box.GetInnerRect()
if w <= 0 || h <= 0 {
return
}
// Render each item. Each item occupies exactly one row with one blank row
// between items for visual breathing room, matching the Figma layout.
// We use a 1-cell vertical inset inside the border (top and bottom).
innerY := y + 1 // 1-cell top inset
innerH := h - 2 // reserve 1-cell bottom inset
_ = innerH
for i, item := range m.items {
row := innerY + i*2 // 2 rows per item: text + blank separator
if row >= y+h-1 {
break // do not overflow the border bottom
}
isSelected := i == m.selectedIdx
var fg, bg tcell.Color
var bold bool
if isSelected {
fg = theme.ColorSelectionFg
bg = theme.ColorSelectionBg
bold = false
} else {
fg = theme.ColorPrimary
bg = theme.ColorBackground
bold = false
}
style := tcell.StyleDefault.Background(bg).Foreground(fg)
if bold {
style = style.Bold(true)
}
// Build the label: ">" prefix for selected, " " otherwise.
prefix := " "
if isSelected {
prefix = "> "
}
label := prefix + item.Label
// Fill the entire row width with the selection background.
// This gives the full-width highlight shown in the mockup.
fillRow(screen, x, row, w, style)
// Draw the label text (with left padding inside the border).
textX := x + m.innerPad
drawText(screen, textX, row, label, style)
}
}
// InputHandler handles keyboard events for the menu widget.
func (m *MenuWidget) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
return m.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
switch event.Key() {
case tcell.KeyUp:
if m.selectedIdx > 0 {
m.selectedIdx--
if m.onSelectionChanged != nil {
m.onSelectionChanged(m.selectedIdx)
}
}
case tcell.KeyDown:
if m.selectedIdx < len(m.items)-1 {
m.selectedIdx++
if m.onSelectionChanged != nil {
m.onSelectionChanged(m.selectedIdx)
}
}
case tcell.KeyHome:
if m.selectedIdx != 0 {
m.selectedIdx = 0
if m.onSelectionChanged != nil {
m.onSelectionChanged(m.selectedIdx)
}
}
case tcell.KeyEnd:
last := len(m.items) - 1
if m.selectedIdx != last {
m.selectedIdx = last
if m.onSelectionChanged != nil {
m.onSelectionChanged(m.selectedIdx)
}
}
case tcell.KeyEnter:
if m.onEnter != nil {
m.onEnter(m.selectedIdx)
}
case tcell.KeyEscape:
if m.onBack != nil {
m.onBack()
}
}
})
}
// HasFocus is required to satisfy tview.Primitive.
func (m *MenuWidget) HasFocus() bool {
return m.Box.HasFocus()
}
// Focus is required to satisfy tview.Primitive.
func (m *MenuWidget) Focus(delegate func(p tview.Primitive)) {
m.Box.Focus(delegate)
}
// fillRow paints an entire horizontal row with the given style.
func fillRow(screen tcell.Screen, x, y, width int, style tcell.Style) {
for col := x; col < x+width; col++ {
screen.SetContent(col, y, ' ', nil, style)
}
}
// drawText writes a string starting at (x, y) using the given style.
func drawText(screen tcell.Screen, x, y int, text string, style tcell.Style) {
for i, ch := range text {
screen.SetContent(x+i, y, ch, nil, style)
}
}
+93
View File
@@ -0,0 +1,93 @@
// Package layout provides responsive dimension calculations for the Nebara OS TUI.
// All functions operate on terminal cell dimensions, not pixels.
package layout
// Constraints holds calculated layout values for a given terminal size.
type Constraints struct {
TermWidth int
TermHeight int
// Outer horizontal margin in cells (applied to both left and right sides).
MarginH int
// Outer vertical margin in cells (applied to top and bottom).
MarginV int
// ContentWidth is the usable width between the two horizontal margins.
ContentWidth int
// AppInfoLines is the number of rows reserved for appliance information
// (product title + 4 metadata rows).
AppInfoLines int
// MessageVisible indicates whether the message row should be shown.
// On very compact terminals it may be suppressed.
MessageVisible bool
// MenuInnerPad is the horizontal padding inside the menu border.
MenuInnerPad int
// FooterLines is the number of rows occupied by the footer (description + shortcuts).
FooterLines int
// IsMinimum flags terminals smaller than the supported minimum.
IsMinimum bool
}
// MinWidth is the smallest terminal width the application supports gracefully.
const MinWidth = 60
// MinHeight is the smallest terminal height the application supports gracefully.
const MinHeight = 20
// Calculate derives layout constraints from terminal dimensions.
// Reference: Figma frame 1920×1080 with 80px outer margins (4.17% of width)
// and 60px vertical margins (5.56% of height).
func Calculate(termW, termH int) Constraints {
if termW < MinWidth || termH < MinHeight {
return Constraints{
TermWidth: termW,
TermHeight: termH,
IsMinimum: true,
}
}
// Horizontal margin: ~4.17% of width, min 4 cells, max 12 cells.
marginH := clamp(termW*417/10000, 4, 12)
// Vertical margin: ~5.56% of height, min 1 cell, max 4 cells.
marginV := clamp(termH*556/10000, 1, 4)
contentWidth := termW - marginH*2
// Menu inner horizontal padding (space between border char and text).
menuInnerPad := 2
if termW < 100 {
menuInnerPad = 1
}
// Message row visibility: suppress on very compact terminals.
messageVisible := termH >= 24
return Constraints{
TermWidth: termW,
TermHeight: termH,
MarginH: marginH,
MarginV: marginV,
ContentWidth: contentWidth,
AppInfoLines: 6, // blank + product title + blank + 4 metadata rows
MessageVisible: messageVisible,
MenuInnerPad: menuInnerPad,
FooterLines: 2,
IsMinimum: false,
}
}
func clamp(v, lo, hi int) int {
if v < lo {
return lo
}
if v > hi {
return hi
}
return v
}
+73
View File
@@ -0,0 +1,73 @@
package layout_test
import (
"testing"
"github.com/nebara/nebara-os/internal/ui/layout"
)
func TestCalculate_Normal(t *testing.T) {
c := layout.Calculate(120, 40)
if c.IsMinimum {
t.Error("120×40 should not be minimum")
}
if c.ContentWidth <= 0 {
t.Error("ContentWidth should be positive")
}
if c.MarginH <= 0 {
t.Error("MarginH should be positive")
}
if c.ContentWidth != c.TermWidth-c.MarginH*2 {
t.Errorf("ContentWidth mismatch: got %d, expected %d", c.ContentWidth, c.TermWidth-c.MarginH*2)
}
}
func TestCalculate_TooSmall(t *testing.T) {
c := layout.Calculate(40, 10)
if !c.IsMinimum {
t.Error("40×10 should be flagged as minimum/too-small")
}
}
func TestCalculate_BoundaryMinWidth(t *testing.T) {
c := layout.Calculate(layout.MinWidth, layout.MinHeight)
if c.IsMinimum {
t.Errorf("exact minimum %dx%d should not be flagged as too small", layout.MinWidth, layout.MinHeight)
}
}
func TestCalculate_MarginClamps(t *testing.T) {
// Very wide terminal: margin should not exceed 12.
cWide := layout.Calculate(400, 80)
if cWide.MarginH > 12 {
t.Errorf("MarginH should not exceed 12 on very wide terminal, got %d", cWide.MarginH)
}
// Narrow but valid terminal: margin should not go below 4.
cNarrow := layout.Calculate(60, 20)
if cNarrow.MarginH < 4 {
t.Errorf("MarginH should be at least 4 on narrow terminal, got %d", cNarrow.MarginH)
}
}
func TestCalculate_MessageVisibility(t *testing.T) {
cCompact := layout.Calculate(80, 23)
if cCompact.MessageVisible {
t.Error("message should be hidden on terminals shorter than 24 rows")
}
cNormal := layout.Calculate(80, 24)
if !cNormal.MessageVisible {
t.Error("message should be visible at 24 rows")
}
}
func TestCalculate_MenuInnerPad(t *testing.T) {
cNarrow := layout.Calculate(80, 24)
if cNarrow.MenuInnerPad != 1 {
t.Errorf("expected menu inner pad 1 for narrow terminal, got %d", cNarrow.MenuInnerPad)
}
cWide := layout.Calculate(120, 40)
if cWide.MenuInnerPad != 2 {
t.Errorf("expected menu inner pad 2 for wide terminal, got %d", cWide.MenuInnerPad)
}
}
+127
View File
@@ -0,0 +1,127 @@
// Package navigation implements the menu navigation stack for Nebara OS TUI.
package navigation
import "github.com/nebara/nebara-os/internal/model"
// Level represents one level in the navigation stack.
type Level struct {
Items []model.MenuItem
SelectedIdx int
}
// Navigator maintains a stack of menu levels and tracks the current position.
type Navigator struct {
stack []Level
}
// NewNavigator creates a Navigator rooted at the main menu.
func NewNavigator(items []model.MenuItem, initialIdx int) *Navigator {
if initialIdx < 0 || initialIdx >= len(items) {
initialIdx = 0
}
return &Navigator{
stack: []Level{
{Items: items, SelectedIdx: initialIdx},
},
}
}
// Current returns the active menu level.
func (n *Navigator) Current() *Level {
if len(n.stack) == 0 {
return nil
}
return &n.stack[len(n.stack)-1]
}
// Depth returns the current nesting depth (0 = main menu).
func (n *Navigator) Depth() int {
return len(n.stack) - 1
}
// IsAtRoot reports whether the navigator is at the main menu level.
func (n *Navigator) IsAtRoot() bool {
return len(n.stack) == 1
}
// SelectedItem returns the currently highlighted menu item.
func (n *Navigator) SelectedItem() *model.MenuItem {
cur := n.Current()
if cur == nil {
return nil
}
return &cur.Items[cur.SelectedIdx]
}
// MoveUp moves the selection one row up, clamping at the first item.
func (n *Navigator) MoveUp() {
cur := n.Current()
if cur == nil {
return
}
if cur.SelectedIdx > 0 {
cur.SelectedIdx--
}
}
// MoveDown moves the selection one row down, clamping at the last item.
func (n *Navigator) MoveDown() {
cur := n.Current()
if cur == nil {
return
}
if cur.SelectedIdx < len(cur.Items)-1 {
cur.SelectedIdx++
}
}
// MoveFirst jumps to the first item.
func (n *Navigator) MoveFirst() {
cur := n.Current()
if cur != nil {
cur.SelectedIdx = 0
}
}
// MoveLast jumps to the last item.
func (n *Navigator) MoveLast() {
cur := n.Current()
if cur != nil {
cur.SelectedIdx = len(cur.Items) - 1
}
}
// Enter descends into the submenu of the currently selected item.
// Returns false when the item has no children (leaf node).
func (n *Navigator) Enter() bool {
item := n.SelectedItem()
if item == nil || len(item.Children) == 0 {
return false
}
n.stack = append(n.stack, Level{
Items: item.Children,
SelectedIdx: 0,
})
return true
}
// Back pops the current level from the stack, returning to the parent.
// Returns false when already at the root.
func (n *Navigator) Back() bool {
if len(n.stack) <= 1 {
return false
}
n.stack = n.stack[:len(n.stack)-1]
return true
}
// BreadcrumbTitle builds a display title for the current navigation depth.
// It returns empty string when at the root.
func (n *Navigator) BreadcrumbTitle() string {
if len(n.stack) <= 1 {
return ""
}
// Return the label of the parent item whose children we are browsing.
parent := n.stack[len(n.stack)-2]
return parent.Items[parent.SelectedIdx].Label
}
+170
View File
@@ -0,0 +1,170 @@
package navigation_test
import (
"testing"
"github.com/nebara/nebara-os/internal/model"
"github.com/nebara/nebara-os/internal/ui/navigation"
)
func sampleItems() []model.MenuItem {
return []model.MenuItem{
{Label: "Item A", Description: "Desc A", Children: []model.MenuItem{
{Label: "Child A1", Description: "Desc A1"},
{Label: "Child A2", Description: "Desc A2"},
}},
{Label: "Item B", Description: "Desc B"},
{Label: "Item C", Description: "Desc C"},
}
}
func TestNewNavigator_InitialIndex(t *testing.T) {
nav := navigation.NewNavigator(sampleItems(), 1)
if nav.Current().SelectedIdx != 1 {
t.Errorf("expected initial index 1, got %d", nav.Current().SelectedIdx)
}
}
func TestNewNavigator_InvalidIndex(t *testing.T) {
nav := navigation.NewNavigator(sampleItems(), 99)
if nav.Current().SelectedIdx != 0 {
t.Errorf("out-of-range index should clamp to 0, got %d", nav.Current().SelectedIdx)
}
}
func TestMoveDown(t *testing.T) {
nav := navigation.NewNavigator(sampleItems(), 0)
nav.MoveDown()
if nav.Current().SelectedIdx != 1 {
t.Errorf("expected index 1 after MoveDown, got %d", nav.Current().SelectedIdx)
}
}
func TestMoveDown_Clamps(t *testing.T) {
items := sampleItems()
nav := navigation.NewNavigator(items, len(items)-1)
nav.MoveDown()
if nav.Current().SelectedIdx != len(items)-1 {
t.Errorf("MoveDown should clamp at last item")
}
}
func TestMoveUp(t *testing.T) {
nav := navigation.NewNavigator(sampleItems(), 2)
nav.MoveUp()
if nav.Current().SelectedIdx != 1 {
t.Errorf("expected index 1 after MoveUp, got %d", nav.Current().SelectedIdx)
}
}
func TestMoveUp_Clamps(t *testing.T) {
nav := navigation.NewNavigator(sampleItems(), 0)
nav.MoveUp()
if nav.Current().SelectedIdx != 0 {
t.Errorf("MoveUp should clamp at 0")
}
}
func TestMoveFirst(t *testing.T) {
nav := navigation.NewNavigator(sampleItems(), 2)
nav.MoveFirst()
if nav.Current().SelectedIdx != 0 {
t.Errorf("MoveFirst should jump to 0")
}
}
func TestMoveLast(t *testing.T) {
nav := navigation.NewNavigator(sampleItems(), 0)
nav.MoveLast()
last := len(sampleItems()) - 1
if nav.Current().SelectedIdx != last {
t.Errorf("MoveLast should jump to %d, got %d", last, nav.Current().SelectedIdx)
}
}
func TestEnter_HasChildren(t *testing.T) {
nav := navigation.NewNavigator(sampleItems(), 0) // Item A has children
ok := nav.Enter()
if !ok {
t.Fatal("Enter on item with children should return true")
}
if nav.Depth() != 1 {
t.Errorf("expected depth 1, got %d", nav.Depth())
}
if nav.Current().Items[0].Label != "Child A1" {
t.Errorf("expected Child A1, got %s", nav.Current().Items[0].Label)
}
}
func TestEnter_NoChildren(t *testing.T) {
nav := navigation.NewNavigator(sampleItems(), 1) // Item B has no children
ok := nav.Enter()
if ok {
t.Fatal("Enter on leaf item should return false")
}
if nav.Depth() != 0 {
t.Errorf("depth should remain 0 for leaf enter")
}
}
func TestBack(t *testing.T) {
nav := navigation.NewNavigator(sampleItems(), 0)
nav.Enter()
ok := nav.Back()
if !ok {
t.Fatal("Back from submenu should return true")
}
if nav.Depth() != 0 {
t.Errorf("expected depth 0 after back, got %d", nav.Depth())
}
}
func TestBack_AtRoot(t *testing.T) {
nav := navigation.NewNavigator(sampleItems(), 0)
ok := nav.Back()
if ok {
t.Fatal("Back at root should return false")
}
}
func TestIsAtRoot(t *testing.T) {
nav := navigation.NewNavigator(sampleItems(), 0)
if !nav.IsAtRoot() {
t.Error("should be at root initially")
}
nav.Enter()
if nav.IsAtRoot() {
t.Error("should not be at root after Enter")
}
nav.Back()
if !nav.IsAtRoot() {
t.Error("should be at root after Back")
}
}
func TestSelectionPreservedAfterBack(t *testing.T) {
nav := navigation.NewNavigator(sampleItems(), 0)
nav.MoveDown() // idx=1
nav.MoveDown() // idx=2
nav.Back() // at root, nothing changes
if nav.Current().SelectedIdx != 2 {
t.Errorf("selection should be 2, got %d", nav.Current().SelectedIdx)
}
nav.MoveFirst() // idx=0
nav.Enter() // into Item A's children
nav.Back() // back to root, parent should restore idx=0
if nav.Current().SelectedIdx != 0 {
t.Errorf("after returning from submenu, root selection should be 0, got %d", nav.Current().SelectedIdx)
}
}
func TestBreadcrumbTitle(t *testing.T) {
nav := navigation.NewNavigator(sampleItems(), 0)
if nav.BreadcrumbTitle() != "" {
t.Error("breadcrumb should be empty at root")
}
nav.Enter()
if nav.BreadcrumbTitle() != "Item A" {
t.Errorf("breadcrumb should be 'Item A', got %q", nav.BreadcrumbTitle())
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

+188
View File
@@ -0,0 +1,188 @@
// Package sixel provides SIXEL graphics decoding, cell synchronization,
// and terminal rendering for the Nebara OS TUI.
package sixel
import (
"bytes"
_ "embed"
"fmt"
"image/png"
"io"
"os"
"runtime"
"sync"
"github.com/gdamore/tcell/v2"
gosixel "github.com/mattn/go-sixel"
)
// DefaultLogoPNG contains the official Nebara OS PNG logo asset.
//
//go:embed logo.png
var DefaultLogoPNG []byte
const (
// DefaultLogoCellWidth is the number of character columns reserved for the logo.
// 221px image width / ~8.5-9px cell width ≈ 25-26 columns.
DefaultLogoCellWidth = 26
// DefaultLogoCellHeight is the number of character rows reserved for the logo.
// 19px image height fits in 1 row (with breathing room from the spacer below).
DefaultLogoCellHeight = 1
)
// LogoRenderer handles encoding and rendering of the SIXEL logo.
type LogoRenderer struct {
mu sync.Mutex
sixelData []byte
pixelWidth int
pixelHeight int
cellCols int
cellRows int
enabled bool
lastX int
lastY int
hasDrawn bool
needsDraw bool
}
// NewLogoRenderer initializes the renderer using the official PNG asset.
func NewLogoRenderer() (*LogoRenderer, error) {
return NewLogoRendererFromPNG(DefaultLogoPNG)
}
// NewLogoRendererFromPNG initializes the renderer from custom PNG bytes.
func NewLogoRendererFromPNG(pngBytes []byte) (*LogoRenderer, error) {
img, err := png.Decode(bytes.NewReader(pngBytes))
if err != nil {
return nil, fmt.Errorf("decode logo PNG: %w", err)
}
bounds := img.Bounds()
pw := bounds.Dx()
ph := bounds.Dy()
var buf bytes.Buffer
enc := gosixel.NewEncoder(&buf)
enc.Transparent = true
if err := enc.Encode(img); err != nil {
return nil, fmt.Errorf("encode SIXEL: %w", err)
}
return &LogoRenderer{
sixelData: buf.Bytes(),
pixelWidth: pw,
pixelHeight: ph,
cellCols: DefaultLogoCellWidth,
cellRows: DefaultLogoCellHeight,
enabled: true,
lastX: -1,
lastY: -1,
needsDraw: true,
}, nil
}
// SetEnabled toggles SIXEL rendering.
func (r *LogoRenderer) SetEnabled(enabled bool) {
r.mu.Lock()
defer r.mu.Unlock()
r.enabled = enabled
r.needsDraw = true
}
// IsEnabled returns whether SIXEL rendering is currently active.
func (r *LogoRenderer) IsEnabled() bool {
r.mu.Lock()
defer r.mu.Unlock()
return r.enabled
}
// Invalidate forces a re-render of the SIXEL logo on the next draw cycle.
func (r *LogoRenderer) Invalidate() {
r.mu.Lock()
defer r.mu.Unlock()
r.needsDraw = true
}
// CellCols returns the number of terminal character cells the logo occupies.
func (r *LogoRenderer) CellCols() int {
return r.cellCols
}
// CellRows returns the number of terminal character rows the logo occupies.
func (r *LogoRenderer) CellRows() int {
return r.cellRows
}
// SixelDataLength returns the byte size of the cached SIXEL stream.
func (r *LogoRenderer) SixelDataLength() int {
return len(r.sixelData)
}
// Draw locks the tcell screen buffer region and outputs the SIXEL sequence to the TTY.
func (r *LogoRenderer) Draw(screen tcell.Screen, x, y int) error {
r.mu.Lock()
defer r.mu.Unlock()
if !r.enabled || len(r.sixelData) == 0 {
return nil
}
// 1. Lock the screen region in tcell buffer so tcell.Show() skips these cells.
// This prevents tcell from clearing or drawing characters over the logo.
screen.LockRegion(x, y, r.cellCols, r.cellRows, true)
// Check if we need to re-emit the SIXEL data to the terminal.
// We re-emit if position changed, or if explicitly invalidated (e.g. after resize/modal).
posChanged := (x != r.lastX || y != r.lastY)
if !r.hasDrawn || posChanged || r.needsDraw {
if err := r.emitSixel(x, y); err != nil {
return err
}
r.lastX = x
r.lastY = y
r.hasDrawn = true
r.needsDraw = false
}
return nil
}
// emitSixel writes the escape sequences and SIXEL payload to the active terminal.
func (r *LogoRenderer) emitSixel(x, y int) error {
tty, err := openTTY()
if err != nil {
return fmt.Errorf("open TTY: %w", err)
}
defer tty.Close()
// ANSI cursor positioning is 1-indexed: row = y + 1, col = x + 1.
// Sequence:
// \x1b[s : Save current cursor position
// \x1b[%d;%dH: Move to target (row, col)
// [SIXEL payload]
// \x1b[u : Restore cursor position
posSeq := fmt.Sprintf("\x1b[s\x1b[%d;%dH", y+1, x+1)
if _, err := io.WriteString(tty, posSeq); err != nil {
return err
}
if _, err := tty.Write(r.sixelData); err != nil {
return err
}
if _, err := io.WriteString(tty, "\x1b[u"); err != nil {
return err
}
return nil
}
// openTTY opens the active terminal output device directly, bypassing tcell text buffers.
func openTTY() (io.WriteCloser, error) {
if runtime.GOOS == "windows" {
return os.OpenFile("CONOUT$", os.O_WRONLY, 0)
}
return os.OpenFile("/dev/tty", os.O_WRONLY, 0)
}
+70
View File
@@ -0,0 +1,70 @@
package sixel_test
import (
"testing"
"github.com/gdamore/tcell/v2"
"github.com/nebara/nebara-os/internal/ui/sixel"
)
func TestNewLogoRenderer(t *testing.T) {
r, err := sixel.NewLogoRenderer()
if err != nil {
t.Fatalf("NewLogoRenderer failed: %v", err)
}
if r.SixelDataLength() == 0 {
t.Error("SIXEL data should not be empty")
}
if r.CellCols() != sixel.DefaultLogoCellWidth {
t.Errorf("expected CellCols %d, got %d", sixel.DefaultLogoCellWidth, r.CellCols())
}
if r.CellRows() != sixel.DefaultLogoCellHeight {
t.Errorf("expected CellRows %d, got %d", sixel.DefaultLogoCellHeight, r.CellRows())
}
if !r.IsEnabled() {
t.Error("Renderer should be enabled by default")
}
}
func TestToggleEnabled(t *testing.T) {
r, err := sixel.NewLogoRenderer()
if err != nil {
t.Fatalf("NewLogoRenderer failed: %v", err)
}
r.SetEnabled(false)
if r.IsEnabled() {
t.Error("Renderer should be disabled")
}
r.SetEnabled(true)
if !r.IsEnabled() {
t.Error("Renderer should be re-enabled")
}
}
func TestDraw_SimulationScreenLock(t *testing.T) {
r, err := sixel.NewLogoRenderer()
if err != nil {
t.Fatalf("NewLogoRenderer failed: %v", err)
}
sim := tcell.NewSimulationScreen("")
if err := sim.Init(); err != nil {
t.Fatalf("simScreen init failed: %v", err)
}
defer sim.Fini()
sim.SetSize(120, 40)
// In test environment, emitSixel may fail if CONOUT$ or /dev/tty is unavailable in CI/headless,
// but LockRegion must be executed on the screen.
_ = r.Draw(sim, 4, 1)
// Invalidate and verify
r.Invalidate()
}
+70
View File
@@ -0,0 +1,70 @@
// Package theme defines centralized color constants and text styles
// for the Nebara OS TUI design system.
package theme
import "github.com/gdamore/tcell/v2"
// Color design tokens — do not modify these values.
// They are final design specifications from the Nebara OS design system.
const (
HexBackground = "#000000"
HexPrimary = "#F5F5F5"
HexSecondary = "#BBBBBB"
HexBorder = "#BBBBBB"
HexSelectionBg = "#BBBBBB"
HexSelectionFg = "#000000"
HexWarning = "#FFCC00"
HexError = "#B30D18"
HexInfo = "#0088FF"
HexLogoAccent = "#B30D18"
)
// Resolved tcell colors from the design token hex values.
var (
ColorBackground = tcell.NewHexColor(0x000000)
ColorPrimary = tcell.NewHexColor(0xF5F5F5)
ColorSecondary = tcell.NewHexColor(0xBBBBBB)
ColorBorder = tcell.NewHexColor(0xBBBBBB)
ColorSelectionBg = tcell.NewHexColor(0xBBBBBB)
ColorSelectionFg = tcell.NewHexColor(0x000000)
ColorWarning = tcell.NewHexColor(0xFFCC00)
ColorError = tcell.NewHexColor(0xB30D18)
ColorInfo = tcell.NewHexColor(0x0088FF)
ColorLogoAccent = tcell.NewHexColor(0xB30D18)
)
// StyleNormal is the default text style: primary foreground on black.
var StyleNormal = tcell.StyleDefault.
Background(ColorBackground).
Foreground(ColorPrimary)
// StyleSecondary is used for labels and secondary information.
var StyleSecondary = tcell.StyleDefault.
Background(ColorBackground).
Foreground(ColorSecondary)
// StyleBold is used for product title and header text.
var StyleBold = tcell.StyleDefault.
Background(ColorBackground).
Foreground(ColorPrimary).
Bold(true)
// StyleSelected is the highlighted menu row style.
var StyleSelected = tcell.StyleDefault.
Background(ColorSelectionBg).
Foreground(ColorSelectionFg)
// StyleWarning applies the warning foreground color.
var StyleWarning = tcell.StyleDefault.
Background(ColorBackground).
Foreground(ColorWarning)
// StyleError applies the error foreground color.
var StyleError = tcell.StyleDefault.
Background(ColorBackground).
Foreground(ColorError)
// StyleInfo applies the info foreground color.
var StyleInfo = tcell.StyleDefault.
Background(ColorBackground).
Foreground(ColorInfo)