Files

303 lines
8.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}