picoclaw/cmd/picoclaw-launcher-tui/ui/app.go

124 lines
2.8 KiB
Go
Raw Normal View History

// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
//
// Copyright (c) 2026 PicoClaw contributors
package ui
import (
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
tuicfg "github.com/sipeed/picoclaw/cmd/picoclaw-launcher-tui/config"
)
// App is the root TUI application.
type App struct {
tapp *tview.Application
pages *tview.Pages
pageStack []string
cfg *tuicfg.TUIConfig
configPath string
homeRefreshFn func()
}
// New creates and wires up the TUI application.
func New(cfg *tuicfg.TUIConfig, configPath string) *App {
a := &App{
tapp: tview.NewApplication(),
pages: tview.NewPages(),
pageStack: []string{},
cfg: cfg,
configPath: configPath,
}
a.tapp.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyEscape {
return a.goBack()
}
return event
})
a.buildPages()
return a
}
// Run starts the TUI event loop.
func (a *App) Run() error {
return a.tapp.SetRoot(a.pages, true).EnableMouse(true).Run()
}
func (a *App) buildPages() {
a.pages.AddPage("home", a.newHomePage(), true, true)
a.pageStack = []string{"home"}
}
func (a *App) navigateTo(name string, page tview.Primitive) {
a.pages.AddPage(name, page, true, false)
a.pageStack = append(a.pageStack, name)
a.pages.SwitchToPage(name)
}
func (a *App) goBack() *tcell.EventKey {
if len(a.pageStack) <= 1 {
return nil
}
a.pageStack = a.pageStack[:len(a.pageStack)-1]
prev := a.pageStack[len(a.pageStack)-1]
if prev == "home" && a.homeRefreshFn != nil {
a.homeRefreshFn()
}
a.pages.SwitchToPage(prev)
return nil
}
func (a *App) showModal(name string, primitive tview.Primitive) {
a.pages.AddPage(name, primitive, true, true)
}
func (a *App) hideModal(name string) {
a.pages.HidePage(name)
a.pages.RemovePage(name)
}
func (a *App) save() {
_ = tuicfg.Save(a.configPath, a.cfg)
}
func (a *App) showError(msg string) {
modal := tview.NewModal().
SetText("Error: " + msg).
AddButtons([]string{"OK"}).
SetDoneFunc(func(_ int, _ string) {
a.hideModal("error")
})
a.showModal("error", modal)
}
func (a *App) confirmDelete(label string, onConfirm func()) {
modal := tview.NewModal().
SetText("Delete " + label + "?\nThis cannot be undone.").
AddButtons([]string{"Delete", "Cancel"}).
SetDoneFunc(func(_ int, buttonLabel string) {
a.hideModal("confirm-delete")
if buttonLabel == "Delete" {
onConfirm()
}
})
a.showModal("confirm-delete", modal)
}
func centeredForm(form *tview.Form, width, height int) tview.Primitive {
return tview.NewGrid().
SetColumns(0, width, 0).
SetRows(0, height, 0).
AddItem(form, 1, 1, 1, 1, 0, 0, true)
}
func hintBar(text string) *tview.TextView {
tv := tview.NewTextView().
SetText(text).
SetTextAlign(tview.AlignCenter)
tv.SetBackgroundColor(tcell.ColorDarkBlue)
return tv
}