Refactor UI state management and improve error handling in the application. Added success and error messages to enhance user feedback during package creation and device connection status updates.

This commit is contained in:
2026-01-29 10:45:12 -06:00
parent a44f433bba
commit f5a688e741
7 changed files with 120 additions and 77 deletions
+29 -46
View File
@@ -2,28 +2,26 @@ package main
import (
"encoding/binary"
"fmt"
"io/fs"
"os"
"path/filepath"
"strconv"
"log"
"fmt"
)
type fileData struct {
name string
name string
content []byte
}
type cpkg struct {
packageName string
files []fileData
files []fileData
}
func NewCpkg(packageName string) *cpkg {
return &cpkg{
packageName: packageName,
files: make([]fileData, 0),
files: make([]fileData, 0),
}
}
@@ -44,12 +42,12 @@ func (c *cpkg) createCpkgFileHeader(file fileData) []byte {
headerBytes := make([]byte, 0)
headerBytes = append(headerBytes, byte(len(file.name)))
headerBytes = append(headerBytes, []byte(file.name)...)
// file size (uint32 - 4 bytes)
fileSizeBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(fileSizeBytes, uint32(len(file.content)))
headerBytes = append(headerBytes, fileSizeBytes...)
return headerBytes
}
@@ -60,17 +58,17 @@ func (c *cpkg) createCpkgFileHeader(file fileData) []byte {
func (c *cpkg) createCpkgHeader() []byte {
headerBytes := make([]byte, 0)
headerBytes = append(headerBytes, []byte("Cpkg")...)
// version (uint16 - 2 bytes)
versionBytes := make([]byte, 2)
binary.LittleEndian.PutUint16(versionBytes, 1)
headerBytes = append(headerBytes, versionBytes...)
// file count (uint32 - 4 bytes)
fileCountBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(fileCountBytes, uint32(len(c.files)))
headerBytes = append(headerBytes, fileCountBytes...)
return headerBytes
}
@@ -80,7 +78,7 @@ func (c *cpkg) Create() (bool, error) {
return false, err
}
defer file.Close()
// Write main header
header := c.createCpkgHeader()
n, err := file.Write(header)
@@ -90,7 +88,7 @@ func (c *cpkg) Create() (bool, error) {
if n != len(header) {
return false, fmt.Errorf("incomplete header write: wrote %d of %d bytes", n, len(header))
}
// Write each file
for i, fileData := range c.files {
// Write file header
@@ -102,7 +100,7 @@ func (c *cpkg) Create() (bool, error) {
if n != len(fileHeader) {
return false, fmt.Errorf("incomplete header write for file %d (%s): wrote %d of %d bytes", i, fileData.name, n, len(fileHeader))
}
// Write file content
n, err = file.Write(fileData.content)
if err != nil {
@@ -111,21 +109,10 @@ func (c *cpkg) Create() (bool, error) {
if n != len(fileData.content) {
return false, fmt.Errorf("incomplete content write for file %d (%s): wrote %d of %d bytes", i, fileData.name, n, len(fileData.content))
}
log.Printf("Wrote file %d: %s (%d bytes)", i, fileData.name, len(fileData.content))
}
return true, nil
}
func (c *cpkg) DebugPrint() {
log.Println("Package Name:", c.packageName)
idx := 0
for _, file := range c.files {
log.Println("File " + strconv.Itoa(idx) + " header")
log.Println(c.createCpkgFileHeader(file))
idx++
}
return true, nil
}
func (c *cpkg) Unpack(fsys fs.FS) error {
@@ -134,33 +121,32 @@ func (c *cpkg) Unpack(fsys fs.FS) error {
return err
}
defer file.Close()
// Read main header (10 bytes)
header := make([]byte, 10)
_, err = file.Read(header)
if err != nil {
return fmt.Errorf("failed to read header: %w", err)
}
// Verify magic number
magicNumber := string(header[:4])
if magicNumber != "Cpkg" {
return fmt.Errorf("invalid cpkg file: wrong magic number")
}
// Verify version
version := binary.LittleEndian.Uint16(header[4:6])
if version != 1 {
return fmt.Errorf("unsupported cpkg file version: %d", version)
}
// Read file count
fileCount := binary.LittleEndian.Uint32(header[6:10])
log.Printf("Unpacking %d files from %s.cpkg", fileCount, c.packageName)
// Clear existing files array
c.files = make([]fileData, 0, fileCount)
// Read each file sequentially
for i := 0; i < int(fileCount); i++ {
// Read filename length (1 byte)
@@ -173,7 +159,7 @@ func (c *cpkg) Unpack(fsys fs.FS) error {
return fmt.Errorf("incomplete read of filename length for file %d: got %d bytes, expected 1", i, n)
}
fileNameLength := fileNameLengthBuf[0]
// Read filename
fileNameBuf := make([]byte, fileNameLength)
n, err = file.Read(fileNameBuf)
@@ -184,7 +170,7 @@ func (c *cpkg) Unpack(fsys fs.FS) error {
return fmt.Errorf("incomplete read of filename for file %d: got %d bytes, expected %d", i, n, fileNameLength)
}
fileName := string(fileNameBuf)
// Read file size (4 bytes)
fileSizeBuf := make([]byte, 4)
n, err = file.Read(fileSizeBuf)
@@ -195,7 +181,7 @@ func (c *cpkg) Unpack(fsys fs.FS) error {
return fmt.Errorf("incomplete read of file size for file %d: got %d bytes, expected 4", i, n)
}
fileSize := binary.LittleEndian.Uint32(fileSizeBuf)
// Read file content
fileContent := make([]byte, fileSize)
n, err = file.Read(fileContent)
@@ -205,8 +191,7 @@ func (c *cpkg) Unpack(fsys fs.FS) error {
if n != int(fileSize) {
return fmt.Errorf("incomplete read of content for file %d (%s): got %d bytes, expected %d", i, fileName, n, fileSize)
}
log.Printf("Read file %d: %s (%d bytes)", i, fileName, fileSize)
c.files = append(c.files, fileData{name: fileName, content: fileContent})
}
@@ -215,24 +200,22 @@ func (c *cpkg) Unpack(fsys fs.FS) error {
if err != nil {
return fmt.Errorf("failed to create output directory: %w", err)
}
for _, fileData := range c.files {
outputPath := filepath.Join(c.packageName, fileData.name)
// Create parent directories if they don't exist
parentDir := filepath.Dir(outputPath)
err = os.MkdirAll(parentDir, 0755)
if err != nil {
return fmt.Errorf("failed to create directory for file %s: %w", fileData.name, err)
}
err = os.WriteFile(outputPath, fileData.content, 0644)
if err != nil {
return fmt.Errorf("failed to write file %s: %w", fileData.name, err)
}
log.Printf("Wrote file: %s", outputPath)
}
log.Printf("Successfully unpacked %d files to %s/", len(c.files), c.packageName)
return nil
}
}
+10 -5
View File
@@ -68,13 +68,18 @@ func (d *Device) Connect() error {
// ExitDevMode sends the command to exit dev mode.
func (d *Device) ExitDevMode() error {
if !d.Connected || d.Port == nil {
return fmt.Errorf("device not connected")
if d.Port == nil {
return fmt.Errorf("port not open")
}
// Send exit dev mode command
// TODO: Replace with the actual command sequence to exit dev mode
// This is currently not implemented on the Cardputer side.
// disable dev mode
_, err := d.Port.Write([]byte{0xAA, 0x05, 0x00, 0x00, 0x00})
if err != nil {
return fmt.Errorf("failed to send exit dev mode command: %w", err)
}
// Allow some time for the device to respond or settle if needed
time.Sleep(100 * time.Millisecond)
return nil
}
+4 -3
View File
@@ -15,7 +15,7 @@ require (
github.com/creack/goselect v0.1.2 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/gdamore/encoding v1.0.1 // indirect
github.com/gdamore/tcell/v2 v2.8.1 // indirect
github.com/gdamore/tcell/v2 v2.9.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
@@ -23,11 +23,12 @@ require (
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/navidys/tvxwidgets v0.12.1 // indirect
github.com/rivo/tview v0.42.1-0.20250929082832-e113793670e2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.bug.st/serial v1.6.4 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/term v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/term v0.34.0 // indirect
golang.org/x/text v0.28.0 // indirect
)
+8
View File
@@ -24,6 +24,8 @@ github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uh
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU=
github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw=
github.com/gdamore/tcell/v2 v2.9.0 h1:N6t+eqK7/xwtRPwxzs1PXeRWnm0H9l02CrgJ7DLn1ys=
github.com/gdamore/tcell/v2 v2.9.0/go.mod h1:8/ZoqM9rxzYphT9tH/9LnunhV9oPBqwS8WHGYm5nrmo=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
@@ -39,6 +41,8 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/navidys/tvxwidgets v0.12.1 h1:/5yJf/0MPlg50VKnaAfnRF1sBMPos/Aeb9tY0/UXJ3M=
github.com/navidys/tvxwidgets v0.12.1/go.mod h1:3EQbBvdokrZsEjnXKfOdcYAQk4dZIQSfmTJPxQbBE9A=
github.com/rivo/tview v0.42.1-0.20250929082832-e113793670e2 h1:0SWZkAwSpcwyWOTFxFOVjnB+nrUkHAPNnERVYfVzRow=
github.com/rivo/tview v0.42.1-0.20250929082832-e113793670e2/go.mod h1:cSfIYfhpSGCjp3r/ECJb+GKS7cGJnqV8vfjQPwoXyfY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -102,6 +106,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@@ -114,6 +120,8 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+20 -9
View File
@@ -24,7 +24,10 @@ func MonitorDevice(ctx context.Context, app *tview.Application, frame **tview.Fr
if err != nil {
if device.Connected {
device.Close()
UpdateDeviceStatus(app, *frame, state, "Cardputer disconnected", tcell.ColorRed)
state.Error = "Cardputer disconnected"
UpdateDeviceStatus(app, *frame, state, "Waiting for cardputer...", tcell.ColorWhite)
} else {
UpdateDeviceStatus(app, *frame, state, "Waiting for cardputer...", tcell.ColorWhite)
}
} else {
if !device.Connected {
@@ -34,6 +37,9 @@ func MonitorDevice(ctx context.Context, app *tview.Application, frame **tview.Fr
UpdateDeviceStatus(app, *frame, state, "Error connecting to cardputer: "+err.Error(), tcell.ColorRed)
} else {
UpdateDeviceStatus(app, *frame, state, "Connected to cardputer: "+port, tcell.ColorGreen)
if state.Error == "Cardputer disconnected" {
state.Error = ""
}
}
} else {
UpdateDeviceStatus(app, *frame, state, "Connected to cardputer: "+port, tcell.ColorGreen)
@@ -47,10 +53,21 @@ func main() {
// Initialize Device manager
device := NewDevice()
// Ensure cleanup happens no matter how we exit
defer func() {
log.Println("Cleanup: Disconnecting device...")
if err := device.Close(); err != nil {
log.Printf("Cleanup: Error disconnecting device: %v\n", err)
} else {
log.Println("Cleanup: Device disconnected successfully")
}
}()
app := tview.NewApplication()
var currentFrame *tview.Frame
state := &UIState{
Page: "home",
Page: "home",
Error: "",
}
frame := AppUI(app, &currentFrame, state)
currentFrame = frame
@@ -66,15 +83,9 @@ func main() {
// Handle signals in a goroutine
go func() {
<-sigChan
log.Println("Signal received, shutting down...")
// Cancel context to stop monitoring
cancel()
// Disconnect device gracefully
if device.Connected {
log.Println("Disconnecting device...")
if err := device.Close(); err != nil {
log.Printf("Error disconnecting device: %v\n", err)
}
}
// Stop the application
app.Stop()
}()
+9 -12
View File
@@ -1,10 +1,9 @@
package main
import (
"io/fs"
"encoding/json"
"io/fs"
"strings"
"log"
)
type PackageJSON struct {
@@ -35,39 +34,37 @@ func loadPackageJSON(fsys fs.FS) (*PackageJSON, error) {
func createCpkg(fsys fs.FS, pkgData *PackageJSON) (*cpkg, error) {
pkg := NewCpkg(pkgData.Pkg)
// Walk through all directories recursively
err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
// Skip directories themselves (we only want files)
if d.IsDir() {
return nil
}
// Skip certain files
fileName := d.Name()
if fileName == "debug.log" || strings.HasSuffix(fileName, ".cpkg") {
log.Println("Skipping file:", path)
return nil
}
// Add file with full relative path
log.Println("Adding file:", path)
err = pkg.AddFile(fsys, path)
if err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
success, err := pkg.Create()
if err != nil {
return nil, err
@@ -76,4 +73,4 @@ func createCpkg(fsys fs.FS, pkgData *PackageJSON) (*cpkg, error) {
return nil, err
}
return pkg, nil
}
}
+40 -2
View File
@@ -1,12 +1,16 @@
package main
import (
"os"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
type UIState struct {
Page string
Page string
Error string
Success string
}
func AppUI(app *tview.Application, currentFrame **tview.Frame, state *UIState) *tview.Frame {
@@ -18,6 +22,7 @@ func AppUI(app *tview.Application, currentFrame **tview.Frame, state *UIState) *
app.SetRoot(newFrame, true)
})
list.AddItem("Upload to Cardputer", "Creates a package and uploads it to the connected Cardputer", 'u', nil)
list.AddItem("Live Refresh", "Monitors for changes and automatically uploads to the Cardputer (WIP)", 'l', nil)
frame := tview.NewFrame(list)
frame.SetBorders(0, 0, 0, 0, 0, 0)
frame.AddText("Parchment - v0.0.1", true, tview.AlignLeft, tcell.ColorWhite)
@@ -26,9 +31,36 @@ func AppUI(app *tview.Application, currentFrame **tview.Frame, state *UIState) *
return frame
}
func SetError(state *UIState, text string) {
state.Success = ""
state.Error = text
}
func SetSuccess(state *UIState, text string) {
state.Error = ""
state.Success = text
}
func PackageUI(app *tview.Application, currentFrame **tview.Frame, state *UIState) *tview.Frame {
list := tview.NewList()
list.AddItem("Create Package", "Creates a .cpkg file in the running directory", 'c', nil)
list.AddItem("Create Package", "Creates a .cpkg file in the running directory", 'c', func() {
fsys := os.DirFS(".")
pkgData, err := loadPackageJSON(fsys)
if err != nil {
SetError(state, "Failed to load package.json")
return
}
if pkgData == nil {
SetError(state, "Failed to load package.json")
return
}
pkg, err := createCpkg(fsys, pkgData)
if err != nil {
SetError(state, err.Error())
} else {
SetSuccess(state, "Created file "+pkg.packageName+".cpkg")
}
})
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, state)
@@ -52,6 +84,12 @@ func UpdateDeviceStatus(app *tview.Application, frame *tview.Frame, state *UISta
case "package":
frame.AddText("Package Tools", true, tview.AlignLeft, tcell.ColorWhite)
}
if state.Error != "" {
frame.AddText(state.Error, false, tview.AlignRight, tcell.ColorRed)
}
if state.Success != "" {
frame.AddText(state.Success, false, tview.AlignRight, tcell.ColorGreen)
}
frame.AddText(status, false, tview.AlignLeft, color)
})
}