"os"
"os/exec"
"os/signal"
"path/filepath"
"regexp"
"slices"
"strings"
"sync"
"syscall"
304 unmodified lines
Expand all
_ = os.Chmod(sockPath, 0700)
vt := xterm.New(xterm.WithCols(80), xterm.WithRows(24), xterm.WithScrollback(5000))
cmd := exec.Command(command[0], command[1:]...)
// sessions are interactive: drop CI so programs inside do not turn
// their color detection off, and give remote spawns (exec'd over ssh
// without a tty env) a usable TERM
env := lo.Reject(os.Environ(), func(kv string, _ int) bool {
return strings.HasPrefix(kv, "CI=")
})
if t := os.Getenv("TERM"); t == "" || t == "dumb" {
env = append(env, "TERM=xterm-256color", "COLORTERM=truecolor")
}
cmd.Env = append(os.Environ(), "TOBI_SESSION="+id, "TOBI_CONTROL="+sockPath)
cmd.Env = append(env, "TOBI_SESSION="+id, "TOBI_CONTROL="+sockPath)
ptyFile, err := pty.StartWithSize(cmd, &pty.Winsize{Cols: 80, Rows: 24})
if err != nil {
_ = listener.Close()
owner.sess.Command = title
}
})
vt.RegisterOscHandler(7, owner)
// the vt tracks no background color, so OSC 11 probes would go
// unanswered: answer dark ourselves, once, deterministically
vt.RegisterOscHandler(11, xterm.NewOscStringHandler(func(payload string) bool {
if payload == "?" {
_, _ = owner.ptyFile.Write([]byte("\x1b]11;rgb:0000/0000/0000\x1b\\"))
return true
}
return false
}))
// the vt answers the rest of the inner program's queries (DSR, DA);
// route those responses back into the pty
vt.OnData(func(s string) {
_, _ = owner.ptyFile.Write([]byte(s))
})
owner.idleTimer = time.AfterFunc(detachedTTL, owner.Close)
go owner.readPTYLoop()
go func() { _ = cmd.Wait() }()
111 unmodified lines
Expand all
o.mu.Unlock()
vc.Close()
}
// querySeq matches terminal-detection queries an inner program sends
// (OSC 10/11/12, DSR, DA1-3, DECRPM 2026, XTVERSION, kitty flags). the vt
// answers these itself; forwarding them would make every attached viewer's
// real terminal answer too, and the extra answers leak into the inner
// program's input
// oscQuery matches the fg/bg/cursor-color probes the vt cannot answer
// (it tracks neither), so readPTYLoop replies to those itself
var oscQuery = regexp.MustCompile(`\x1b\]1[012];\?(?:\x07|\x1b\\)`)
const oscBgAnswer = "\x1b]11;rgb:0000/0000/0000\x1b\\"
// querySeq matches terminal-detection queries an inner program sends
// (OSC 10/11/12, DSR, DA1-3, DECRPM 2026, XTVERSION, kitty flags). the vt
// answers them; forwarding them to viewers would make every attached
// viewer's real terminal answer too, and the extra answers leak into the
// inner program's input
var querySeq = regexp.MustCompile(
oscQuery.String() +
`|\x1b\[\?6n|\x1b\[6n` + // DSR / DECDSR
`|\x1b\[=0?c|\x1b\[>0?c|\x1b\[0?c` + // DA3 / DA2 / DA1
`|\x1b\[\?2026\$p` + // synchronized output supported?
`|\x1b\[>0?q` + // XTVERSION
`|\x1b\[\?u`) // kitty keyboard protocol query
func (o *Owner) readPTYLoop() {
buf := make([]byte, 4096)
for {
n, err := o.ptyFile.Read(buf)
if n > 0 {
chunk := slices.Clone(buf[:n])
raw := slices.Clone(buf[:n])
// the vt tracks no bg color: answer background probes ourselves,
// straight into the pty input of the inner program
if oscQuery.Match(raw) {
_, _ = o.ptyFile.Write([]byte(oscBgAnswer))
}
o.mu.Lock()
// the vt sees everything except the OSC probes it cannot answer
_, _ = o.vt.Write(oscQuery.ReplaceAll(raw, nil))
// viewers get everything except the queries; the vt's answers
// reach the inner program through the pty instead
_, _ = o.vt.Write(chunk)
for vc := range o.viewers {
select {
case vc.ch <- outMsg{t: protocol.TypeOutput, p: chunk}:
default:
vc.Close() // too slow to keep up: drop it
if chunk := querySeq.ReplaceAll(raw, nil); len(chunk) > 0 {
for vc := range o.viewers {
select {
case vc.ch <- outMsg{t: protocol.TypeOutput, p: chunk}:
default:
vc.Close() // too slow to keep up: drop it
}
}
}
o.mu.Unlock()
}