dashboard / erock/pico / refactor(tui): use vaxis #48 rss

accepted · opened on 2025-02-24T12:55:08Z by erock
Help
checkout latest patchset:
ssh pr.pico.sh print pr-48 | git am -3
checkout any patchset in a patch request:
ssh pr.pico.sh print ps-X | git am -3
add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add 48
add review to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add --review 48
accept PR:
ssh pr.pico.sh pr accept 48
close PR:
ssh pr.pico.sh pr close 48

Logs

erock created pr with ps-105 on 2025-02-24T12:55:08Z
erock added ps-106 on 2025-02-25T01:16:25Z
erock changed status on 2025-03-13T14:53:20Z {"status":"accepted"}

Patchsets

ps-105 by erock on 2025-02-24T12:55:08Z
Range Diff ↕ rd-106
1: 9033000 = 1: 9033000 refactor(tui): use vaxis
-: ------- > 2: e191f12 chore(tui): setup page navigation
ps-106 by erock on 2025-02-25T01:16:25Z

Patchset ps-106

refactor(tui): use vaxis

Eric Bower
2025-02-24T12:47:08Z
go.mod
+1 -1
tuivax/ui.go
+30 -0

chore(tui): setup page navigation

Eric Bower
2025-02-25T01:15:47Z
go.mod
+3 -3
go.sum
+0 -2
tuivax/ui.go
+90 -13
Back to top

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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
diff --git a/cmd/vaxis/ssh/main.go b/cmd/vaxis/ssh/main.go
new file mode 100644
index 0000000..b176bd3
--- /dev/null
+++ b/cmd/vaxis/ssh/main.go
@@ -0,0 +1,7 @@
+package main
+
+import "github.com/picosh/pico/pico"
+
+func main() {
+	pico.StartSshServerVaxis()
+}
go.mod link
+1 -1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
diff --git a/go.mod b/go.mod
index 22d6c79..42579e7 100644
--- a/go.mod
+++ b/go.mod
@@ -26,6 +26,7 @@ replace git.sr.ht/~rockorager/vaxis => github.com/antoniomika/vaxis v0.0.0-20250
 
 require (
 	git.sr.ht/~delthas/senpai v0.3.1-0.20240425235039-206be659439e
+	git.sr.ht/~rockorager/vaxis v0.10.3
 	github.com/alecthomas/chroma/v2 v2.14.0
 	github.com/antoniomika/syncmap v1.0.0
 	github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
@@ -79,7 +80,6 @@ require (
 	codeberg.org/emersion/go-scfg v0.1.0 // indirect
 	dario.cat/mergo v1.0.0 // indirect
 	filippo.io/edwards25519 v1.1.0 // indirect
-	git.sr.ht/~rockorager/vaxis v0.10.3 // indirect
 	github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
 	github.com/DavidGamba/go-getoptions v0.31.0 // indirect
 	github.com/Masterminds/goutils v1.1.1 // indirect
pico/ssh_vaxis.go link
+126 -0
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
diff --git a/pico/ssh_vaxis.go b/pico/ssh_vaxis.go
new file mode 100644
index 0000000..37fee51
--- /dev/null
+++ b/pico/ssh_vaxis.go
@@ -0,0 +1,126 @@
+package pico
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"os/signal"
+	"syscall"
+	"time"
+
+	"git.sr.ht/~rockorager/vaxis"
+	"github.com/charmbracelet/promwish"
+	"github.com/charmbracelet/ssh"
+	"github.com/charmbracelet/wish"
+	"github.com/picosh/pico/db/postgres"
+	"github.com/picosh/pico/shared"
+	"github.com/picosh/pico/tuivax"
+	wsh "github.com/picosh/pico/wish"
+	"github.com/picosh/send/auth"
+	"github.com/picosh/send/list"
+	"github.com/picosh/send/pipe"
+	wishrsync "github.com/picosh/send/protocols/rsync"
+	"github.com/picosh/send/protocols/scp"
+	"github.com/picosh/send/protocols/sftp"
+	"github.com/picosh/send/proxy"
+	"github.com/picosh/utils"
+)
+
+func createRouterVaxis(cfg *shared.ConfigSite, handler *UploadHandler, cliHandler *CliHandler) proxy.Router {
+	return func(sh ssh.Handler, s ssh.Session) []wish.Middleware {
+		return []wish.Middleware{
+			pipe.Middleware(handler, ""),
+			list.Middleware(handler),
+			scp.Middleware(handler),
+			wishrsync.Middleware(handler),
+			auth.Middleware(handler),
+			wsh.PtyMdw(createTui()),
+			WishMiddleware(cliHandler),
+			wsh.LogMiddleware(handler.GetLogger()),
+		}
+	}
+}
+
+func withProxyVaxis(cfg *shared.ConfigSite, handler *UploadHandler, cliHandler *CliHandler, otherMiddleware ...wish.Middleware) ssh.Option {
+	return func(server *ssh.Server) error {
+		err := sftp.SSHOption(handler)(server)
+		if err != nil {
+			return err
+		}
+
+		return proxy.WithProxy(createRouterVaxis(cfg, handler, cliHandler), otherMiddleware...)(server)
+	}
+}
+
+func createTui() wish.Middleware {
+	return func(next ssh.Handler) ssh.Handler {
+		return func(sesh ssh.Session) {
+			vty, err := shared.NewVConsole(sesh)
+			if err != nil {
+				panic(err)
+			}
+			opts := vaxis.Options{
+				WithConsole: vty,
+			}
+			tuivax.NewTui(opts)
+		}
+	}
+}
+
+func StartSshServerVaxis() {
+	host := utils.GetEnv("PICO_HOST", "0.0.0.0")
+	port := utils.GetEnv("PICO_SSH_PORT", "2222")
+	promPort := utils.GetEnv("PICO_PROM_PORT", "9222")
+	cfg := NewConfigSite()
+	logger := cfg.Logger
+	dbpool := postgres.NewDB(cfg.DbURL, cfg.Logger)
+	defer dbpool.Close()
+
+	handler := NewUploadHandler(
+		dbpool,
+		cfg,
+	)
+	cliHandler := &CliHandler{
+		Logger: logger,
+		DBPool: dbpool,
+	}
+
+	sshAuth := shared.NewSshAuthHandler(dbpool, logger)
+	s, err := wish.NewServer(
+		wish.WithAddress(fmt.Sprintf("%s:%s", host, port)),
+		wish.WithHostKeyPath("ssh_data/term_info_ed25519"),
+		wish.WithPublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
+			sshAuth.PubkeyAuthHandler(ctx, key)
+			return true
+		}),
+		withProxyVaxis(
+			cfg,
+			handler,
+			cliHandler,
+			promwish.Middleware(fmt.Sprintf("%s:%s", host, promPort), "pico-ssh"),
+		),
+	)
+	if err != nil {
+		logger.Error(err.Error())
+		return
+	}
+
+	done := make(chan os.Signal, 1)
+	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
+	logger.Info("starting SSH server on", "host", host, "port", port)
+	go func() {
+		if err = s.ListenAndServe(); err != nil {
+			logger.Error("serve", "err", err.Error())
+			os.Exit(1)
+		}
+	}()
+
+	<-done
+	logger.Info("stopping SSH server")
+	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer func() { cancel() }()
+	if err := s.Shutdown(ctx); err != nil {
+		logger.Error("shutdown", "err", err.Error())
+		os.Exit(1)
+	}
+}
shared/senpai.go link
+11 -2
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
diff --git a/shared/senpai.go b/shared/senpai.go
index 016ff10..3c6f842 100644
--- a/shared/senpai.go
+++ b/shared/senpai.go
@@ -123,10 +123,9 @@ func (v *VConsole) Close() error {
 	return err
 }
 
