Antonio Mika
·
2026-10-02
1package shared
2
3import (
4 "fmt"
5 "log/slog"
6 "sync"
7
8 "git.sr.ht/~delthas/senpai"
9 "github.com/containerd/console"
10 "github.com/picosh/pico/pkg/pssh"
11 "go.rockorager.dev/vaxis"
12)
13
14type consoleData struct {
15 data []byte
16 err error
17}
18
19type VConsole struct {
20 Session *pssh.SSHServerConnSession
21 pty *pssh.Pty
22
23 sizeEnableOnce sync.Once
24
25 windowMu sync.Mutex
26 currentWindow pssh.Window
27
28 readReq chan []byte
29 dataChan chan consoleData
30
31 wg sync.WaitGroup
32}
33
34func (v *VConsole) Read(p []byte) (int, error) {
35 v.wg.Add(1)
36 defer v.wg.Done()
37 tot := 0
38
39 if len(v.readReq) == 0 {
40 select {
41 case v.readReq <- make([]byte, len(p)):
42 case <-v.Session.Context().Done():
43 return 0, v.Session.Context().Err()
44 }
45 }
46
47 v.sizeEnableOnce.Do(func() {
48 tot += copy(p, []byte("\x1b[?2048h"))
49 v.windowMu.Lock()
50 select {
51 case v.dataChan <- consoleData{fmt.Appendf(nil, "\x1b[48;%d;%d;%d;%dt", v.currentWindow.Height, v.currentWindow.Width, v.currentWindow.HeightPixels, v.currentWindow.WidthPixels), nil}:
52 case <-v.Session.Context().Done():
53 return
54 }
55 v.windowMu.Unlock()
56 select {
57 case v.dataChan <- consoleData{[]byte("\x1b[?1;0c"), nil}:
58 case <-v.Session.Context().Done():
59 return
60 }
61 })
62
63 if tot > 0 {
64 return tot, nil
65 }
66
67 select {
68 case data := <-v.dataChan:
69 tot += copy(p, data.data)
70 return tot, data.err
71 case <-v.Session.Context().Done():
72 return 0, v.Session.Context().Err()
73 }
74}
75
76func (v *VConsole) Resize(winSize console.WinSize) error {
77 return v.pty.Resize(int(winSize.Height), int(winSize.Width))
78}
79
80func (v *VConsole) ResizeFrom(c console.Console) error {
81 s, err := c.Size()
82 if err != nil {
83 return err
84 }
85
86 return v.Resize(s)
87}
88
89func (v *VConsole) SetRaw() error {
90 return nil
91}
92
93func (v *VConsole) DisableEcho() error {
94 return nil
95}
96
97func (v *VConsole) Reset() error {
98 _, err := v.Session.Write([]byte("\033[?25h\033[0 q\033[34h\033[?25h\033[39;49m\033[m^O\033[H\033[J\033[?1049l\033[?1l\033>\033[?1000l\033[?1002l\033[?1003l\033[?1006l\033[?2004l"))
99 return err
100}
101
102func (v *VConsole) Size() (int, int, int, int, error) {
103 v.windowMu.Lock()
104 defer v.windowMu.Unlock()
105 return v.currentWindow.Height, v.currentWindow.Width, v.currentWindow.HeightPixels, v.currentWindow.WidthPixels, nil
106}
107
108func (v *VConsole) Fd() uintptr {
109 return 0
110}
111
112func (v *VConsole) Name() string {
113 return v.pty.Name()
114}
115
116func (v *VConsole) Close() error {
117 err := v.Session.Close()
118 v.wg.Wait()
119 close(v.readReq)
120 close(v.dataChan)
121 return err
122}
123
124func (v *VConsole) Write(p []byte) (int, error) {
125 return v.Session.Write(p)
126}
127
128func NewVConsole(sesh *pssh.SSHServerConnSession) (*VConsole, error) {
129 pty, win, ok := sesh.Pty()
130 if !ok {
131 return nil, fmt.Errorf("PTY not found")
132 }
133
134 vty := &VConsole{
135 pty: pty,
136 Session: sesh,
137 currentWindow: pty.Window,
138 readReq: make(chan []byte, 100),
139 dataChan: make(chan consoleData, 100),
140 }
141
142 vty.wg.Add(2)
143
144 go func() {
145 defer vty.wg.Done()
146 for {
147 select {
148 case <-sesh.Context().Done():
149 return
150 case data := <-vty.readReq:
151 n, err := vty.Session.Read(data)
152 select {
153 case vty.dataChan <- consoleData{data[:n], err}:
154 case <-sesh.Context().Done():
155 return
156 }
157 }
158 }
159 }()
160
161 go func() {
162 defer vty.wg.Done()
163 for {
164 select {
165 case <-sesh.Context().Done():
166 return
167 case w := <-win:
168 vty.windowMu.Lock()
169 vty.currentWindow = w
170 vty.windowMu.Unlock()
171 select {
172 case vty.dataChan <- consoleData{fmt.Appendf(nil, "\x1b[48;%d;%d;%d;%dt", w.Height, w.Width, w.HeightPixels, w.WidthPixels), nil}:
173 case <-sesh.Context().Done():
174 return
175 }
176 }
177 }
178 }()
179
180 return vty, nil
181}
182
183func NewSenpaiApp(sesh *pssh.SSHServerConnSession, username, pass string) (*senpai.App, error) {
184 vty, err := NewVConsole(sesh)
185 if err != nil {
186 slog.Error("PTY not found")
187 return nil, err
188 }
189
190 senpaiCfg := senpai.Defaults()
191 senpaiCfg.TLS = true
192 senpaiCfg.Addr = "irc.pico.sh:6697"
193 senpaiCfg.Nick = username
194 senpaiCfg.Password = &pass
195 senpaiCfg.WithConsole = vty
196
197 app, err := senpai.NewApp(senpaiCfg)
198 return app, err
199}
200
201var _ vaxis.Console = (*VConsole)(nil)