267 lines
7.6 KiB
Go
267 lines
7.6 KiB
Go
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)
|
||
}
|
||
}
|