diff --git a/device.go b/device.go index d3959be..a7a34dd 100644 --- a/device.go +++ b/device.go @@ -10,8 +10,8 @@ import ( // Device manages the serial connection to the Cardputer. type Device struct { - PortName string - Port serial.Port + PortName string + Port serial.Port Connected bool } @@ -59,9 +59,22 @@ func (d *Device) Connect() error { d.Close() return fmt.Errorf("failed to send dev mode request: %w", err) } - - // Allow some time for the device to respond or settle if needed - time.Sleep(100 * time.Millisecond) + + // Allow some time for the device to respond or settle if needed + time.Sleep(100 * time.Millisecond) + + return nil +} + +// 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") + } + + // 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. return nil } @@ -69,6 +82,12 @@ func (d *Device) Connect() error { // Close closes the serial port. func (d *Device) Close() error { if d.Port != nil { + // Try to exit dev mode before closing + _ = d.ExitDevMode() + + // Allow some time for the command to be processed + time.Sleep(100 * time.Millisecond) + err := d.Port.Close() d.Port = nil d.Connected = false diff --git a/go.mod b/go.mod index 8e4f835..fdcd717 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,8 @@ require ( github.com/charmbracelet/x/term v0.2.1 // indirect 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/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 @@ -21,9 +23,11 @@ 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/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/text v0.3.8 // indirect + golang.org/x/term v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect ) diff --git a/go.sum b/go.sum index 4129e84..7209f16 100644 --- a/go.sum +++ b/go.sum @@ -20,6 +20,11 @@ github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0 github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= +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/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= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -34,18 +39,85 @@ 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/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= +github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A= go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +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/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= golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +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/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= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/main.go b/main.go index 5957fa5..26ed892 100644 --- a/main.go +++ b/main.go @@ -1,50 +1,84 @@ package main import ( - "fmt" + "context" "log" "os" + "os/signal" + "syscall" + "time" - tea "github.com/charmbracelet/bubbletea" + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" ) -func main() { - // Setup logging to file - f, err := tea.LogToFile("debug.log", "debug") - if err != nil { - fmt.Println("fatal:", err) - os.Exit(1) +func MonitorDevice(ctx context.Context, app *tview.Application, frame **tview.Frame, device *Device) { + ticker := time.NewTicker(100 * time.Millisecond) // Check every 100ms + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + port, err := device.FindCardputer() + if err != nil { + if device.Connected { + device.Close() + UpdateDeviceStatus(app, *frame, "Cardputer disconnected", tcell.ColorRed) + } + } else { + if !device.Connected { + UpdateDeviceStatus(app, *frame, "Cardputer found: "+port, tcell.ColorWhite) + err = device.Connect() + if err != nil { + UpdateDeviceStatus(app, *frame, "Error connecting to cardputer: "+err.Error(), tcell.ColorRed) + } else { + UpdateDeviceStatus(app, *frame, "Connected to cardputer: "+port, tcell.ColorGreen) + } + } else { + UpdateDeviceStatus(app, *frame, "Connected to cardputer: "+port, tcell.ColorGreen) + } + } + } } - defer f.Close() +} +func main() { // Initialize Device manager device := NewDevice() - - // Initial search for device - // We do this synchronously at startup for convenience, - // but the UI could also handle re-scanning. - portName, err := device.FindCardputer() - - // Initialize UI Model - m := InitialModel(device) - - if err == nil { - m.Status = StatusFound - log.Printf("Found Cardputer at %s", portName) - } else { - log.Printf("Cardputer not found at startup: %v", err) - // We start in Searching state, user might plug it in later if we implemented auto-scan, - // but for now it just shows searching or error. - // Since our basic implementation does a one-time scan at start, - // if it fails, we might want to let the user retry or just show the state. - // The current InitialModel defaults to StatusSearching. - // To match original behavior (sort of), if we don't find it, we stay in Searching/Error. - // Actually, let's just leave it as StatusSearching or set Error if we want to be explicit. - } - // Create and run the Bubble Tea program - p := tea.NewProgram(m, tea.WithAltScreen()) - if _, err := p.Run(); err != nil { + app := tview.NewApplication() + var currentFrame *tview.Frame + frame := AppUI(app, ¤tFrame) + currentFrame = frame + app.SetRoot(frame, true) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Set up signal handling for graceful shutdown + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + + // Handle signals in a goroutine + go func() { + <-sigChan + // 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() + }() + + go MonitorDevice(ctx, app, ¤tFrame, device) + + if err := app.Run(); err != nil { log.Fatal("Error running program:", err) } } diff --git a/tui.go b/tui.go index 2ae4291..22ba9a2 100644 --- a/tui.go +++ b/tui.go @@ -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) + }) }