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

open · opened on 2025-02-24T12:55:08Z by erock
Help
# 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
# remove patchset
ssh pr.pico.sh ps rm ps-x
# checkout all patches
ssh pr.pico.sh pr print 48 | git am -3
# print a diff between the last two patches in a patch request
ssh pr.pico.sh pr diff 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

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-105

refactor(tui): use vaxis

Eric Bower
2025-02-24T12:47:08Z
go.mod
+1 -1
tuivax/ui.go
+30 -0
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
 1diff --git a/cmd/vaxis/ssh/main.go b/cmd/vaxis/ssh/main.go
 2new file mode 100644
 3index 0000000..b176bd3
 4--- /dev/null
 5+++ b/cmd/vaxis/ssh/main.go
 6@@ -0,0 +1,7 @@
 7+package main
 8+
 9+import "github.com/picosh/pico/pico"
10+
11+func main() {
12+	pico.StartSshServerVaxis()
13+}
go.mod link
+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
  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+}
shared/senpai.go link
+11 -2
 1diff --git a/shared/senpai.go b/shared/senpai.go
 2index 016ff10..3c6f842 100644
 3--- a/shared/senpai.go
 4+++ b/shared/senpai.go
 5@@ -123,10 +123,9 @@ func (v *VConsole) Close() error {
 6 	return err
 7 }
 8 
 9-func NewSenpaiApp(sesh ssh.Session, username, pass string) (*senpai.App, error) {
10+func NewVConsole(sesh ssh.Session) (*VConsole, error) {
11 	pty, win, ok := sesh.Pty()
12 	if !ok {
13-		slog.Error("PTY not found")
14 		return nil, fmt.Errorf("PTY not found")
15 	}
16 
17@@ -176,6 +175,16 @@ func NewSenpaiApp(sesh ssh.Session, username, pass string) (*senpai.App, error)
18 		}
19 	}()
20 
21+	return vty, nil
22+}
23+
24+func NewSenpaiApp(sesh ssh.Session, username, pass string) (*senpai.App, error) {
25+	vty, err := NewVConsole(sesh)
26+	if err != nil {
27+		slog.Error("PTY not found")
28+		return nil, err
29+	}
30+
31 	senpaiCfg := senpai.Defaults()
32 	senpaiCfg.TLS = true
33 	senpaiCfg.Addr = "irc.pico.sh:6697"
tuivax/ui.go link
+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+}