Embed MOTD banner into TUI
This commit is contained in:
@@ -27,8 +27,9 @@ type exportTargetsMsg struct {
|
|||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
type panelMsg struct {
|
type snapshotMsg struct {
|
||||||
data app.HardwarePanelData
|
banner string
|
||||||
|
panel app.HardwarePanelData
|
||||||
}
|
}
|
||||||
|
|
||||||
type nvidiaGPUsMsg struct {
|
type nvidiaGPUsMsg struct {
|
||||||
|
|||||||
30
audit/internal/tui/snapshot.go
Normal file
30
audit/internal/tui/snapshot.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bee/audit/internal/app"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m model) refreshSnapshotCmd() tea.Cmd {
|
||||||
|
if m.app == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return func() tea.Msg {
|
||||||
|
return snapshotMsg{
|
||||||
|
banner: m.app.MainBanner(),
|
||||||
|
panel: m.app.LoadHardwarePanel(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldRefreshSnapshot(prev, next model) bool {
|
||||||
|
return prev.screen != next.screen || prev.busy != next.busy
|
||||||
|
}
|
||||||
|
|
||||||
|
func emptySnapshot() snapshotMsg {
|
||||||
|
return snapshotMsg{
|
||||||
|
banner: "",
|
||||||
|
panel: app.HardwarePanelData{},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,9 +53,9 @@ func TestUpdateMainMenuEnterActions(t *testing.T) {
|
|||||||
wantBusy bool
|
wantBusy bool
|
||||||
wantCmd bool
|
wantCmd bool
|
||||||
}{
|
}{
|
||||||
{name: "health_check", cursor: 0, wantScreen: screenHealthCheck},
|
{name: "health_check", cursor: 0, wantScreen: screenHealthCheck, wantCmd: true},
|
||||||
{name: "export", cursor: 1, wantScreen: screenMain, wantBusy: true, wantCmd: true},
|
{name: "export", cursor: 1, wantScreen: screenMain, wantBusy: true, wantCmd: true},
|
||||||
{name: "settings", cursor: 2, wantScreen: screenSettings},
|
{name: "settings", cursor: 2, wantScreen: screenSettings, wantCmd: true},
|
||||||
{name: "exit", cursor: 3, wantScreen: screenMain, wantCmd: true},
|
{name: "exit", cursor: 3, wantScreen: screenMain, wantCmd: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,6 +460,55 @@ func TestViewOutputScreenRendersBodyAndBackHint(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestViewRendersBannerModuleAboveScreenBody(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
m := newTestModel()
|
||||||
|
m.banner = "System: Demo Server\nIP: 10.0.0.10"
|
||||||
|
m.width = 60
|
||||||
|
|
||||||
|
view := m.View()
|
||||||
|
|
||||||
|
for _, want := range []string{
|
||||||
|
"┌ MOTD ",
|
||||||
|
"System: Demo Server",
|
||||||
|
"IP: 10.0.0.10",
|
||||||
|
"Health Check",
|
||||||
|
"Export support bundle",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(view, want) {
|
||||||
|
t.Fatalf("view missing %q\nview:\n%s", want, view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSnapshotMsgUpdatesBannerAndPanel(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
m := newTestModel()
|
||||||
|
|
||||||
|
next, cmd := m.Update(snapshotMsg{
|
||||||
|
banner: "System: Demo",
|
||||||
|
panel: app.HardwarePanelData{
|
||||||
|
Header: []string{"Demo header"},
|
||||||
|
Rows: []app.ComponentRow{
|
||||||
|
{Key: "CPU", Status: "PASS", Detail: "ok"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
got := next.(model)
|
||||||
|
|
||||||
|
if cmd != nil {
|
||||||
|
t.Fatal("expected nil cmd")
|
||||||
|
}
|
||||||
|
if got.banner != "System: Demo" {
|
||||||
|
t.Fatalf("banner=%q want %q", got.banner, "System: Demo")
|
||||||
|
}
|
||||||
|
if len(got.panel.Rows) != 1 || got.panel.Rows[0].Key != "CPU" {
|
||||||
|
t.Fatalf("panel rows=%+v", got.panel.Rows)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestViewExportTargetsRendersDeviceMetadata(t *testing.T) {
|
func TestViewExportTargetsRendersDeviceMetadata(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -32,32 +32,32 @@ const (
|
|||||||
type actionKind string
|
type actionKind string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
actionNone actionKind = ""
|
actionNone actionKind = ""
|
||||||
actionDHCPOne actionKind = "dhcp_one"
|
actionDHCPOne actionKind = "dhcp_one"
|
||||||
actionStaticIPv4 actionKind = "static_ipv4"
|
actionStaticIPv4 actionKind = "static_ipv4"
|
||||||
actionExportBundle actionKind = "export_bundle"
|
actionExportBundle actionKind = "export_bundle"
|
||||||
actionRunAll actionKind = "run_all"
|
actionRunAll actionKind = "run_all"
|
||||||
actionRunMemorySAT actionKind = "run_memory_sat"
|
actionRunMemorySAT actionKind = "run_memory_sat"
|
||||||
actionRunStorageSAT actionKind = "run_storage_sat"
|
actionRunStorageSAT actionKind = "run_storage_sat"
|
||||||
actionRunCPUSAT actionKind = "run_cpu_sat"
|
actionRunCPUSAT actionKind = "run_cpu_sat"
|
||||||
actionRunAMDGPUSAT actionKind = "run_amd_gpu_sat"
|
actionRunAMDGPUSAT actionKind = "run_amd_gpu_sat"
|
||||||
)
|
)
|
||||||
|
|
||||||
type model struct {
|
type model struct {
|
||||||
app *app.App
|
app *app.App
|
||||||
runtimeMode runtimeenv.Mode
|
runtimeMode runtimeenv.Mode
|
||||||
|
|
||||||
screen screen
|
screen screen
|
||||||
prevScreen screen
|
prevScreen screen
|
||||||
cursor int
|
cursor int
|
||||||
busy bool
|
busy bool
|
||||||
busyTitle string
|
busyTitle string
|
||||||
title string
|
title string
|
||||||
body string
|
body string
|
||||||
mainMenu []string
|
mainMenu []string
|
||||||
settingsMenu []string
|
settingsMenu []string
|
||||||
networkMenu []string
|
networkMenu []string
|
||||||
serviceMenu []string
|
serviceMenu []string
|
||||||
|
|
||||||
services []string
|
services []string
|
||||||
interfaces []platform.InterfaceInfo
|
interfaces []platform.InterfaceInfo
|
||||||
@@ -74,6 +74,7 @@ type model struct {
|
|||||||
panel app.HardwarePanelData
|
panel app.HardwarePanelData
|
||||||
panelFocus bool
|
panelFocus bool
|
||||||
panelCursor int
|
panelCursor int
|
||||||
|
banner string
|
||||||
|
|
||||||
// Health Check screen
|
// Health Check screen
|
||||||
hcSel [4]bool
|
hcSel [4]bool
|
||||||
@@ -95,6 +96,9 @@ type model struct {
|
|||||||
progressLines []string
|
progressLines []string
|
||||||
progressPrefix string
|
progressPrefix string
|
||||||
progressSince time.Time
|
progressSince time.Time
|
||||||
|
|
||||||
|
// Terminal size
|
||||||
|
width int
|
||||||
}
|
}
|
||||||
|
|
||||||
type formField struct {
|
type formField struct {
|
||||||
@@ -151,9 +155,7 @@ func newModel(application *app.App, runtimeMode runtimeenv.Mode) model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m model) Init() tea.Cmd {
|
func (m model) Init() tea.Cmd {
|
||||||
return func() tea.Msg {
|
return m.refreshSnapshotCmd()
|
||||||
return panelMsg{data: m.app.LoadHardwarePanel()}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m model) confirmBody() (string, string) {
|
func (m model) confirmBody() (string, string) {
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import (
|
|||||||
|
|
||||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
m.width = msg.Width
|
||||||
|
return m, nil
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
if m.busy {
|
if m.busy {
|
||||||
if msg.String() == "ctrl+c" {
|
if msg.String() == "ctrl+c" {
|
||||||
@@ -16,7 +19,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
return m.updateKey(msg)
|
next, cmd := m.updateKey(msg)
|
||||||
|
nextModel := next.(model)
|
||||||
|
if shouldRefreshSnapshot(m, nextModel) {
|
||||||
|
return nextModel, tea.Batch(cmd, nextModel.refreshSnapshotCmd())
|
||||||
|
}
|
||||||
|
return nextModel, cmd
|
||||||
case satProgressMsg:
|
case satProgressMsg:
|
||||||
if m.busy && m.progressPrefix != "" {
|
if m.busy && m.progressPrefix != "" {
|
||||||
if len(msg.lines) > 0 {
|
if len(msg.lines) > 0 {
|
||||||
@@ -25,6 +33,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, pollSATProgress(m.progressPrefix, m.progressSince)
|
return m, pollSATProgress(m.progressPrefix, m.progressSince)
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
|
case snapshotMsg:
|
||||||
|
m.banner = msg.banner
|
||||||
|
m.panel = msg.panel
|
||||||
|
return m, nil
|
||||||
case resultMsg:
|
case resultMsg:
|
||||||
m.busy = false
|
m.busy = false
|
||||||
m.busyTitle = ""
|
m.busyTitle = ""
|
||||||
@@ -49,7 +61,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
m.screen = screenOutput
|
m.screen = screenOutput
|
||||||
m.cursor = 0
|
m.cursor = 0
|
||||||
return m, nil
|
return m, m.refreshSnapshotCmd()
|
||||||
case servicesMsg:
|
case servicesMsg:
|
||||||
m.busy = false
|
m.busy = false
|
||||||
m.busyTitle = ""
|
m.busyTitle = ""
|
||||||
@@ -58,12 +70,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.body = msg.err.Error()
|
m.body = msg.err.Error()
|
||||||
m.prevScreen = screenSettings
|
m.prevScreen = screenSettings
|
||||||
m.screen = screenOutput
|
m.screen = screenOutput
|
||||||
return m, nil
|
return m, m.refreshSnapshotCmd()
|
||||||
}
|
}
|
||||||
m.services = msg.services
|
m.services = msg.services
|
||||||
m.screen = screenServices
|
m.screen = screenServices
|
||||||
m.cursor = 0
|
m.cursor = 0
|
||||||
return m, nil
|
return m, m.refreshSnapshotCmd()
|
||||||
case interfacesMsg:
|
case interfacesMsg:
|
||||||
m.busy = false
|
m.busy = false
|
||||||
m.busyTitle = ""
|
m.busyTitle = ""
|
||||||
@@ -72,12 +84,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.body = msg.err.Error()
|
m.body = msg.err.Error()
|
||||||
m.prevScreen = screenNetwork
|
m.prevScreen = screenNetwork
|
||||||
m.screen = screenOutput
|
m.screen = screenOutput
|
||||||
return m, nil
|
return m, m.refreshSnapshotCmd()
|
||||||
}
|
}
|
||||||
m.interfaces = msg.ifaces
|
m.interfaces = msg.ifaces
|
||||||
m.screen = screenInterfacePick
|
m.screen = screenInterfacePick
|
||||||
m.cursor = 0
|
m.cursor = 0
|
||||||
return m, nil
|
return m, m.refreshSnapshotCmd()
|
||||||
case exportTargetsMsg:
|
case exportTargetsMsg:
|
||||||
m.busy = false
|
m.busy = false
|
||||||
m.busyTitle = ""
|
m.busyTitle = ""
|
||||||
@@ -86,15 +98,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.body = msg.err.Error()
|
m.body = msg.err.Error()
|
||||||
m.prevScreen = screenMain
|
m.prevScreen = screenMain
|
||||||
m.screen = screenOutput
|
m.screen = screenOutput
|
||||||
return m, nil
|
return m, m.refreshSnapshotCmd()
|
||||||
}
|
}
|
||||||
m.targets = msg.targets
|
m.targets = msg.targets
|
||||||
m.screen = screenExportTargets
|
m.screen = screenExportTargets
|
||||||
m.cursor = 0
|
m.cursor = 0
|
||||||
return m, nil
|
return m, m.refreshSnapshotCmd()
|
||||||
case panelMsg:
|
|
||||||
m.panel = msg.data
|
|
||||||
return m, nil
|
|
||||||
case nvidiaGPUsMsg:
|
case nvidiaGPUsMsg:
|
||||||
return m.handleNvidiaGPUsMsg(msg)
|
return m.handleNvidiaGPUsMsg(msg)
|
||||||
case nvtopClosedMsg:
|
case nvtopClosedMsg:
|
||||||
@@ -120,7 +129,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
} else {
|
} else {
|
||||||
m.body = msg.body
|
m.body = msg.body
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, m.refreshSnapshotCmd()
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
@@ -154,10 +163,6 @@ func (m model) updateKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
m.body = ""
|
m.body = ""
|
||||||
m.title = ""
|
m.title = ""
|
||||||
m.pendingAction = actionNone
|
m.pendingAction = actionNone
|
||||||
// Refresh panel when returning to main screen.
|
|
||||||
if m.prevScreen == screenMain {
|
|
||||||
return m, func() tea.Msg { return panelMsg{data: m.app.LoadHardwarePanel()} }
|
|
||||||
}
|
|
||||||
return m, nil
|
return m, nil
|
||||||
case "ctrl+c":
|
case "ctrl+c":
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
|
|
||||||
"bee/audit/internal/platform"
|
"bee/audit/internal/platform"
|
||||||
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Column widths for two-column main layout.
|
// Column widths for two-column main layout.
|
||||||
@@ -34,6 +34,7 @@ func colorStatus(status string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m model) View() string {
|
func (m model) View() string {
|
||||||
|
var body string
|
||||||
if m.busy {
|
if m.busy {
|
||||||
title := "bee"
|
title := "bee"
|
||||||
if m.busyTitle != "" {
|
if m.busyTitle != "" {
|
||||||
@@ -46,41 +47,44 @@ func (m model) View() string {
|
|||||||
fmt.Fprintf(&b, " %s\n", l)
|
fmt.Fprintf(&b, " %s\n", l)
|
||||||
}
|
}
|
||||||
b.WriteString("\n[ctrl+c] quit\n")
|
b.WriteString("\n[ctrl+c] quit\n")
|
||||||
return b.String()
|
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 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 removable filesystem", renderTargetItems(m.targets), m.cursor)
|
||||||
|
case screenInterfacePick:
|
||||||
|
body = renderMenu("Interfaces", "Select interface", renderInterfaceItems(m.interfaces), m.cursor)
|
||||||
|
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 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 fmt.Sprintf("%s\n\nWorking...\n\n[ctrl+c] quit\n", title)
|
|
||||||
}
|
|
||||||
switch m.screen {
|
|
||||||
case screenMain:
|
|
||||||
return renderTwoColumnMain(m)
|
|
||||||
case screenHealthCheck:
|
|
||||||
return renderHealthCheck(m)
|
|
||||||
case screenSettings:
|
|
||||||
return renderMenu("Settings", "Select action", m.settingsMenu, m.cursor)
|
|
||||||
case screenNetwork:
|
|
||||||
return renderMenu("Network", "Select action", m.networkMenu, m.cursor)
|
|
||||||
case screenServices:
|
|
||||||
return renderMenu("Services", "Select service", m.services, m.cursor)
|
|
||||||
case screenServiceAction:
|
|
||||||
return renderMenu("Service: "+m.selectedService, "Select action", m.serviceMenu, m.cursor)
|
|
||||||
case screenExportTargets:
|
|
||||||
return renderMenu("Export support bundle", "Select removable filesystem", renderTargetItems(m.targets), m.cursor)
|
|
||||||
case screenInterfacePick:
|
|
||||||
return renderMenu("Interfaces", "Select interface", renderInterfaceItems(m.interfaces), m.cursor)
|
|
||||||
case screenStaticForm:
|
|
||||||
return renderForm("Static IPv4: "+m.selectedIface, m.formFields, m.formIndex)
|
|
||||||
case screenConfirm:
|
|
||||||
title, body := m.confirmBody()
|
|
||||||
return renderConfirm(title, body, m.cursor)
|
|
||||||
case screenNvidiaSATSetup:
|
|
||||||
return renderNvidiaSATSetup(m)
|
|
||||||
case screenNvidiaSATRunning:
|
|
||||||
return renderNvidiaSATRunning()
|
|
||||||
case screenOutput:
|
|
||||||
return fmt.Sprintf("%s\n\n%s\n\n[enter/esc] back [ctrl+c] quit\n", m.title, strings.TrimSpace(m.body))
|
|
||||||
default:
|
|
||||||
return "bee\n"
|
|
||||||
}
|
}
|
||||||
|
return m.renderWithBanner(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderTwoColumnMain renders the main screen with menu on the left and hardware panel on the right.
|
// renderTwoColumnMain renders the main screen with menu on the left and hardware panel on the right.
|
||||||
@@ -231,3 +235,60 @@ func resultCmd(title, body string, err error, back screen) tea.Cmd {
|
|||||||
return resultMsg{title: title, body: body, err: err, back: back}
|
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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user