mirror of
https://github.com/wisplite/parchment.git
synced 2026-06-27 13:47:08 -05:00
329 lines
8.5 KiB
Go
329 lines
8.5 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/bubbles/textinput"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/charmbracelet/lipgloss"
|
|
)
|
|
|
|
// AppStatus represents the current state of the application.
|
|
type AppStatus int
|
|
|
|
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
|
|
}
|
|
|
|
// Custom messages for Bubble Tea commands
|
|
type connectMsg struct {
|
|
err error
|
|
}
|
|
|
|
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}
|
|
}
|
|
}
|