Logs
Patchset ps-106
refactor(tui): use vaxis
Eric Bower
cmd/vaxis/ssh/main.go
+7
-0
go.mod
+1
-1
pico/ssh_vaxis.go
+126
-0
shared/senpai.go
+11
-2
tuivax/ui.go
+30
-0
chore(tui): setup page navigation
Eric Bower
go.mod
+3
-3
go.sum
+0
-2
pico/ssh_vaxis.go
+13
-4
tui/pages/pages.go
+8
-1
tuivax/ui.go
+90
-13
refactor(tui): use vaxis
While we really enjoyed the charm stack for our ssh apps, we are at a point where we want to reduce our overall dependencies for our SSH apps. With charm we have: - `crypto/ssh` - `gliberlabs/ssh` - `charmbracelet/ssh` - `charmbracelet/wish` There's a lot that can go wrong here and we have seen quite a bit of thrashing within these libraries that required us to make moderate changes when upgrading. We also enjoyed bubbletea/lipgloss but we are at the point where we would like to switch to `vaxis` since it is more inline with our design ethos. We are basically going to replace 5 go packages with 1 and we are starting with the TUI.
cmd/vaxis/ssh/main.go
link
+7
-0
+7
-0
go.mod
link
+1
-1
+1
-1
1diff --git a/go.mod b/go.mod
2index 22d6c79..42579e7 100644
3--- a/go.mod
4+++ b/go.mod
5@@ -26,6 +26,7 @@ replace git.sr.ht/~rockorager/vaxis => github.com/antoniomika/vaxis v0.0.0-20250
6
7 require (
8 git.sr.ht/~delthas/senpai v0.3.1-0.20240425235039-206be659439e
9+ git.sr.ht/~rockorager/vaxis v0.10.3
10 github.com/alecthomas/chroma/v2 v2.14.0
11 github.com/antoniomika/syncmap v1.0.0
12 github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
13@@ -79,7 +80,6 @@ require (
14 codeberg.org/emersion/go-scfg v0.1.0 // indirect
15 dario.cat/mergo v1.0.0 // indirect
16 filippo.io/edwards25519 v1.1.0 // indirect
17- git.sr.ht/~rockorager/vaxis v0.10.3 // indirect
18 github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
19 github.com/DavidGamba/go-getoptions v0.31.0 // indirect
20 github.com/Masterminds/goutils v1.1.1 // indirect
pico/ssh_vaxis.go
link
+126
-0
+126
-0
1diff --git a/pico/ssh_vaxis.go b/pico/ssh_vaxis.go
2new file mode 100644
3index 0000000..37fee51
4--- /dev/null
5+++ b/pico/ssh_vaxis.go
6@@ -0,0 +1,126 @@
7+package pico
8+
9+import (
10+ "context"
11+ "fmt"
12+ "os"
13+ "os/signal"
14+ "syscall"
15+ "time"
16+
17+ "git.sr.ht/~rockorager/vaxis"
18+ "github.com/charmbracelet/promwish"
19+ "github.com/charmbracelet/ssh"
20+ "github.com/charmbracelet/wish"
21+ "github.com/picosh/pico/db/postgres"
22+ "github.com/picosh/pico/shared"
23+ "github.com/picosh/pico/tuivax"
24+ wsh "github.com/picosh/pico/wish"
25+ "github.com/picosh/send/auth"
26+ "github.com/picosh/send/list"
27+ "github.com/picosh/send/pipe"
28+ wishrsync "github.com/picosh/send/protocols/rsync"
29+ "github.com/picosh/send/protocols/scp"
30+ "github.com/picosh/send/protocols/sftp"
31+ "github.com/picosh/send/proxy"
32+ "github.com/picosh/utils"
33+)
34+
35+func createRouterVaxis(cfg *shared.ConfigSite, handler *UploadHandler, cliHandler *CliHandler) proxy.Router {
36+ return func(sh ssh.Handler, s ssh.Session) []wish.Middleware {
37+ return []wish.Middleware{
38+ pipe.Middleware(handler, ""),
39+ list.Middleware(handler),
40+ scp.Middleware(handler),
41+ wishrsync.Middleware(handler),
42+ auth.Middleware(handler),
43+ wsh.PtyMdw(createTui()),
44+ WishMiddleware(cliHandler),
45+ wsh.LogMiddleware(handler.GetLogger()),
46+ }
47+ }
48+}
49+
50+func withProxyVaxis(cfg *shared.ConfigSite, handler *UploadHandler, cliHandler *CliHandler, otherMiddleware ...wish.Middleware) ssh.Option {
51+ return func(server *ssh.Server) error {
52+ err := sftp.SSHOption(handler)(server)
53+ if err != nil {
54+ return err
55+ }
56+
57+ return proxy.WithProxy(createRouterVaxis(cfg, handler, cliHandler), otherMiddleware...)(server)
58+ }
59+}
60+
61+func createTui() wish.Middleware {
62+ return func(next ssh.Handler) ssh.Handler {
63+ return func(sesh ssh.Session) {
64+ vty, err := shared.NewVConsole(sesh)
65+ if err != nil {
66+ panic(err)
67+ }
68+ opts := vaxis.Options{
69+ WithConsole: vty,
70+ }
71+ tuivax.NewTui(opts)
72+ }
73+ }
74+}
75+
76+func StartSshServerVaxis() {
77+ host := utils.GetEnv("PICO_HOST", "0.0.0.0")
78+ port := utils.GetEnv("PICO_SSH_PORT", "2222")
79+ promPort := utils.GetEnv("PICO_PROM_PORT", "9222")
80+ cfg := NewConfigSite()
81+ logger := cfg.Logger
82+ dbpool := postgres.NewDB(cfg.DbURL, cfg.Logger)
83+ defer dbpool.Close()
84+
85+ handler := NewUploadHandler(
86+ dbpool,
87+ cfg,
88+ )
89+ cliHandler := &CliHandler{
90+ Logger: logger,
91+ DBPool: dbpool,
92+ }
93+
94+ sshAuth := shared.NewSshAuthHandler(dbpool, logger)
95+ s, err := wish.NewServer(
96+ wish.WithAddress(fmt.Sprintf("%s:%s", host, port)),
97+ wish.WithHostKeyPath("ssh_data/term_info_ed25519"),
98+ wish.WithPublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
99+ sshAuth.PubkeyAuthHandler(ctx, key)
100+ return true
101+ }),
102+ withProxyVaxis(
103+ cfg,
104+ handler,
105+ cliHandler,
106+ promwish.Middleware(fmt.Sprintf("%s:%s", host, promPort), "pico-ssh"),
107+ ),
108+ )
109+ if err != nil {
110+ logger.Error(err.Error())
111+ return
112+ }
113+
114+ done := make(chan os.Signal, 1)
115+ signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
116+ logger.Info("starting SSH server on", "host", host, "port", port)
117+ go func() {
118+ if err = s.ListenAndServe(); err != nil {
119+ logger.Error("serve", "err", err.Error())
120+ os.Exit(1)
121+ }
122+ }()
123+
124+ <-done
125+ logger.Info("stopping SSH server")
126+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
127+ defer func() { cancel() }()
128+ if err := s.Shutdown(ctx); err != nil {
129+ logger.Error("shutdown", "err", err.Error())
130+ os.Exit(1)
131+ }
132+}
tuivax/ui.go
link
+30
-0
+30
-0
1diff --git a/tuivax/ui.go b/tuivax/ui.go
2new file mode 100644
3index 0000000..538b789
4--- /dev/null
5+++ b/tuivax/ui.go
6@@ -0,0 +1,30 @@
7+package tuivax
8+
9+import (
10+ "git.sr.ht/~rockorager/vaxis"
11+)
12+
13+func NewTui(opts vaxis.Options) {
14+ vx, err := vaxis.New(opts)
15+ if err != nil {
16+ panic(err)
17+ }
18+ defer vx.Close()
19+ for ev := range vx.Events() {
20+ switch ev := ev.(type) {
21+ case vaxis.Key:
22+ switch ev.String() {
23+ case "Ctrl+c":
24+ return
25+ case "q":
26+ return
27+ case "Escape":
28+ return
29+ }
30+ }
31+ win := vx.Window()
32+ win.Clear()
33+ win.Print(vaxis.Segment{Text: "Hello, World!"})
34+ vx.Render()
35+ }
36+}
chore(tui): setup page navigation
go.mod
link
+3
-3
+3
-3
1diff --git a/go.mod b/go.mod
2index 42579e7..9b199b5 100644
3--- a/go.mod
4+++ b/go.mod
5@@ -16,17 +16,17 @@ go 1.23.1
6
7 // replace git.sr.ht/~delthas/senpai => ../../senpai
8
9-// replace git.sr.ht/~rockorager/vaxis => ../../vaxis
10+replace git.sr.ht/~rockorager/vaxis => ../../../src/vaxis
11
12 // replace github.com/charmbracelet/wish => ../../wish
13
14 replace git.sr.ht/~delthas/senpai => github.com/antoniomika/senpai v0.0.0-20250114180426-3061ddccec76
15
16-replace git.sr.ht/~rockorager/vaxis => github.com/antoniomika/vaxis v0.0.0-20250114030546-8524674789ca
17+// replace git.sr.ht/~rockorager/vaxis => github.com/antoniomika/vaxis v0.0.0-20250114030546-8524674789ca
18
19 require (
20 git.sr.ht/~delthas/senpai v0.3.1-0.20240425235039-206be659439e
21- git.sr.ht/~rockorager/vaxis v0.10.3
22+ git.sr.ht/~rockorager/vaxis v0.12.1-0.20250219164615-8231ece877f3
23 github.com/alecthomas/chroma/v2 v2.14.0
24 github.com/antoniomika/syncmap v1.0.0
25 github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
go.sum
link
+0
-2
+0
-2
1diff --git a/go.sum b/go.sum
2index 89d665e..00c9214 100644
3--- a/go.sum
4+++ b/go.sum
5@@ -68,8 +68,6 @@ github.com/antoniomika/senpai v0.0.0-20250114180426-3061ddccec76 h1:WAGPWAxThUoz
6 github.com/antoniomika/senpai v0.0.0-20250114180426-3061ddccec76/go.mod h1:HIPY7uPDy44ep6xKwfsW6JTmDUTiC5DXh0l4yZP/aT4=
7 github.com/antoniomika/syncmap v1.0.0 h1:iFSfbQFQOvHZILFZF+hqWosO0no+W9+uF4y2VEyMKWU=
8 github.com/antoniomika/syncmap v1.0.0/go.mod h1:fK2829foEYnO4riNfyUn0SHQZt4ue3DStYjGU+sJj38=
9-github.com/antoniomika/vaxis v0.0.0-20250114030546-8524674789ca h1:vD1Ioetf2ikrlkvHu7FIpbPgtMIJpaU16YRR+HA+/2Y=
10-github.com/antoniomika/vaxis v0.0.0-20250114030546-8524674789ca/go.mod h1:h94aKek3frIV1hJbdXjqnBqaLkbWXvV+UxAsQHg9bns=
11 github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
12 github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
13 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
pico/ssh_vaxis.go
link
+13
-4
+13
-4
1diff --git a/pico/ssh_vaxis.go b/pico/ssh_vaxis.go
2index 37fee51..d109539 100644
3--- a/pico/ssh_vaxis.go
4+++ b/pico/ssh_vaxis.go
5@@ -14,6 +14,7 @@ import (
6 "github.com/charmbracelet/wish"
7 "github.com/picosh/pico/db/postgres"
8 "github.com/picosh/pico/shared"
9+ "github.com/picosh/pico/tui/common"
10 "github.com/picosh/pico/tuivax"
11 wsh "github.com/picosh/pico/wish"
12 "github.com/picosh/send/auth"
13@@ -27,14 +28,22 @@ import (
14 )
15
16 func createRouterVaxis(cfg *shared.ConfigSite, handler *UploadHandler, cliHandler *CliHandler) proxy.Router {
17- return func(sh ssh.Handler, s ssh.Session) []wish.Middleware {
18+ return func(sh ssh.Handler, sesh ssh.Session) []wish.Middleware {
19+ shrd := &common.SharedModel{
20+ Session: sesh,
21+ Cfg: cfg,
22+ Dbpool: handler.DBPool,
23+ Width: 80,
24+ Height: 24,
25+ Logger: cfg.Logger,
26+ }
27 return []wish.Middleware{
28 pipe.Middleware(handler, ""),
29 list.Middleware(handler),
30 scp.Middleware(handler),
31 wishrsync.Middleware(handler),
32 auth.Middleware(handler),
33- wsh.PtyMdw(createTui()),
34+ wsh.PtyMdw(createTui(shrd)),
35 WishMiddleware(cliHandler),
36 wsh.LogMiddleware(handler.GetLogger()),
37 }
38@@ -52,7 +61,7 @@ func withProxyVaxis(cfg *shared.ConfigSite, handler *UploadHandler, cliHandler *
39 }
40 }
41
42-func createTui() wish.Middleware {
43+func createTui(shrd *common.SharedModel) wish.Middleware {
44 return func(next ssh.Handler) ssh.Handler {
45 return func(sesh ssh.Session) {
46 vty, err := shared.NewVConsole(sesh)
47@@ -62,7 +71,7 @@ func createTui() wish.Middleware {
48 opts := vaxis.Options{
49 WithConsole: vty,
50 }
51- tuivax.NewTui(opts)
52+ tuivax.NewTui(opts, shrd)
53 }
54 }
55 }
tui/pages/pages.go
link
+8
-1
+8
-1
1diff --git a/tui/pages/pages.go b/tui/pages/pages.go
2index 0859b25..e5a1861 100644
3--- a/tui/pages/pages.go
4+++ b/tui/pages/pages.go
5@@ -1,6 +1,9 @@
6 package pages
7
8-import tea "github.com/charmbracelet/bubbletea"
9+import (
10+ "git.sr.ht/~rockorager/vaxis/vxfw"
11+ tea "github.com/charmbracelet/bubbletea"
12+)
13
14 type Page int
15
16@@ -27,6 +30,10 @@ func Navigate(page Page) tea.Cmd {
17 }
18 }
19
20+func NavigateVx(page Page) vxfw.Command {
21+ return NavigateMsg{page}
22+}
23+
24 func ToTitle(page Page) string {
25 switch page {
26 case CreateAccountPage:
tuivax/ui.go
link
+90
-13
+90
-13
1diff --git a/tuivax/ui.go b/tuivax/ui.go
2index 538b789..a4d25c4 100644
3--- a/tuivax/ui.go
4+++ b/tuivax/ui.go
5@@ -1,30 +1,107 @@
6 package tuivax
7
8 import (
9+ "fmt"
10+
11 "git.sr.ht/~rockorager/vaxis"
12+ "git.sr.ht/~rockorager/vaxis/widgets/list"
13+ "github.com/picosh/pico/tui/common"
14 )
15
16-func NewTui(opts vaxis.Options) {
17+var menuChoices = []string{
18+ "pubkeys",
19+ "tokens",
20+ "settings",
21+ "logs",
22+ "analytics",
23+ "chat",
24+ "pico+",
25+}
26+
27+type UIVx struct {
28+ shared *common.SharedModel
29+ vx *vaxis.Vaxis
30+
31+ page string
32+ quit bool
33+ menu list.List
34+}
35+
36+type Navigate struct {
37+ To string
38+}
39+
40+type Quit struct{}
41+
42+func (ui *UIVx) menuPage(win vaxis.Window, ev vaxis.Event) {
43+ switch msg := ev.(type) {
44+ case vaxis.Key:
45+ switch msg.String() {
46+ case "Ctrl+c", "q", "Escape":
47+ ui.quit = true
48+ case "Down", "j":
49+ ui.menu.Down()
50+ case "Up", "k":
51+ ui.menu.Up()
52+ case "Enter":
53+ ui.page = menuChoices[ui.menu.Index()]
54+ }
55+ }
56+ ui.menu.Draw(win)
57+}
58+
59+func (ui *UIVx) keysPage(win vaxis.Window, ev vaxis.Event) {
60+ switch msg := ev.(type) {
61+ case vaxis.Key:
62+ switch msg.String() {
63+ case "Ctrl+c":
64+ ui.quit = true
65+ case "q", "Escape":
66+ ui.page = "menu"
67+ }
68+ }
69+ win.Print(vaxis.Segment{Text: "Hello, World!"})
70+}
71+
72+func NewTui(opts vaxis.Options, shared *common.SharedModel) {
73 vx, err := vaxis.New(opts)
74 if err != nil {
75 panic(err)
76 }
77 defer vx.Close()
78+
79+ ui := UIVx{
80+ shared: shared,
81+ vx: vx,
82+
83+ page: "menu",
84+ menu: list.New(menuChoices),
85+ }
86+
87 for ev := range vx.Events() {
88- switch ev := ev.(type) {
89- case vaxis.Key:
90- switch ev.String() {
91- case "Ctrl+c":
92- return
93- case "q":
94- return
95- case "Escape":
96- return
97- }
98- }
99 win := vx.Window()
100 win.Clear()
101- win.Print(vaxis.Segment{Text: "Hello, World!"})
102+
103+ // header
104+ win.Print(vaxis.Segment{
105+ Text: fmt.Sprintf("pico.sh • %s", ui.page),
106+ })
107+
108+ // page window
109+ width, height := win.Size()
110+ pageWin := win.New(0, 2, width, height-2)
111+
112+ switch ui.page {
113+ case "menu":
114+ ui.menuPage(pageWin, ev)
115+ case "pubkeys":
116+ ui.keysPage(pageWin, ev)
117+ }
118+
119+ if ui.quit {
120+ return
121+ }
122+
123 vx.Render()
124 }
125 }