Skip to content

Instantly share code, notes, and snippets.

@yungwarlock
Last active March 11, 2025 15:46
Show Gist options
  • Save yungwarlock/730abfca7b23ad7853986d8ca229d111 to your computer and use it in GitHub Desktop.
Save yungwarlock/730abfca7b23ad7853986d8ca229d111 to your computer and use it in GitHub Desktop.
Interactive terminal UIs over SSH
package main
import (
"fmt"
"log"
"sync"
"errors"
"github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/terminfo"
"github.com/gliderlabs/ssh"
)
func drawText(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string) {
row := y1
col := x1
for _, r := range text {
s.SetContent(col, row, r, nil, style)
col++
if col >= x2 {
row++
col = x1
}
if row > y2 {
break
}
}
}
func drawBox(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string) {
if y2 < y1 {
y1, y2 = y2, y1
}
if x2 < x1 {
x1, x2 = x2, x1
}
// Fill background
for row := y1; row <= y2; row++ {
for col := x1; col <= x2; col++ {
s.SetContent(col, row, ' ', nil, style)
}
}
// Draw borders
for col := x1; col <= x2; col++ {
s.SetContent(col, y1, tcell.RuneHLine, nil, style)
s.SetContent(col, y2, tcell.RuneHLine, nil, style)
}
for row := y1 + 1; row < y2; row++ {
s.SetContent(x1, row, tcell.RuneVLine, nil, style)
s.SetContent(x2, row, tcell.RuneVLine, nil, style)
}
// Only draw corners if necessary
if y1 != y2 && x1 != x2 {
s.SetContent(x1, y1, tcell.RuneULCorner, nil, style)
s.SetContent(x2, y1, tcell.RuneURCorner, nil, style)
s.SetContent(x1, y2, tcell.RuneLLCorner, nil, style)
s.SetContent(x2, y2, tcell.RuneLRCorner, nil, style)
}
drawText(s, x1+1, y1+1, x2-1, y2-1, style, text)
}
func NewSessionScreen(s ssh.Session) (tcell.Screen, error) {
pi, ch, ok := s.Pty()
if !ok {
return nil, errors.New("no pty requested")
}
ti, err := terminfo.LookupTerminfo(pi.Term)
if err != nil {
return nil, err
}
screen, err := tcell.NewTerminfoScreenFromTtyTerminfo(&tty{
Session: s,
size: pi.Window,
ch: ch,
}, ti)
if err != nil {
return nil, err
}
return screen, nil
}
func main() {
ssh.Handle(func(sess ssh.Session) {
s, err := NewSessionScreen(sess)
if err != nil {
fmt.Fprintln(sess.Stderr(), "unable to create screen:", err)
return
}
defStyle := tcell.StyleDefault.Background(tcell.ColorReset).Foreground(tcell.ColorReset)
boxStyle := tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorPurple)
// Initialize screen
if err := s.Init(); err != nil {
log.Fatalf("%+v", err)
}
s.SetStyle(defStyle)
s.EnableMouse()
s.EnablePaste()
s.Clear()
// Draw initial boxes
drawBox(s, 1, 1, 42, 7, boxStyle, "Click and drag to draw a box")
drawBox(s, 5, 9, 32, 14, boxStyle, "Press C to reset")
quit := func() {
// You have to catch panics in a defer, clean up, and
// re-raise them - otherwise your application can
// die without leaving any diagnostic trace.
maybePanic := recover()
s.Fini()
if maybePanic != nil {
panic(maybePanic)
}
}
defer quit()
// Here's how to get the screen size when you need it.
// xmax, ymax := s.Size()
// Here's an example of how to inject a keystroke where it will
// be picked up by the next PollEvent call. Note that the
// queue is LIFO, it has a limited length, and PostEvent() can
// return an error.
// s.PostEvent(tcell.NewEventKey(tcell.KeyRune, rune('a'), 0))
// Event loop
ox, oy := -1, -1
for {
// Update screen
s.Show()
// Poll event
ev := s.PollEvent()
// Process event
switch ev := ev.(type) {
case *tcell.EventResize:
s.Sync()
case *tcell.EventKey:
if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC {
return
} else if ev.Key() == tcell.KeyCtrlL {
s.Sync()
} else if ev.Rune() == 'C' || ev.Rune() == 'c' {
s.Clear()
}
case *tcell.EventMouse:
x, y := ev.Position()
switch ev.Buttons() {
case tcell.Button1, tcell.Button2:
if ox < 0 {
ox, oy = x, y // record location when click started
}
case tcell.ButtonNone:
if ox >= 0 {
label := fmt.Sprintf("%d,%d to %d,%d", ox, oy, x, y)
drawBox(s, ox, oy, x, y, boxStyle, label)
ox, oy = -1, -1
}
}
}
}
})
log.Fatal(ssh.ListenAndServe(":2222", nil))
}
type tty struct {
ssh.Session
size ssh.Window
ch <-chan ssh.Window
resizecb func()
mu sync.Mutex
}
func (t *tty) Start() error {
go func() {
for win := range t.ch {
t.size = win
t.notifyResize()
}
}()
return nil
}
func (t *tty) Stop() error {
return nil
}
func (t *tty) Drain() error {
return nil
}
func (t *tty) WindowSize() (tcell.WindowSize, error) {
size := tcell.WindowSize{
Width: t.size.Width,
Height: t.size.Height,
}
return size, nil
}
func (t *tty) NotifyResize(cb func()) {
t.mu.Lock()
defer t.mu.Unlock()
t.resizecb = cb
}
func (t *tty) notifyResize() {
t.mu.Lock()
defer t.mu.Unlock()
if t.resizecb != nil {
t.resizecb()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment