updated the app to use tview because the bubbletea code was unmaintainable

This commit is contained in:
2026-01-29 06:06:21 -06:00
parent e5688ae361
commit 038bc3e88c
5 changed files with 204 additions and 357 deletions
+35 -317
View File
@@ -1,328 +1,46 @@
package main
import (
"fmt"
"log"
"os"
"strings"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
// AppStatus represents the current state of the application.
type AppStatus int
func AppUI(app *tview.Application, currentFrame **tview.Frame) *tview.Frame {
list := tview.NewList()
list.AddItem("CPkg Tools", "Create and unpack cpkg files for distribution", 'c', func() {
newFrame := PackageUI(app, currentFrame)
*currentFrame = newFrame
app.SetRoot(newFrame, true)
})
list.AddItem("Upload to Cardputer", "Creates a package and uploads it to the connected Cardputer", 'u', nil)
frame := tview.NewFrame(list)
frame.SetBorders(0, 0, 0, 0, 0, 0)
frame.AddText("Parchment - v0.0.1", true, tview.AlignLeft, tcell.ColorWhite)
frame.AddText("Searching for device...", false, tview.AlignLeft, tcell.ColorWhite)
const (
StatusSearching AppStatus = iota
StatusFound
StatusConnecting
StatusConnected
StatusError
StatusUploading
StatusUploaded
StatusPromptUnpack
StatusUnpacking
StatusUnpacked
)
// UIModel is the Bubble Tea model for the application.
type UIModel struct {
Device *Device
Status AppStatus
ErrorMsg string
TextInput textinput.Model
Width int
Height int
return frame
}
// Custom messages for Bubble Tea commands
type connectMsg struct {
err error
func PackageUI(app *tview.Application, currentFrame **tview.Frame) *tview.Frame {
list := tview.NewList()
list.AddItem("Create Package", "Creates a .cpkg file in the running directory", 'c', nil)
list.AddItem("Unpack Package", "Unpacks a chosen .cpkg file into a subdirectory", 'u', nil)
list.AddItem("Back", "Go back to the home page", 'b', func() {
newFrame := AppUI(app, currentFrame)
*currentFrame = newFrame
app.SetRoot(newFrame, true)
})
frame := tview.NewFrame(list)
frame.SetBorders(0, 0, 0, 0, 0, 0)
frame.AddText("Package Mode", true, tview.AlignLeft, tcell.ColorWhite)
return frame
}
type uploadMsg struct {
err error
}
type unpackMsg struct {
err error
}
// InitialModel creates the initial state of the model.
func InitialModel(device *Device) UIModel {
ti := textinput.New()
ti.Placeholder = "package-name"
ti.CharLimit = 256
ti.Width = 40
return UIModel{
Device: device,
Status: StatusSearching,
TextInput: ti,
}
}
// Init initializes the model.
func (m UIModel) Init() tea.Cmd {
return textinput.Blink
}
// Update handles messages and updates the model.
func (m UIModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.Width = msg.Width
m.Height = msg.Height
return m, nil
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q":
// Ensure device is closed on exit
if m.Device != nil {
m.Device.Close()
}
return m, tea.Quit
}
}
// Handle input based on current status
if m.Status == StatusPromptUnpack {
return m.updatePromptUnpack(msg)
}
return m.updateMain(msg)
}
func (m UIModel) updatePromptUnpack(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "esc":
if m.Device.Connected {
m.Status = StatusConnected
} else {
m.Status = StatusFound
}
m.TextInput.Reset()
return m, nil
case "enter":
filename := m.TextInput.Value()
if filename != "" {
m.Status = StatusUnpacking
m.TextInput.Reset()
return m, debugUnpackCmd(filename)
}
}
}
var cmd tea.Cmd
m.TextInput, cmd = m.TextInput.Update(msg)
return m, cmd
}
func (m UIModel) updateMain(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "c", "enter":
if m.Status == StatusFound {
m.Status = StatusConnecting
return m, connectCmd(m.Device)
}
case "u":
if m.Status == StatusConnected {
m.Status = StatusUploading
return m, uploadCmd(m.Device)
}
case "d":
if m.Status == StatusFound || m.Status == StatusConnected || m.Status == StatusUploaded || m.Status == StatusUnpacked {
m.Status = StatusPromptUnpack
m.TextInput.Focus()
return m, textinput.Blink
}
}
case connectMsg:
if msg.err != nil {
m.Status = StatusError
m.ErrorMsg = msg.err.Error()
return m, nil
}
m.Status = StatusConnected
return m, nil
case uploadMsg:
if msg.err != nil {
m.Status = StatusError
m.ErrorMsg = msg.err.Error()
return m, nil
}
m.Status = StatusUploaded
return m, nil
case unpackMsg:
if msg.err != nil {
m.Status = StatusError
m.ErrorMsg = msg.err.Error()
return m, nil
}
m.Status = StatusUnpacked
return m, nil
}
return m, nil
}
// View renders the application UI.
func (m UIModel) View() string {
var s strings.Builder
// Styles
titleStyle := lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("#FAFAFA")).
Background(lipgloss.Color("#7D56F4")).
Padding(0, 1).
MarginBottom(1)
statusStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#7D56F4")).Bold(true)
errorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#FF0000")).Bold(true)
successStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#00FF00")).Bold(true)
keyStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#626262")).Bold(true)
descStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#A0A0A0"))
// Header
s.WriteString(titleStyle.Render("Parchment - Cardstock Dev Server"))
s.WriteString("\n")
// Content
content := strings.Builder{}
switch m.Status {
case StatusSearching:
content.WriteString(statusStyle.Render("⏳ Searching for Cardputer..."))
case StatusFound:
content.WriteString(successStyle.Render("✓ Cardputer found"))
content.WriteString(fmt.Sprintf(" at %s\n\n", m.Device.PortName))
content.WriteString(keyStyle.Render("[c] / [enter]") + descStyle.Render(" Connect and enter dev mode\n"))
content.WriteString(keyStyle.Render("[d]") + descStyle.Render(" Debug pack/unpack (local)\n"))
case StatusConnecting:
content.WriteString(statusStyle.Render("⏳ Connecting and requesting dev mode..."))
case StatusConnected:
content.WriteString(successStyle.Render("✓ Connected to Cardputer!"))
content.WriteString(fmt.Sprintf("\n\nPort: %s\n", m.Device.PortName))
content.WriteString("Device is in developer mode.\n\n")
content.WriteString(keyStyle.Render("[u]") + descStyle.Render(" Upload package from current directory\n"))
content.WriteString(keyStyle.Render("[d]") + descStyle.Render(" Debug pack/unpack (local)\n"))
case StatusUploading:
content.WriteString(statusStyle.Render("⏳ Uploading package to device..."))
case StatusUploaded:
content.WriteString(successStyle.Render("✓ Package uploaded successfully!"))
content.WriteString(fmt.Sprintf("\n\nPort: %s\n", m.Device.PortName))
content.WriteString("\n")
content.WriteString(keyStyle.Render("[d]") + descStyle.Render(" Debug pack/unpack\n"))
case StatusPromptUnpack:
content.WriteString(statusStyle.Render("Debug Unpack"))
content.WriteString("\n\nEnter the filename to unpack (without .cpkg extension):\n\n")
content.WriteString(m.TextInput.View())
content.WriteString("\n\n")
content.WriteString(keyStyle.Render("[enter]") + descStyle.Render(" Unpack\n"))
content.WriteString(keyStyle.Render("[esc]") + descStyle.Render(" Cancel\n"))
case StatusUnpacking:
content.WriteString(statusStyle.Render("⏳ Debug packing and unpacking..."))
case StatusUnpacked:
content.WriteString(successStyle.Render("✓ Debug pack/unpack completed!"))
content.WriteString("\n\nCheck debug.log for details.\n")
content.WriteString(keyStyle.Render("[d]") + descStyle.Render(" Run again\n"))
case StatusError:
content.WriteString(errorStyle.Render("✗ Error: "))
content.WriteString(m.ErrorMsg)
content.WriteString("\n\n")
if m.Device.Connected {
content.WriteString(keyStyle.Render("[u]") + descStyle.Render(" Retry upload\n"))
} else {
content.WriteString(keyStyle.Render("[c]") + descStyle.Render(" Retry connection\n"))
}
}
// Render content with some margin
s.WriteString(lipgloss.NewStyle().Margin(1, 0, 1, 0).Render(content.String()))
// Footer
s.WriteString("\n")
footer := lipgloss.NewStyle().Foreground(lipgloss.Color("#626262")).Render("Press [q] or [ctrl+c] to quit")
s.WriteString(footer)
return lipgloss.NewStyle().Padding(1, 2).Render(s.String())
}
// Commands
func connectCmd(d *Device) tea.Cmd {
return func() tea.Msg {
if err := d.Connect(); err != nil {
return connectMsg{err: err}
}
return connectMsg{err: nil}
}
}
func uploadCmd(d *Device) tea.Cmd {
return func() tea.Msg {
// Mock upload for now or implement actual upload logic using Device
// We'll use the existing logic from main.go adapted here
fsys := os.DirFS(".")
pkgData, err := loadPackageJSON(fsys)
if err != nil {
return uploadMsg{err: err}
}
if pkgData == nil {
return uploadMsg{err: fmt.Errorf("package.json not found")}
}
log.Println("Loading package data:", pkgData)
pkg, err := createCpkg(fsys, pkgData)
if err != nil {
return uploadMsg{err: err}
}
pkg.DebugPrint()
// TODO: Actually send the package over serial using d.Write()
// For now we just simulate success as per original code
return uploadMsg{err: nil}
}
}
func debugUnpackCmd(filename string) tea.Cmd {
return func() tea.Msg {
fsys := os.DirFS(".")
packageName := strings.TrimSuffix(filename, ".cpkg")
log.Printf("Attempting to unpack: %s.cpkg", packageName)
if _, err := os.Stat(packageName + ".cpkg"); os.IsNotExist(err) {
return unpackMsg{err: fmt.Errorf("file not found: %s.cpkg", packageName)}
}
unpackPkg := NewCpkg(packageName)
err := unpackPkg.Unpack(fsys)
if err != nil {
return unpackMsg{err: fmt.Errorf("failed to unpack: %w", err)}
}
log.Println("Debug unpack completed successfully")
return unpackMsg{err: nil}
}
func UpdateDeviceStatus(app *tview.Application, frame *tview.Frame, status string, color tcell.Color) {
app.QueueUpdateDraw(func() {
frame.Clear()
frame.AddText("Parchment - v0.0.1", true, tview.AlignLeft, tcell.ColorWhite)
frame.AddText(status, false, tview.AlignLeft, color)
})
}