Push: Initial push

This commit is contained in:
2026-10-13 19:36:58 +07:00
parent 949ff7ff37
commit e4b62d1f49
17 changed files with 1970 additions and 0 deletions
+326
View File
@@ -0,0 +1,326 @@
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() {}