Del: Update
This commit is contained in:
@@ -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() {}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user