- 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
144 lines
3.0 KiB
Go
144 lines
3.0 KiB
Go
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"))
|
|
}
|