- 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
149 lines
2.9 KiB
Go
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()
|
|
}
|