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
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user