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