package tui import ( "fmt" "strings" "bee/audit/internal/platform" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) // Column widths for two-column main layout. const leftColWidth = 30 var ( stylePass = lipgloss.NewStyle().Foreground(lipgloss.Color("10")) // bright green styleFail = lipgloss.NewStyle().Foreground(lipgloss.Color("9")) // bright red styleCancel = lipgloss.NewStyle().Foreground(lipgloss.Color("11")) // bright yellow styleNA = lipgloss.NewStyle().Foreground(lipgloss.Color("8")) // dark gray ) func colorStatus(status string) string { switch status { case "PASS": return stylePass.Render("PASS") case "FAIL": return styleFail.Render("FAIL") case "CANCEL": return styleCancel.Render("CANC") default: return styleNA.Render("N/A ") } } func (m model) View() string { var body string if m.busy { title := "bee" if m.busyTitle != "" { title = m.busyTitle } if len(m.progressLines) > 0 { var b strings.Builder fmt.Fprintf(&b, "%s\n\n", title) for _, l := range m.progressLines { fmt.Fprintf(&b, " %s\n", l) } b.WriteString("\n[ctrl+c] quit\n") body = b.String() } else { body = fmt.Sprintf("%s\n\nWorking...\n\n[ctrl+c] quit\n", title) } } else { switch m.screen { case screenMain: body = renderTwoColumnMain(m) case screenHealthCheck: body = renderHealthCheck(m) case screenBurnInTests: body = renderBurnInTests(m) case screenSettings: body = renderMenu("Settings", "Select action", m.settingsMenu, m.cursor) case screenNetwork: body = renderMenu("Network", "Select action", m.networkMenu, m.cursor) case screenServices: body = renderMenu("Services", "Select service", m.services, m.cursor) case screenServiceAction: body = renderMenu("Service: "+m.selectedService, "Select action", m.serviceMenu, m.cursor) case screenExportTargets: body = renderMenu( "Export support bundle", "Select writable removable filesystem (read-only/boot media hidden)", renderTargetItems(m.targets), m.cursor, ) case screenInterfacePick: body = renderMenu("Interfaces", "Select interface", renderInterfaceItems(m.interfaces), m.cursor) case screenTools: body = renderMenu("Tools", "Select action", m.toolsMenu, m.cursor) case screenInstallDiskPick: body = renderInstallDiskPick(m) case screenStaticForm: body = renderForm("Static IPv4: "+m.selectedIface, m.formFields, m.formIndex) case screenConfirm: title, confirmBody := m.confirmBody() body = renderConfirm(title, confirmBody, m.cursor) case screenNvidiaSATSetup: body = renderNvidiaSATSetup(m) case screenNvidiaSATRunning: body = renderNvidiaSATRunning() case screenGPUStressRunning: body = renderGPUStressRunning(m) case screenOutput: body = fmt.Sprintf("%s\n\n%s\n\n[enter/esc] back [ctrl+c] quit\n", m.title, strings.TrimSpace(m.body)) default: body = "bee\n" } } return m.renderWithBanner(body) } // renderTwoColumnMain renders the main screen with menu on the left and hardware panel on the right. func renderTwoColumnMain(m model) string { // Left column lines leftLines := []string{"bee", ""} for i, item := range m.mainMenu { pfx := " " if !m.panelFocus && m.cursor == i { pfx = "> " } leftLines = append(leftLines, pfx+item) } // Right column lines rightLines := buildPanelLines(m) // Render side by side var b strings.Builder maxRows := max(len(leftLines), len(rightLines)) for i := 0; i < maxRows; i++ { l := "" if i < len(leftLines) { l = leftLines[i] } r := "" if i < len(rightLines) { r = rightLines[i] } w := lipgloss.Width(l) if w < leftColWidth { l += strings.Repeat(" ", leftColWidth-w) } b.WriteString(l + " │ " + r + "\n") } sep := strings.Repeat("─", leftColWidth) + "─┴─" + strings.Repeat("─", 46) b.WriteString(sep + "\n") if m.panelFocus { b.WriteString("[↑↓] move [enter] details [tab/←] menu [ctrl+c] quit\n") } else { b.WriteString("[↑↓] move [enter] select [tab/→] panel [ctrl+c] quit\n") } return b.String() } func buildPanelLines(m model) []string { p := m.panel var lines []string for _, h := range p.Header { lines = append(lines, h) } if len(p.Header) > 0 && len(p.Rows) > 0 { lines = append(lines, "") } for i, row := range p.Rows { pfx := " " if m.panelFocus && m.panelCursor == i { pfx = "> " } status := colorStatus(row.Status) lines = append(lines, fmt.Sprintf("%s%s %-4s %s", pfx, status, row.Key, row.Detail)) } return lines } func renderTargetItems(targets []platform.RemovableTarget) []string { items := make([]string, 0, len(targets)) for _, target := range targets { desc := fmt.Sprintf("%s [%s %s]", target.Device, target.FSType, target.Size) if target.Label != "" { desc += " label=" + target.Label } if target.Mountpoint != "" { desc += " mounted=" + target.Mountpoint } items = append(items, desc) } return items } func renderInterfaceItems(interfaces []platform.InterfaceInfo) []string { items := make([]string, 0, len(interfaces)) for _, iface := range interfaces { label := iface.Name if len(iface.IPv4) > 0 { label += " [" + strings.Join(iface.IPv4, ", ") + "]" } items = append(items, label) } return items } func renderMenu(title, subtitle string, items []string, cursor int) string { var body strings.Builder fmt.Fprintf(&body, "%s\n\n%s\n\n", title, subtitle) if len(items) == 0 { body.WriteString("(no items)\n") } else { for i, item := range items { prefix := " " if i == cursor { prefix = "> " } fmt.Fprintf(&body, "%s%s\n", prefix, item) } } body.WriteString("\n[↑/↓] move [enter] select [esc] back [ctrl+c] quit\n") return body.String() } func renderForm(title string, fields []formField, idx int) string { var body strings.Builder fmt.Fprintf(&body, "%s\n\n", title) for i, field := range fields { prefix := " " if i == idx { prefix = "> " } fmt.Fprintf(&body, "%s%s: %s\n", prefix, field.Label, field.Value) } body.WriteString("\n[tab/↑/↓] move [enter] next/submit [backspace] delete [esc] cancel\n") return body.String() } func renderConfirm(title, body string, cursor int) string { options := []string{"Confirm", "Cancel"} var out strings.Builder fmt.Fprintf(&out, "%s\n\n%s\n\n", title, body) for i, option := range options { prefix := " " if i == cursor { prefix = "> " } fmt.Fprintf(&out, "%s%s\n", prefix, option) } out.WriteString("\n[←/→/↑/↓] move [enter] select [esc] cancel\n") return out.String() } func resultCmd(title, body string, err error, back screen) tea.Cmd { return func() tea.Msg { return resultMsg{title: title, body: body, err: err, back: back} } } func (m model) renderWithBanner(body string) string { body = strings.TrimRight(body, "\n") banner := renderBannerModule(m.banner, m.width) if banner == "" { if body == "" { return "" } return body + "\n" } if body == "" { return banner + "\n" } return banner + "\n\n" + body + "\n" } func renderBannerModule(banner string, width int) string { banner = strings.TrimSpace(banner) if banner == "" { return "" } lines := strings.Split(banner, "\n") contentWidth := 0 for _, line := range lines { if w := lipgloss.Width(line); w > contentWidth { contentWidth = w } } if width > 0 && width-4 > contentWidth { contentWidth = width - 4 } if contentWidth < 20 { contentWidth = 20 } label := " MOTD " topFill := contentWidth + 2 - lipgloss.Width(label) if topFill < 0 { topFill = 0 } var b strings.Builder b.WriteString("┌" + label + strings.Repeat("─", topFill) + "┐\n") for _, line := range lines { b.WriteString("│ " + padRight(line, contentWidth) + " │\n") } b.WriteString("└" + strings.Repeat("─", contentWidth+2) + "┘") return b.String() } func padRight(value string, width int) string { if gap := width - lipgloss.Width(value); gap > 0 { return value + strings.Repeat(" ", gap) } return value }