Files
nono-opencode-tinfoil/tui/dashboard.go
T
kawaiipunk a5f97a3a1b Add TUI for managing sandbox profile, relicense to GPLv3
- Add Go/Bubble Tea TUI app under tui/ with dashboard (status checks,
  launch, validate) and profile editor (commands, filesystem, network,
  environment sub-tabs)
- Add Makefile targets: tui, tui-build, tui-install
- Add CI step to build TUI
- Relicense MIT -> GPL-3.0
- Rewrite wrapper to resolve real opencode binary from PATH (supports
  brew, npm, volta)
- Add PATH ordering detection to install script
- Expand README with how-it-works diagram, TUI docs, keybinding table
2026-08-12 19:37:54 +01:00

149 lines
2.9 KiB
Go

package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
tea "github.com/charmbracelet/bubbletea"
)
type DashboardModel struct {
checks []CheckResult
cursor int
maxCursor int
output string
width int
}
func NewDashboardModel(checks []CheckResult) DashboardModel {
return DashboardModel{
checks: checks,
maxCursor: 2,
}
}
type checksDoneMsg struct {
results []CheckResult
}
type launchResultMsg struct {
err error
}
type validateResultMsg struct {
output string
err error
}
func runChecksCmd() tea.Cmd {
return func() tea.Msg {
return checksDoneMsg{results: RunChecks()}
}
}
func launchOpencodeCmd() tea.Cmd {
home, _ := os.UserHomeDir()
wrapper := filepath.Join(home, ".local", "bin", "opencode")
cmd := exec.Command(wrapper)
return tea.ExecProcess(cmd, func(err error) tea.Msg {
return launchResultMsg{err: err}
})
}
func validateProfileCmd() tea.Cmd {
return func() tea.Msg {
cmd := exec.Command("nono", "profile", "validate", "opencode-tinfoil")
out, err := cmd.CombinedOutput()
return validateResultMsg{output: strings.TrimSpace(string(out)), err: err}
}
}
func (m DashboardModel) Update(msg tea.Msg) (DashboardModel, tea.Cmd) {
switch msg := msg.(type) {
case checksDoneMsg:
m.checks = msg.results
return m, nil
case launchResultMsg:
if msg.err != nil {
m.output = fmt.Sprintf("Launch failed: %v", msg.err)
} else {
m.output = "opencode exited normally"
}
return m, nil
case validateResultMsg:
if msg.err != nil {
m.output = "Validation: " + msg.output
} else {
m.output = "Validation OK: " + msg.output
}
return m, nil
case tea.KeyMsg:
switch msg.String() {
case "up", "k":
if m.cursor > 0 {
m.cursor--
}
case "down", "j":
if m.cursor < m.maxCursor {
m.cursor++
}
case "l":
if m.cursor == 0 {
return m, launchOpencodeCmd()
}
case "v":
if m.cursor == 1 {
return m, validateProfileCmd()
}
case "r":
m.output = "Refreshing..."
return m, runChecksCmd()
}
}
return m, nil
}
func (m DashboardModel) View() string {
var b strings.Builder
b.WriteString("Dashboard\n\n")
for _, check := range m.checks {
var icon string
style := checkOKStyle
if check.Status == StatusOK {
icon = "✓"
} else {
icon = "✗"
style = checkFailStyle
}
b.WriteString(fmt.Sprintf(" %s %s %s\n",
style.Render(icon),
labelStyle.Render(check.Label),
detailStyle.Render(check.Detail),
))
}
b.WriteString("\n")
actions := []string{"Launch opencode in sandbox", "Validate profile", "Refresh checks"}
for i, action := range actions {
cursor := " "
if i == m.cursor {
cursor = cursorStyle.Render("> ")
}
b.WriteString(fmt.Sprintf("%s%s\n", cursor, action))
}
if m.output != "" {
b.WriteString("\n" + detailStyle.Render(m.output))
}
b.WriteString("\n\n" + helpStyle.Render(" l:launch v:validate r:refresh Tab:editor"))
return b.String()
}