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:
+143
@@ -0,0 +1,143 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type listMode int
|
||||
|
||||
const (
|
||||
listBrowse listMode = iota
|
||||
listAdding
|
||||
listEditing
|
||||
)
|
||||
|
||||
type EditableList struct {
|
||||
title string
|
||||
items []string
|
||||
cursor int
|
||||
mode listMode
|
||||
input textinput.Model
|
||||
}
|
||||
|
||||
func NewEditableList(title string, items []string) EditableList {
|
||||
ti := textinput.New()
|
||||
ti.Placeholder = "enter value..."
|
||||
ti.CharLimit = 200
|
||||
return EditableList{
|
||||
title: title,
|
||||
items: items,
|
||||
mode: listBrowse,
|
||||
input: ti,
|
||||
}
|
||||
}
|
||||
|
||||
func (l EditableList) Items() []string {
|
||||
return l.items
|
||||
}
|
||||
|
||||
func (l EditableList) IsEditing() bool {
|
||||
return l.mode != listBrowse
|
||||
}
|
||||
|
||||
func (l EditableList) Update(msg tea.Msg) (EditableList, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch l.mode {
|
||||
case listBrowse:
|
||||
switch msg.String() {
|
||||
case "up", "k":
|
||||
if l.cursor > 0 {
|
||||
l.cursor--
|
||||
}
|
||||
case "down", "j":
|
||||
if l.cursor < len(l.items)-1 {
|
||||
l.cursor++
|
||||
}
|
||||
case "a":
|
||||
l.mode = listAdding
|
||||
l.input.SetValue("")
|
||||
l.input.Focus()
|
||||
return l, textinput.Blink
|
||||
case "x", "delete":
|
||||
if len(l.items) > 0 && l.cursor < len(l.items) {
|
||||
l.items = append(l.items[:l.cursor], l.items[l.cursor+1:]...)
|
||||
if l.cursor > len(l.items)-1 {
|
||||
l.cursor = len(l.items) - 1
|
||||
}
|
||||
if l.cursor < 0 {
|
||||
l.cursor = 0
|
||||
}
|
||||
}
|
||||
case "enter":
|
||||
if len(l.items) > 0 {
|
||||
l.mode = listEditing
|
||||
l.input.SetValue(l.items[l.cursor])
|
||||
l.input.Focus()
|
||||
return l, textinput.Blink
|
||||
}
|
||||
}
|
||||
case listAdding, listEditing:
|
||||
switch msg.String() {
|
||||
case "enter":
|
||||
val := strings.TrimSpace(l.input.Value())
|
||||
if val != "" {
|
||||
if l.mode == listAdding {
|
||||
l.items = append(l.items, val)
|
||||
l.cursor = len(l.items) - 1
|
||||
} else {
|
||||
l.items[l.cursor] = val
|
||||
}
|
||||
}
|
||||
l.mode = listBrowse
|
||||
l.input.Blur()
|
||||
case "esc":
|
||||
l.mode = listBrowse
|
||||
l.input.Blur()
|
||||
default:
|
||||
var cmd tea.Cmd
|
||||
l.input, cmd = l.input.Update(msg)
|
||||
return l, cmd
|
||||
}
|
||||
}
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
func (l EditableList) View() string {
|
||||
var b strings.Builder
|
||||
|
||||
switch l.mode {
|
||||
case listAdding:
|
||||
b.WriteString(fmt.Sprintf("%s (adding)\n", l.title))
|
||||
b.WriteString(fmt.Sprintf("> %s\n", l.input.View()))
|
||||
b.WriteString(helpStyle.Render(" enter to confirm · esc to cancel"))
|
||||
case listEditing:
|
||||
b.WriteString(fmt.Sprintf("%s (editing)\n", l.title))
|
||||
b.WriteString(fmt.Sprintf("> %s\n", l.input.View()))
|
||||
b.WriteString(helpStyle.Render(" enter to confirm · esc to cancel"))
|
||||
default:
|
||||
b.WriteString(l.title + "\n")
|
||||
if len(l.items) == 0 {
|
||||
b.WriteString(helpStyle.Render(" (empty)"))
|
||||
}
|
||||
for i, item := range l.items {
|
||||
cursor := " "
|
||||
if i == l.cursor {
|
||||
cursor = cursorStyle.Render("> ")
|
||||
}
|
||||
b.WriteString(fmt.Sprintf("%s%s\n", cursor, item))
|
||||
}
|
||||
}
|
||||
|
||||
content := b.String()
|
||||
style := boxStyle
|
||||
if l.mode != listBrowse {
|
||||
style = activeBoxStyle
|
||||
}
|
||||
return style.Render(strings.TrimRight(content, "\n"))
|
||||
}
|
||||
Reference in New Issue
Block a user