main pico / pkg / shared / senpai.go
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)