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-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
 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()
+	}
+}