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() {}