mirror of
https://github.com/wisplite/parchment.git
synced 2026-06-27 13:47:08 -05:00
updated the app to use tview because the bubbletea code was unmaintainable
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -60,8 +60,21 @@ func (d *Device) Connect() error {
|
||||
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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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()
|
||||
app := tview.NewApplication()
|
||||
var currentFrame *tview.Frame
|
||||
frame := AppUI(app, ¤tFrame)
|
||||
currentFrame = frame
|
||||
app.SetRoot(frame, true)
|
||||
|
||||
// Initialize UI Model
|
||||
m := InitialModel(device)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
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.
|
||||
}
|
||||
// Set up signal handling for graceful shutdown
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
// Create and run the Bubble Tea program
|
||||
p := tea.NewProgram(m, tea.WithAltScreen())
|
||||
if _, err := p.Run(); err != nil {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user