diff --git a/internal/app/application.go b/internal/app/application.go deleted file mode 100644 index 95c809c..0000000 --- a/internal/app/application.go +++ /dev/null @@ -1,326 +0,0 @@ -package app - -import ( - "github.com/gdamore/tcell/v2" - "github.com/nebara/nebara-os/internal/ui/components" - "github.com/nebara/nebara-os/internal/ui/layout" - "github.com/nebara/nebara-os/internal/ui/theme" - "github.com/rivo/tview" -) - -// Application is the root Nebara OS TUI application object. -// It owns the tview application, all UI components, and the application state. -type Application struct { - tviewApp *tview.Application - state *State - - // tview layout primitives - pages *tview.Pages - rootFlex *tview.Flex // full-screen outer flex - contentFlex *tview.Flex // inner vertical flex with margins applied - - // Named UI components - header *components.HeaderWidget - appInfo *components.ApplianceInfoWidget - msgWidget *components.MessageWidget - menuWidget *components.MenuWidget - description *components.DescriptionWidget - footer *components.FooterWidget - - // Current layout constraints - constraints layout.Constraints - - // Minimum-size overlay: when true we only show the size warning. - tooSmall bool -} - -// NewApplication constructs the complete TUI application from application state. -func NewApplication(state *State) *Application { - a := &Application{ - tviewApp: tview.NewApplication(), - state: state, - } - a.tviewApp.EnableMouse(false) - a.buildUI() - a.wireCallbacks() - a.syncDescription() - return a -} - -// Run starts the TUI event loop and blocks until the application exits. -func (a *Application) Run() error { - return a.tviewApp.Run() -} - -// buildUI constructs all widgets and assembles the layout. -func (a *Application) buildUI() { - s := a.state - - // --- Widgets --- - a.header = components.NewHeaderWidget(s.Appliance.Version, 8) - a.appInfo = components.NewApplianceInfoWidget(s.Appliance) - a.msgWidget = components.NewMessageWidget() - a.msgWidget.SetMessage(s.Message) - - // Determine initial inner padding for menu. - a.menuWidget = components.NewMenuWidget( - a.state.Navigator.Current().Items, - 2, - ) - a.menuWidget.SetSelectedIndex(a.state.Navigator.Current().SelectedIdx) - - a.description = components.NewDescriptionWidget() - a.footer = components.NewFooterWidget() - - // --- Assemble layout --- - a.contentFlex = a.buildContentFlex() - - // Outer flex: left margin | content | right margin - a.rootFlex = tview.NewFlex().SetDirection(tview.FlexColumn) - a.rootFlex.SetBackgroundColor(theme.ColorBackground) - - a.pages = tview.NewPages() - a.pages.SetBackgroundColor(theme.ColorBackground) - a.pages.AddPage("main", a.rootFlex, true, true) - - // Initial layout pass — will be recalculated on first draw/resize. - a.applyConstraints(layout.Calculate(120, 40)) - - a.tviewApp.SetRoot(a.pages, true) - a.tviewApp.SetFocus(a.menuWidget) - - // Handle terminal resize. - a.tviewApp.SetBeforeDrawFunc(func(screen tcell.Screen) bool { - tw, th := screen.Size() - c := layout.Calculate(tw, th) - if c.IsMinimum != a.tooSmall || - c.MarginH != a.constraints.MarginH || - c.MarginV != a.constraints.MarginV || - c.ContentWidth != a.constraints.ContentWidth { - a.applyConstraints(c) - } - return false - }) - - // Global key: Ctrl+C to quit, F1 for help. - a.tviewApp.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - switch event.Key() { - case tcell.KeyCtrlC: - a.tviewApp.Stop() - return nil - case tcell.KeyF1: - a.showHelp() - return nil - } - return event - }) -} - -// buildContentFlex assembles the vertical content stack (without outer margins). -func (a *Application) buildContentFlex() *tview.Flex { - // Spacer utility. - spacer := func(rows int) tview.Primitive { - box := tview.NewBox().SetBackgroundColor(theme.ColorBackground) - _ = rows - return box - } - - // Determine menu height: 2 rows per item (text + blank) + 2 border rows + 2 inset rows. - menuHeight := len(a.state.Navigator.Current().Items)*2 + 4 - - // Message row: always 1 row tall (hidden by making it 0 when not visible). - msgHeight := 0 - if a.state.Message.IsVisible() { - msgHeight = 1 - } - - cf := tview.NewFlex().SetDirection(tview.FlexRow) - cf.SetBackgroundColor(theme.ColorBackground) - - // Row 0: top vertical margin (spacer). - cf.AddItem(spacer(1), 1, 0, false) // placeholder; overridden in applyConstraints - - // Row 1: Header. - cf.AddItem(a.header, 1, 0, false) - - // Row 2: blank line after header. - cf.AddItem(spacer(1), 1, 0, false) - - // Row 3: Appliance info block (product title + blank + 4 metadata rows = 6 rows). - cf.AddItem(a.appInfo, 6, 0, false) - - // Row 4: Message row (0 or 1 row). - cf.AddItem(a.msgWidget, msgHeight, 0, false) - - // Row 5: blank line before menu. - cf.AddItem(spacer(1), 1, 0, false) - - // Row 6: Navigation menu. - cf.AddItem(a.menuWidget, menuHeight, 0, false) - - // Row 7: Flexible whitespace — expands to consume remaining vertical space. - cf.AddItem(spacer(0), 0, 1, false) - - // Row 8: Contextual description. - cf.AddItem(a.description, 1, 0, false) - - // Row 9: blank between description and footer. - cf.AddItem(spacer(1), 1, 0, false) - - // Row 10: Footer. - cf.AddItem(a.footer, 1, 0, false) - - // Row 11: bottom vertical margin. - cf.AddItem(spacer(1), 1, 0, false) - - return cf -} - -// applyConstraints rebuilds margins and updates child widget parameters. -func (a *Application) applyConstraints(c layout.Constraints) { - a.constraints = c - a.tooSmall = c.IsMinimum - - // Clear and rebuild the outer flex. - a.rootFlex.Clear() - if c.IsMinimum { - // Show a minimal warning box. - warn := tview.NewBox().SetBackgroundColor(theme.ColorBackground) - a.rootFlex.AddItem(warn, 0, 1, false) - a.tviewApp.SetFocus(warn) - return - } - - // Update header margin. - a.header.SetMarginH(c.MarginH) - a.header.Refresh(c.TermWidth) - - // Update footer density. - a.footer.Refresh(c.TermWidth) - - // Update menu inner padding. - a.menuWidget.SetInnerPad(c.MenuInnerPad) - - // Update menu height based on current items. - menuItemCount := len(a.state.Navigator.Current().Items) - menuHeight := menuItemCount*2 + 4 - - // Message visibility. - msgHeight := 0 - if a.state.Message.IsVisible() && c.MessageVisible { - msgHeight = 1 - } - - // Rebuild content flex items with updated sizes. - // We rebuild the whole content flex to keep sizing in sync. - a.contentFlex = a.rebuildContentFlex(c, menuHeight, msgHeight) - - // Outer layout: left margin | content | right margin. - a.rootFlex.AddItem(tview.NewBox().SetBackgroundColor(theme.ColorBackground), c.MarginH, 0, false) - a.rootFlex.AddItem(a.contentFlex, 0, 1, true) - a.rootFlex.AddItem(tview.NewBox().SetBackgroundColor(theme.ColorBackground), c.MarginH, 0, false) - - a.tviewApp.SetFocus(a.menuWidget) -} - -// rebuildContentFlex constructs a fresh vertical flex with updated sizing. -func (a *Application) rebuildContentFlex(c layout.Constraints, menuHeight, msgHeight int) *tview.Flex { - spacer := func() *tview.Box { - return tview.NewBox().SetBackgroundColor(theme.ColorBackground) - } - - cf := tview.NewFlex().SetDirection(tview.FlexRow) - cf.SetBackgroundColor(theme.ColorBackground) - - cf.AddItem(spacer(), c.MarginV, 0, false) - cf.AddItem(a.header, 1, 0, false) - cf.AddItem(spacer(), 1, 0, false) - cf.AddItem(a.appInfo, 6, 0, false) - - if c.MessageVisible { - cf.AddItem(a.msgWidget, msgHeight, 0, false) - } - - cf.AddItem(spacer(), 1, 0, false) - cf.AddItem(a.menuWidget, menuHeight, 0, true) - cf.AddItem(spacer(), 0, 1, false) // flexible whitespace - cf.AddItem(a.description, 1, 0, false) - cf.AddItem(spacer(), 1, 0, false) - cf.AddItem(a.footer, 1, 0, false) - cf.AddItem(spacer(), c.MarginV, 0, false) - - return cf -} - -// wireCallbacks connects navigation events to UI updates. -func (a *Application) wireCallbacks() { - a.menuWidget.SetOnSelectionChanged(func(idx int) { - a.state.Navigator.Current().SelectedIdx = idx - a.syncDescription() - }) - - a.menuWidget.SetOnEnter(func(idx int) { - item := a.state.Navigator.SelectedItem() - if item == nil { - return - } - if !a.state.Navigator.Enter() { - // Leaf item — show not-implemented notice. - a.showNotImplemented(item.Label) - return - } - // Descended into submenu: refresh menu widget. - a.refreshMenuForCurrentLevel() - }) - - a.menuWidget.SetOnBack(func() { - if a.state.Navigator.Back() { - a.refreshMenuForCurrentLevel() - } - }) -} - -// refreshMenuForCurrentLevel updates the menu widget to reflect the current -// navigation level and recalculates layout. -func (a *Application) refreshMenuForCurrentLevel() { - cur := a.state.Navigator.Current() - a.menuWidget.SetItems(cur.Items) - a.menuWidget.SetSelectedIndex(cur.SelectedIdx) - a.syncDescription() - // Recalculate layout with new menu item count. - a.applyConstraints(a.constraints) -} - -// syncDescription updates the contextual description for the selected item. -func (a *Application) syncDescription() { - item := a.state.Navigator.SelectedItem() - if item == nil { - a.description.SetDescription("") - return - } - a.description.SetDescription(item.Description) -} - -// showNotImplemented displays the not-implemented modal. -func (a *Application) showNotImplemented(label string) { - modal := components.NewNotImplementedModal(label, func() { - a.pages.RemovePage("modal") - a.tviewApp.SetFocus(a.menuWidget) - }) - a.pages.AddPage("modal", modal, true, true) - a.tviewApp.SetFocus(modal) -} - -// showHelp displays the help modal. -func (a *Application) showHelp() { - helpModal := components.HelpModal(func() { - a.pages.RemovePage("help") - a.tviewApp.SetFocus(a.menuWidget) - }) - a.pages.AddPage("help", helpModal, true, true) - a.tviewApp.SetFocus(helpModal) -} - -// SetInnerPad exposes the inner padding setter on MenuWidget for use by app. -// (Defined here so app package can call it without modifying the component.) -func init() {} diff --git a/internal/app/state.go b/internal/app/state.go deleted file mode 100644 index 74d3158..0000000 --- a/internal/app/state.go +++ /dev/null @@ -1,28 +0,0 @@ -// Package app wires together the Nebara OS TUI application. -package app - -import ( - "github.com/nebara/nebara-os/internal/mock" - "github.com/nebara/nebara-os/internal/model" - "github.com/nebara/nebara-os/internal/ui/navigation" -) - -// State holds the complete runtime state of the TUI application. -type State struct { - Appliance model.ApplianceInfo - Message *model.SystemMessage - Navigator *navigation.Navigator - ShowModal bool - ModalLabel string - ShowHelp bool -} - -// NewStateFromMock constructs application state from a mock scenario. -func NewStateFromMock(s mock.State) *State { - nav := navigation.NewNavigator(model.MainMenu(), s.InitialMenu) - return &State{ - Appliance: s.Appliance, - Message: s.Message, - Navigator: nav, - } -} diff --git a/internal/mock/states.go b/internal/mock/states.go deleted file mode 100644 index 9d9481c..0000000 --- a/internal/mock/states.go +++ /dev/null @@ -1,48 +0,0 @@ -// Package mock provides predefined appliance states for prototype validation. -package mock - -import ( - "github.com/nebara/nebara-os/internal/model" -) - -// State bundles all data required to render a complete TUI screen. -type State struct { - Appliance model.ApplianceInfo - Message *model.SystemMessage - InitialMenu int // zero-based index into model.MainMenu() -} - -// WarningState reproduces Figma mock state A. -func WarningState() State { - return State{ - Appliance: model.ApplianceInfo{ - ProductName: "Nebara S3GX Appliance", - Version: "1.0.1", - Hostname: "jkt-s3gx-lin-01.domain.local", - Address: "10.249.23.13/26", - Uptime: "132d 12h 32m", - Status: "Healthy", - }, - Message: &model.SystemMessage{ - Severity: model.SeverityWarning, - Text: "[Warn] Update Available to version 2.1.4!", - }, - InitialMenu: 2, // Security & Access - } -} - -// NormalState reproduces Figma mock state B. -func NormalState() State { - return State{ - Appliance: model.ApplianceInfo{ - ProductName: "Nebara S3GX Appliance", - Version: "2.1.4", - Hostname: "sin-s3gx-lin-05.domain.local", - Address: "10.175.15.11/25", - Uptime: "111d 14h 21m", - Status: "Healthy", - }, - Message: nil, - InitialMenu: 3, // System Maintenance - } -} diff --git a/internal/mock/states_test.go b/internal/mock/states_test.go deleted file mode 100644 index 55fbc68..0000000 --- a/internal/mock/states_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package mock_test - -import ( - "testing" - - "github.com/nebara/nebara-os/internal/mock" - "github.com/nebara/nebara-os/internal/model" -) - -func TestWarningState(t *testing.T) { - s := mock.WarningState() - - if s.Appliance.ProductName == "" { - t.Error("ProductName should not be empty") - } - if s.Appliance.Version != "1.0.1" { - t.Errorf("expected version 1.0.1, got %s", s.Appliance.Version) - } - if s.Message == nil { - t.Fatal("warning state should have a message") - } - if s.Message.Severity != model.SeverityWarning { - t.Error("expected SeverityWarning") - } - if !s.Message.IsVisible() { - t.Error("warning message should be visible") - } - if s.InitialMenu < 0 || s.InitialMenu >= 6 { - t.Errorf("InitialMenu out of range: %d", s.InitialMenu) - } -} - -func TestNormalState(t *testing.T) { - s := mock.NormalState() - - if s.Appliance.ProductName == "" { - t.Error("ProductName should not be empty") - } - if s.Appliance.Version != "2.1.4" { - t.Errorf("expected version 2.1.4, got %s", s.Appliance.Version) - } - if s.Message != nil && s.Message.IsVisible() { - t.Error("normal state should have no visible message") - } - if s.InitialMenu < 0 || s.InitialMenu >= 6 { - t.Errorf("InitialMenu out of range: %d", s.InitialMenu) - } -} - -func TestWarningState_CorrectMenuSelection(t *testing.T) { - s := mock.WarningState() - menu := model.MainMenu() - if s.InitialMenu >= len(menu) { - t.Fatalf("InitialMenu %d exceeds menu length %d", s.InitialMenu, len(menu)) - } - if menu[s.InitialMenu].Label != "Security & Access" { - t.Errorf("warning state should select 'Security & Access', got %q", menu[s.InitialMenu].Label) - } -} - -func TestNormalState_CorrectMenuSelection(t *testing.T) { - s := mock.NormalState() - menu := model.MainMenu() - if s.InitialMenu >= len(menu) { - t.Fatalf("InitialMenu %d exceeds menu length %d", s.InitialMenu, len(menu)) - } - if menu[s.InitialMenu].Label != "System Maintenance" { - t.Errorf("normal state should select 'System Maintenance', got %q", menu[s.InitialMenu].Label) - } -} - -func TestSystemMessage_IsVisible_Nil(t *testing.T) { - var m *model.SystemMessage - if m.IsVisible() { - t.Error("nil message should not be visible") - } -} - -func TestSystemMessage_IsVisible_Empty(t *testing.T) { - m := &model.SystemMessage{Severity: model.SeverityWarning, Text: ""} - if m.IsVisible() { - t.Error("empty text message should not be visible") - } -} - -func TestSystemMessage_IsVisible_SeverityNone(t *testing.T) { - m := &model.SystemMessage{Severity: model.SeverityNone, Text: "some text"} - if m.IsVisible() { - t.Error("SeverityNone message should not be visible") - } -} diff --git a/internal/model/appliance.go b/internal/model/appliance.go deleted file mode 100644 index 60cb53c..0000000 --- a/internal/model/appliance.go +++ /dev/null @@ -1,12 +0,0 @@ -// Package model defines the data structures for the Nebara OS TUI. -package model - -// ApplianceInfo holds the display data for the appliance information section. -type ApplianceInfo struct { - ProductName string - Version string - Hostname string - Address string - Uptime string - Status string -} diff --git a/internal/model/menu.go b/internal/model/menu.go deleted file mode 100644 index b503e45..0000000 --- a/internal/model/menu.go +++ /dev/null @@ -1,82 +0,0 @@ -package model - -// MenuItem represents a single entry in any menu level. -type MenuItem struct { - Label string - Description string - Children []MenuItem -} - -// MainMenu returns the complete main menu data structure. -func MainMenu() []MenuItem { - return []MenuItem{ - { - Label: "System Configuration", - Description: "Configure appliance hostname, system identity, time and general settings.", - Children: []MenuItem{ - {Label: "Hostname & Domain", Description: "Configure the appliance hostname and domain name."}, - {Label: "Date & Time", Description: "Set the system date and time."}, - {Label: "NTP Configuration", Description: "Configure Network Time Protocol servers."}, - {Label: "System Information", Description: "View system hardware and software information."}, - {Label: "Appliance Identity", Description: "Configure appliance identity and asset tags."}, - }, - }, - { - Label: "Network Configuration", - Description: "Configure management interfaces, IP addressing, DNS and network connectivity.", - Children: []MenuItem{ - {Label: "Management Interfaces", Description: "Configure management network interfaces."}, - {Label: "IPv4 Configuration", Description: "Configure IPv4 addresses and subnet masks."}, - {Label: "DNS Configuration", Description: "Configure DNS resolver settings."}, - {Label: "Default Gateway & Routes", Description: "Configure default gateway and static routes."}, - {Label: "VLAN Configuration", Description: "Configure VLAN tagging and interfaces."}, - {Label: "Network Bonding", Description: "Configure interface bonding and aggregation."}, - {Label: "Connectivity Test", Description: "Test network connectivity to targets."}, - }, - }, - { - Label: "Security & Access", - Description: "Manage user accounts, authentication and remote access.", - Children: []MenuItem{ - {Label: "Local Users", Description: "Manage local user accounts."}, - {Label: "Authentication Settings", Description: "Configure authentication methods."}, - {Label: "Password Policy", Description: "Configure password complexity and expiry policies."}, - {Label: "Remote Access", Description: "Configure SSH and remote management access."}, - {Label: "Session Management", Description: "View and manage active sessions."}, - }, - }, - { - Label: "System Maintenance", - Description: "Manage appliance updates, configuration backups and maintenance tasks.", - Children: []MenuItem{ - {Label: "Appliance Updates", Description: "Check for and apply appliance software updates."}, - {Label: "Configuration Backup", Description: "Back up the current appliance configuration."}, - {Label: "Configuration Restore", Description: "Restore appliance configuration from a backup."}, - {Label: "System Restart", Description: "Safely restart the appliance."}, - {Label: "System Shutdown", Description: "Safely shut down the appliance."}, - }, - }, - { - Label: "Diagnostics", - Description: "View system health, diagnostics and troubleshooting information.", - Children: []MenuItem{ - {Label: "System Health", Description: "View overall system health status."}, - {Label: "Hardware Information", Description: "View hardware component information."}, - {Label: "Network Diagnostics", Description: "Run network diagnostic tools."}, - {Label: "Service Status", Description: "View the status of system services."}, - {Label: "System Logs", Description: "Browse and filter system log entries."}, - }, - }, - { - Label: "Support & Recovery", - Description: "Access support tools, recovery options and troubleshooting functions.", - Children: []MenuItem{ - {Label: "Support Information", Description: "View support contact and licensing information."}, - {Label: "Diagnostic Bundle", Description: "Generate a diagnostic bundle for support."}, - {Label: "Recovery Options", Description: "Access factory reset and recovery options."}, - {Label: "Troubleshooting Shell", Description: "Not available in this prototype."}, - {Label: "Support Access", Description: "Manage remote support access credentials."}, - }, - }, - } -} diff --git a/internal/model/message.go b/internal/model/message.go deleted file mode 100644 index 5847089..0000000 --- a/internal/model/message.go +++ /dev/null @@ -1,23 +0,0 @@ -package model - -// MessageSeverity classifies the severity of a system message. -type MessageSeverity int - -const ( - // SeverityNone means no message is active. - SeverityNone MessageSeverity = iota - SeverityWarning - SeverityError - SeverityInfo -) - -// SystemMessage represents an optional system-level status message. -type SystemMessage struct { - Severity MessageSeverity - Text string -} - -// IsVisible returns true when there is an active message to display. -func (m *SystemMessage) IsVisible() bool { - return m != nil && m.Severity != SeverityNone && m.Text != "" -} diff --git a/internal/ui/components/header.go b/internal/ui/components/header.go deleted file mode 100644 index 7ce14fd..0000000 --- a/internal/ui/components/header.go +++ /dev/null @@ -1,266 +0,0 @@ -package components - -import ( - "fmt" - "strings" - - "github.com/gdamore/tcell/v2" - "github.com/nebara/nebara-os/internal/model" - "github.com/nebara/nebara-os/internal/ui/theme" - "github.com/rivo/tview" -) - -// HeaderWidget renders the NEBARA - OS logo and version on the same row. -// The "O" character in "OS" is rendered in the logo accent color (#B30D18). -// All other characters use the primary foreground (#F5F5F5). -// -// Terminal limitation: tview.TextView supports color tags for mixed-color -// text on a single line, which we use here. -type HeaderWidget struct { - *tview.TextView - version string - marginH int -} - -// NewHeaderWidget creates the header with the given version string and margin. -func NewHeaderWidget(version string, marginH int) *HeaderWidget { - tv := tview.NewTextView(). - SetDynamicColors(true). - SetScrollable(false) - tv.SetBackgroundColor(theme.ColorBackground) - - h := &HeaderWidget{ - TextView: tv, - version: version, - marginH: marginH, - } - 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 -} - -// Refresh recalculates the header text for the current terminal width. -func (h *HeaderWidget) Refresh(termWidth int) { - h.refresh(termWidth) -} - -func (h *HeaderWidget) refresh(termWidth int) { - // Logo: "NEBARA - OS" with "O" in accent red. - // tview color tag format: [#RRGGBB] - logoPlain := "NEBARA - " - // "O" accent, "S" back to primary - logoColored := fmt.Sprintf( - "[%s]%s[%s]O[%s]S", - theme.HexPrimary, - logoPlain, - theme.HexLogoAccent, - theme.HexPrimary, - ) - - versionText := "Version " + h.version - - // The content width between the margins for text layout purposes. - // Visible character count for the logo is 11 ("NEBARA - OS"). - // Visible character count for version: len("Version ") + len(version). - logoVisibleLen := len("NEBARA - OS") - versionVisibleLen := len(versionText) - contentWidth := termWidth - h.marginH*2 - - // Number of spaces between logo and version to right-align version. - 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) - } -} diff --git a/internal/ui/components/menu.go b/internal/ui/components/menu.go deleted file mode 100644 index f4d4a97..0000000 --- a/internal/ui/components/menu.go +++ /dev/null @@ -1,211 +0,0 @@ -// 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) - } -} diff --git a/internal/ui/layout/responsive.go b/internal/ui/layout/responsive.go deleted file mode 100644 index d14b318..0000000 --- a/internal/ui/layout/responsive.go +++ /dev/null @@ -1,93 +0,0 @@ -// 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 -} diff --git a/internal/ui/layout/responsive_test.go b/internal/ui/layout/responsive_test.go deleted file mode 100644 index f80b188..0000000 --- a/internal/ui/layout/responsive_test.go +++ /dev/null @@ -1,73 +0,0 @@ -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) - } -} diff --git a/internal/ui/navigation/navigator.go b/internal/ui/navigation/navigator.go deleted file mode 100644 index 9f1ae41..0000000 --- a/internal/ui/navigation/navigator.go +++ /dev/null @@ -1,127 +0,0 @@ -// 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 -} diff --git a/internal/ui/navigation/navigator_test.go b/internal/ui/navigation/navigator_test.go deleted file mode 100644 index ff8fe54..0000000 --- a/internal/ui/navigation/navigator_test.go +++ /dev/null @@ -1,170 +0,0 @@ -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()) - } -} diff --git a/internal/ui/theme/colors.go b/internal/ui/theme/colors.go deleted file mode 100644 index 924b645..0000000 --- a/internal/ui/theme/colors.go +++ /dev/null @@ -1,70 +0,0 @@ -// 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)