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
+24 -5
View File
@@ -10,8 +10,8 @@ import (
// Device manages the serial connection to the Cardputer. // Device manages the serial connection to the Cardputer.
type Device struct { type Device struct {
PortName string PortName string
Port serial.Port Port serial.Port
Connected bool Connected bool
} }
@@ -59,9 +59,22 @@ func (d *Device) Connect() error {
d.Close() d.Close()
return fmt.Errorf("failed to send dev mode request: %w", err) return fmt.Errorf("failed to send dev mode request: %w", err)
} }
// Allow some time for the device to respond or settle if needed // Allow some time for the device to respond or settle if needed
time.Sleep(100 * time.Millisecond) 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 return nil
} }
@@ -69,6 +82,12 @@ func (d *Device) Connect() error {
// Close closes the serial port. // Close closes the serial port.
func (d *Device) Close() error { func (d *Device) Close() error {
if d.Port != nil { 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() err := d.Port.Close()
d.Port = nil d.Port = nil
d.Connected = false d.Connected = false
+5 -1
View File
@@ -14,6 +14,8 @@ require (
github.com/charmbracelet/x/term v0.2.1 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/creack/goselect v0.1.2 // indirect github.com/creack/goselect v0.1.2 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // 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/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // 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/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // 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/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.bug.st/serial v1.6.4 // indirect go.bug.st/serial v1.6.4 // indirect
golang.org/x/sys v0.36.0 // 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
) )
+72
View File
@@ -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/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 h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= 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 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 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= 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/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 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= 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.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 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 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 h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 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 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A=
go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI= 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-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.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 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 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=
+68 -34
View File
@@ -1,50 +1,84 @@
package main package main
import ( import (
"fmt" "context"
"log" "log"
"os" "os"
"os/signal"
"syscall"
"time"
tea "github.com/charmbracelet/bubbletea" "github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
) )
func main() { func MonitorDevice(ctx context.Context, app *tview.Application, frame **tview.Frame, device *Device) {
// Setup logging to file ticker := time.NewTicker(100 * time.Millisecond) // Check every 100ms
f, err := tea.LogToFile("debug.log", "debug") defer ticker.Stop()
if err != nil { for {
fmt.Println("fatal:", err) select {
os.Exit(1) 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 // Initialize Device manager
device := NewDevice() 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 app := tview.NewApplication()
p := tea.NewProgram(m, tea.WithAltScreen()) var currentFrame *tview.Frame
if _, err := p.Run(); err != nil { frame := AppUI(app, &currentFrame)
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, &currentFrame, device)
if err := app.Run(); err != nil {
log.Fatal("Error running program:", err) log.Fatal("Error running program:", err)
} }
} }
+35 -317
View File
@@ -1,328 +1,46 @@
package main package main
import ( import (
"fmt" "github.com/gdamore/tcell/v2"
"log" "github.com/rivo/tview"
"os"
"strings"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
) )
// AppStatus represents the current state of the application. func AppUI(app *tview.Application, currentFrame **tview.Frame) *tview.Frame {
type AppStatus int 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 ( return frame
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 func PackageUI(app *tview.Application, currentFrame **tview.Frame) *tview.Frame {
type connectMsg struct { list := tview.NewList()
err error 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 { func UpdateDeviceStatus(app *tview.Application, frame *tview.Frame, status string, color tcell.Color) {
err error app.QueueUpdateDraw(func() {
} frame.Clear()
frame.AddText("Parchment - v0.0.1", true, tview.AlignLeft, tcell.ColorWhite)
type unpackMsg struct { frame.AddText(status, false, tview.AlignLeft, color)
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}
}
} }