1package ssh
2
3import (
4 "os"
5 "time"
6
7 tea "github.com/charmbracelet/bubbletea/v2"
8 "github.com/charmbracelet/colorprofile"
9 "github.com/charmbracelet/soft-serve/pkg/access"
10 "github.com/charmbracelet/soft-serve/pkg/backend"
11 "github.com/charmbracelet/soft-serve/pkg/config"
12 "github.com/charmbracelet/soft-serve/pkg/proto"
13 "github.com/charmbracelet/soft-serve/pkg/ui/common"
14 "github.com/charmbracelet/ssh"
15 "github.com/charmbracelet/wish/v2"
16 bm "github.com/charmbracelet/wish/v2/bubbletea"
17 "github.com/prometheus/client_golang/prometheus"
18 "github.com/prometheus/client_golang/prometheus/promauto"
19)
20
21var tuiSessionCounter = promauto.NewCounterVec(prometheus.CounterOpts{
22 Namespace: "soft_serve",
23 Subsystem: "ssh",
24 Name: "tui_session_total",
25 Help: "The total number of TUI sessions",
26}, []string{"repo", "term"})
27
28var tuiSessionDuration = promauto.NewCounterVec(prometheus.CounterOpts{
29 Namespace: "soft_serve",
30 Subsystem: "ssh",
31 Name: "tui_session_seconds_total",
32 Help: "The total time spent in TUI sessions",
33}, []string{"repo", "term"})
34
35// SessionHandler is the soft-serve bubbletea ssh session handler.
36// This middleware must be run after the ContextMiddleware.
37func SessionHandler(s ssh.Session) *tea.Program {
38 pty, _, active := s.Pty()
39 if !active {
40 return nil
41 }
42
43 ctx := s.Context()
44 be := backend.FromContext(ctx)
45 cfg := config.FromContext(ctx)
46 cmd := s.Command()
47 initialRepo := ""
48 if len(cmd) == 1 {
49 initialRepo = cmd[0]
50 auth := be.AccessLevelByPublicKey(ctx, initialRepo, s.PublicKey())
51 if auth < access.ReadOnlyAccess {
52 wish.Fatalln(s, proto.ErrUnauthorized)
53 return nil
54 }
55 }
56
57 opts := bm.MakeOptions(s)
58 opts = append(opts,
59 tea.WithAltScreen(),
60 tea.WithoutCatchPanics(),
61 tea.WithMouseCellMotion(),
62 tea.WithContext(ctx),
63 )
64
65 if testrun, ok := os.LookupEnv("SOFT_SERVE_NO_COLOR"); ok && testrun == "1" {
66 // Disable colors when running tests.
67 opts = append(opts, tea.WithColorProfile(colorprofile.NoTTY))
68 }
69
70 c := common.NewCommon(ctx, pty.Window.Width, pty.Window.Height)
71 c.SetValue(common.ConfigKey, cfg)
72 m := NewUI(c, initialRepo)
73 p := tea.NewProgram(m, opts...)
74
75 tuiSessionCounter.WithLabelValues(initialRepo, pty.Term).Inc()
76
77 start := time.Now()
78 go func() {
79 <-ctx.Done()
80 tuiSessionDuration.WithLabelValues(initialRepo, pty.Term).Add(time.Since(start).Seconds())
81 }()
82
83 return p
84}