-func NewSenpaiApp(sesh ssh.Session, username, pass string) (*senpai.App, error) {
+func NewVConsole(sesh ssh.Session) (*VConsole, error) {
 	pty, win, ok := sesh.Pty()
 	if !ok {
-		slog.Error("PTY not found")
 		return nil, fmt.Errorf("PTY not found")
 	}
 
@@ -176,6 +175,16 @@ func NewSenpaiApp(sesh ssh.Session, username, pass string) (*senpai.App, error)
 		}
 	}()
 
+	return vty, nil
+}
+
+func NewSenpaiApp(sesh ssh.Session, username, pass string) (*senpai.App, error) {
+	vty, err := NewVConsole(sesh)
+	if err != nil {
+		slog.Error("PTY not found")
+		return nil, err
+	}
+
 	senpaiCfg := senpai.Defaults()
 	senpaiCfg.TLS = true
 	senpaiCfg.Addr = "irc.pico.sh:6697"
tuivax/ui.go link
+30 -0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
diff --git a/tuivax/ui.go b/tuivax/ui.go
new file mode 100644
index 0000000..538b789
--- /dev/null
+++ b/tuivax/ui.go
@@ -0,0 +1,30 @@
+package tuivax
+
+import (
+	"git.sr.ht/~rockorager/vaxis"
+)
+
+func NewTui(opts vaxis.Options) {
+	vx, err := vaxis.New(opts)
+	if err != nil {
+		panic(err)
+	}
+	defer vx.Close()
+	for ev := range vx.Events() {
+		switch ev := ev.(type) {
+		case vaxis.Key:
+			switch ev.String() {
+			case "Ctrl+c":
+				return
+			case "q":
+				return
+			case "Escape":
+				return
+			}
+		}
+		win := vx.Window()
+		win.Clear()
+		win.Print(vaxis.Segment{Text: "Hello, World!"})
+		vx.Render()
+	}
+}

chore(tui): setup page navigation

go.mod link
+3 -3
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
diff --git a/go.mod b/go.mod
index 42579e7..9b199b5 100644
--- a/go.mod
+++ b/go.mod
@@ -16,17 +16,17 @@ go 1.23.1
 
 // replace git.sr.ht/~delthas/senpai => ../../senpai
 
-// replace git.sr.ht/~rockorager/vaxis => ../../vaxis
+replace git.sr.ht/~rockorager/vaxis => ../../../src/vaxis
 
 // replace github.com/charmbracelet/wish => ../../wish
 
 replace git.sr.ht/~delthas/senpai => github.com/antoniomika/senpai v0.0.0-20250114180426-3061ddccec76
 
-replace git.sr.ht/~rockorager/vaxis => github.com/antoniomika/vaxis v0.0.0-20250114030546-8524674789ca
+// replace git.sr.ht/~rockorager/vaxis => github.com/antoniomika/vaxis v0.0.0-20250114030546-8524674789ca
 
 require (
 	git.sr.ht/~delthas/senpai v0.3.1-0.20240425235039-206be659439e
-	git.sr.ht/~rockorager/vaxis v0.10.3
+	git.sr.ht/~rockorager/vaxis v0.12.1-0.20250219164615-8231ece877f3
 	github.com/alecthomas/chroma/v2 v2.14.0
 	github.com/antoniomika/syncmap v1.0.0
 	github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
go.sum link
+0 -2
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
diff --git a/go.sum b/go.sum
index 89d665e..00c9214 100644
--- a/go.sum
+++ b/go.sum
@@ -68,8 +68,6 @@ github.com/antoniomika/senpai v0.0.0-20250114180426-3061ddccec76 h1:WAGPWAxThUoz
 github.com/antoniomika/senpai v0.0.0-20250114180426-3061ddccec76/go.mod h1:HIPY7uPDy44ep6xKwfsW6JTmDUTiC5DXh0l4yZP/aT4=
 github.com/antoniomika/syncmap v1.0.0 h1:iFSfbQFQOvHZILFZF+hqWosO0no+W9+uF4y2VEyMKWU=
 github.com/antoniomika/syncmap v1.0.0/go.mod h1:fK2829foEYnO4riNfyUn0SHQZt4ue3DStYjGU+sJj38=
-github.com/antoniomika/vaxis v0.0.0-20250114030546-8524674789ca h1:vD1Ioetf2ikrlkvHu7FIpbPgtMIJpaU16YRR+HA+/2Y=
-github.com/antoniomika/vaxis v0.0.0-20250114030546-8524674789ca/go.mod h1:h94aKek3frIV1hJbdXjqnBqaLkbWXvV+UxAsQHg9bns=
 github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
 github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
pico/ssh_vaxis.go link
+13 -4
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
diff --git a/pico/ssh_vaxis.go b/pico/ssh_vaxis.go
index 37fee51..d109539 100644
--- a/pico/ssh_vaxis.go
+++ b/pico/ssh_vaxis.go
@@ -14,6 +14,7 @@ import (
 	"github.com/charmbracelet/wish"
 	"github.com/picosh/pico/db/postgres"
 	"github.com/picosh/pico/shared"
+	"github.com/picosh/pico/tui/common"
 	"github.com/picosh/pico/tuivax"
 	wsh "github.com/picosh/pico/wish"
 	"github.com/picosh/send/auth"
@@ -27,14 +28,22 @@ import (
 )
 
 func createRouterVaxis(cfg *shared.ConfigSite, handler *UploadHandler, cliHandler *CliHandler) proxy.Router {
-	return func(sh ssh.Handler, s ssh.Session) []wish.Middleware {
+	return func(sh ssh.Handler, sesh ssh.Session) []wish.Middleware {
+		shrd := &common.SharedModel{
+			Session: sesh,
+			Cfg:     cfg,
+			Dbpool:  handler.DBPool,
+			Width:   80,
+			Height:  24,
+			Logger:  cfg.Logger,
+		}
 		return []wish.Middleware{
 			pipe.Middleware(handler, ""),
 			list.Middleware(handler),
 			scp.Middleware(handler),
 			wishrsync.Middleware(handler),
 			auth.Middleware(handler),
-			wsh.PtyMdw(createTui()),
+			wsh.PtyMdw(createTui(shrd)),
 			WishMiddleware(cliHandler),
 			wsh.LogMiddleware(handler.GetLogger()),
 		}
@@ -52,7 +61,7 @@ func withProxyVaxis(cfg *shared.ConfigSite, handler *UploadHandler, cliHandler *
 	}
 }
 
-func createTui() wish.Middleware {
+func createTui(shrd *common.SharedModel) wish.Middleware {
 	return func(next ssh.Handler) ssh.Handler {
 		return func(sesh ssh.Session) {
 			vty, err := shared.NewVConsole(sesh)
@@ -62,7 +71,7 @@ func createTui() wish.Middleware {
 			opts := vaxis.Options{
 				WithConsole: vty,
 			}
-			tuivax.NewTui(opts)
+			tuivax.NewTui(opts, shrd)
 		}
 	}
 }
tui/pages/pages.go link
+8 -1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
diff --git a/tui/pages/pages.go b/tui/pages/pages.go
index 0859b25..e5a1861 100644
--- a/tui/pages/pages.go
+++ b/tui/pages/pages.go
@@ -1,6 +1,9 @@
 package pages
 
-import tea "github.com/charmbracelet/bubbletea"
+import (
+	"git.sr.ht/~rockorager/vaxis/vxfw"
+	tea "github.com/charmbracelet/bubbletea"
+)
 
 type Page int
 
@@ -27,6 +30,10 @@ func Navigate(page Page) tea.Cmd {
 	}
 }
 
+func NavigateVx(page Page) vxfw.Command {
+	return NavigateMsg{page}
+}
+
 func ToTitle(page Page) string {
 	switch page {
 	case CreateAccountPage:
tuivax/ui.go link
+90 -13
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
diff --git a/tuivax/ui.go b/tuivax/ui.go
index 538b789..a4d25c4 100644
--- a/tuivax/ui.go
+++ b/tuivax/ui.go
@@ -1,30 +1,107 @@
 package tuivax
 
 import (
+	"fmt"
+
 	"git.sr.ht/~rockorager/vaxis"
+	"git.sr.ht/~rockorager/vaxis/widgets/list"
+	"github.com/picosh/pico/tui/common"
 )
 
-func NewTui(opts vaxis.Options) {
+var menuChoices = []string{
+	"pubkeys",
+	"tokens",
+	"settings",
+	"logs",
+	"analytics",
+	"chat",
+	"pico+",
+}
+
+type UIVx struct {
+	shared *common.SharedModel
+	vx     *vaxis.Vaxis
+
+	page string
+	quit bool
+	menu list.List
+}
+
+type Navigate struct {
+	To string
+}
+
+type Quit struct{}
+
+func (ui *UIVx) menuPage(win vaxis.Window, ev vaxis.Event) {
+	switch msg := ev.(type) {
+	case vaxis.Key:
+		switch msg.String() {
+		case "Ctrl+c", "q", "Escape":
+			ui.quit = true
+		case "Down", "j":
+			ui.menu.Down()
+		case "Up", "k":
+			ui.menu.Up()
+		case "Enter":
+			ui.page = menuChoices[ui.menu.Index()]
+		}
+	}
+	ui.menu.Draw(win)
+}
+
+func (ui *UIVx) keysPage(win vaxis.Window, ev vaxis.Event) {
+	switch msg := ev.(type) {
+	case vaxis.Key:
+		switch msg.String() {
+		case "Ctrl+c":
+			ui.quit = true
+		case "q", "Escape":
+			ui.page = "menu"
+		}
+	}
+	win.Print(vaxis.Segment{Text: "Hello, World!"})
+}
+
+func NewTui(opts vaxis.Options, shared *common.SharedModel) {
 	vx, err := vaxis.New(opts)
 	if err != nil {
 		panic(err)
 	}
 	defer vx.Close()
+
+	ui := UIVx{
+		shared: shared,
+		vx:     vx,
+
+		page: "menu",
+		menu: list.New(menuChoices),
+	}
+
 	for ev := range vx.Events() {
-		switch ev := ev.(type) {
-		case vaxis.Key:
-			switch ev.String() {
-			case "Ctrl+c":
-				return
-			case "q":
-				return
-			case "Escape":
-				return
-			}
-		}
 		win := vx.Window()
 		win.Clear()
-		win.Print(vaxis.Segment{Text: "Hello, World!"})
+
+		// header
+		win.Print(vaxis.Segment{
+			Text: fmt.Sprintf("pico.sh • %s", ui.page),
+		})
+
+		// page window
+		width, height := win.Size()
+		pageWin := win.New(0, 2, width, height-2)
+
+		switch ui.page {
+		case "menu":
+			ui.menuPage(pageWin, ev)
+		case "pubkeys":
+			ui.keysPage(pageWin, ev)
+		}
+
+		if ui.quit {
+			return
+		}
+
 		vx.Render()
 	}
 }