Commit: Initial Update

This commit is contained in:
2026-10-13 20:35:22 +07:00
parent f1c253b028
commit 43b8bfa253
24 changed files with 2440 additions and 0 deletions
+302
View File
@@ -0,0 +1,302 @@
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)
}
}
+211
View File
@@ -0,0 +1,211 @@
// Package components provides custom tview primitives for the Nebara OS TUI.
// Each component is self-contained and themed using the centralized design system.
package components
import (
"github.com/gdamore/tcell/v2"
"github.com/nebara/nebara-os/internal/model"
"github.com/nebara/nebara-os/internal/ui/theme"
"github.com/rivo/tview"
)
// MenuWidget is a custom tview.Primitive that renders the navigation menu
// with full-width row highlighting, a single-cell border, and inner padding.
// It implements the exact Figma selection design: full-width BBBBBB background.
type MenuWidget struct {
*tview.Box
items []model.MenuItem
selectedIdx int
innerPad int // horizontal cells of padding inside the border
// onSelectionChanged is called whenever the selected index changes.
onSelectionChanged func(idx int)
// onEnter is called when the user presses Enter on an item.
onEnter func(idx int)
// onBack is called when the user presses Escape.
onBack func()
}
// NewMenuWidget creates a MenuWidget with the given items and padding.
func NewMenuWidget(items []model.MenuItem, innerPad int) *MenuWidget {
m := &MenuWidget{
Box: tview.NewBox(),
items: items,
innerPad: innerPad,
}
m.Box.SetBackgroundColor(theme.ColorBackground)
m.Box.SetBorderColor(theme.ColorBorder)
m.Box.SetBorder(true)
return m
}
// SetInnerPad updates the horizontal padding inside the border.
func (m *MenuWidget) SetInnerPad(pad int) {
m.innerPad = pad
}
// SetItems replaces the current menu items.
func (m *MenuWidget) SetItems(items []model.MenuItem) {
m.items = items
if m.selectedIdx >= len(items) {
m.selectedIdx = 0
}
}
// SetSelectedIndex sets the highlighted row.
func (m *MenuWidget) SetSelectedIndex(idx int) {
if idx >= 0 && idx < len(m.items) {
m.selectedIdx = idx
}
}
// SelectedIndex returns the current highlighted row index.
func (m *MenuWidget) SelectedIndex() int {
return m.selectedIdx
}
// SetOnSelectionChanged registers a callback invoked on selection movement.
func (m *MenuWidget) SetOnSelectionChanged(fn func(int)) {
m.onSelectionChanged = fn
}
// SetOnEnter registers a callback invoked when Enter is pressed.
func (m *MenuWidget) SetOnEnter(fn func(int)) {
m.onEnter = fn
}
// SetOnBack registers a callback invoked when Escape is pressed.
func (m *MenuWidget) SetOnBack(fn func()) {
m.onBack = fn
}
// Draw renders the menu widget.
// The border is drawn by tview.Box; we draw the items in the inner area.
func (m *MenuWidget) Draw(screen tcell.Screen) {
// Draw the box border first.
m.Box.Draw(screen)
// Get the inner area (inside the border).
x, y, w, h := m.Box.GetInnerRect()
if w <= 0 || h <= 0 {
return
}
// Render each item. Each item occupies exactly one row with one blank row
// between items for visual breathing room, matching the Figma layout.
// We use a 1-cell vertical inset inside the border (top and bottom).
innerY := y + 1 // 1-cell top inset
innerH := h - 2 // reserve 1-cell bottom inset
_ = innerH
for i, item := range m.items {
row := innerY + i*2 // 2 rows per item: text + blank separator
if row >= y+h-1 {
break // do not overflow the border bottom
}
isSelected := i == m.selectedIdx
var fg, bg tcell.Color
var bold bool
if isSelected {
fg = theme.ColorSelectionFg
bg = theme.ColorSelectionBg
bold = false
} else {
fg = theme.ColorPrimary
bg = theme.ColorBackground
bold = false
}
style := tcell.StyleDefault.Background(bg).Foreground(fg)
if bold {
style = style.Bold(true)
}
// Build the label: ">" prefix for selected, " " otherwise.
prefix := " "
if isSelected {
prefix = "> "
}
label := prefix + item.Label
// Fill the entire row width with the selection background.
// This gives the full-width highlight shown in the mockup.
fillRow(screen, x, row, w, style)
// Draw the label text (with left padding inside the border).
textX := x + m.innerPad
drawText(screen, textX, row, label, style)
}
}
// InputHandler handles keyboard events for the menu widget.
func (m *MenuWidget) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
return m.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
switch event.Key() {
case tcell.KeyUp:
if m.selectedIdx > 0 {
m.selectedIdx--
if m.onSelectionChanged != nil {
m.onSelectionChanged(m.selectedIdx)
}
}
case tcell.KeyDown:
if m.selectedIdx < len(m.items)-1 {
m.selectedIdx++
if m.onSelectionChanged != nil {
m.onSelectionChanged(m.selectedIdx)
}
}
case tcell.KeyHome:
if m.selectedIdx != 0 {
m.selectedIdx = 0
if m.onSelectionChanged != nil {
m.onSelectionChanged(m.selectedIdx)
}
}
case tcell.KeyEnd:
last := len(m.items) - 1
if m.selectedIdx != last {
m.selectedIdx = last
if m.onSelectionChanged != nil {
m.onSelectionChanged(m.selectedIdx)
}
}
case tcell.KeyEnter:
if m.onEnter != nil {
m.onEnter(m.selectedIdx)
}
case tcell.KeyEscape:
if m.onBack != nil {
m.onBack()
}
}
})
}
// HasFocus is required to satisfy tview.Primitive.
func (m *MenuWidget) HasFocus() bool {
return m.Box.HasFocus()
}
// Focus is required to satisfy tview.Primitive.
func (m *MenuWidget) Focus(delegate func(p tview.Primitive)) {
m.Box.Focus(delegate)
}
// fillRow paints an entire horizontal row with the given style.
func fillRow(screen tcell.Screen, x, y, width int, style tcell.Style) {
for col := x; col < x+width; col++ {
screen.SetContent(col, y, ' ', nil, style)
}
}
// drawText writes a string starting at (x, y) using the given style.
func drawText(screen tcell.Screen, x, y int, text string, style tcell.Style) {
for i, ch := range text {
screen.SetContent(x+i, y, ch, nil, style)
}
}