Files
nono-opencode-tinfoil/tui/list.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

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"))
}