Initial release: pure Go mosh client and server
Wire-compatible with C mosh. AES-128-OCB3, SSP transport, fragmentation, protobuf state sync, latch extension fields. Includes ServeRW bridge mode for embedding.
This commit is contained in:
+363
@@ -0,0 +1,363 @@
|
||||
package mosh
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Framebuffer represents a terminal screen state for mosh SSP.
|
||||
// The server captures this from the VT emulator; the diff between
|
||||
// two Framebuffers produces the ANSI escape sequences sent as HostBytes.
|
||||
type Framebuffer struct {
|
||||
W, H int
|
||||
Cells []Cell
|
||||
CurX int
|
||||
CurY int
|
||||
CurVis bool
|
||||
}
|
||||
|
||||
// Cell is a single character cell with SGR attributes.
|
||||
type Cell struct {
|
||||
Rune rune
|
||||
Width int // display width (0 for continuation cells)
|
||||
Attr Attr
|
||||
}
|
||||
|
||||
// Attr holds SGR attributes for a cell.
|
||||
type Attr struct {
|
||||
FG Color
|
||||
BG Color
|
||||
Bold bool
|
||||
Dim bool
|
||||
Italic bool
|
||||
Under bool
|
||||
Blink bool
|
||||
Reverse bool
|
||||
Strike bool
|
||||
}
|
||||
|
||||
// Color represents a terminal color.
|
||||
type Color struct {
|
||||
Type ColorType
|
||||
Value uint32 // index (0-255) or RGB packed as 0x00RRGGBB
|
||||
}
|
||||
|
||||
// ColorType identifies the color encoding.
|
||||
type ColorType byte
|
||||
|
||||
const (
|
||||
ColorDefault ColorType = iota
|
||||
ColorIndex // 0-7 normal, 8-15 bright, 16-255 extended
|
||||
ColorRGB
|
||||
)
|
||||
|
||||
// NewFramebuffer allocates a blank framebuffer.
|
||||
func NewFramebuffer(w, h int) *Framebuffer {
|
||||
fb := &Framebuffer{
|
||||
W: w,
|
||||
H: h,
|
||||
Cells: make([]Cell, w*h),
|
||||
CurVis: true,
|
||||
}
|
||||
for i := range fb.Cells {
|
||||
fb.Cells[i].Rune = ' '
|
||||
fb.Cells[i].Width = 1
|
||||
}
|
||||
return fb
|
||||
}
|
||||
|
||||
// CellAt returns a pointer to the cell at (x, y).
|
||||
func (fb *Framebuffer) CellAt(x, y int) *Cell {
|
||||
if x < 0 || x >= fb.W || y < 0 || y >= fb.H {
|
||||
return nil
|
||||
}
|
||||
return &fb.Cells[y*fb.W+x]
|
||||
}
|
||||
|
||||
// Diff produces the minimal ANSI escape sequence to transform old into fb.
|
||||
// This is what goes into HostBytes.hoststring in the mosh protocol.
|
||||
func (fb *Framebuffer) Diff(old *Framebuffer) []byte {
|
||||
var buf []byte
|
||||
|
||||
// If dimensions changed, send a full redraw.
|
||||
if old == nil || old.W != fb.W || old.H != fb.H {
|
||||
return fb.fullRedraw()
|
||||
}
|
||||
|
||||
// Hide cursor during update.
|
||||
buf = append(buf, "\033[?25l"...)
|
||||
|
||||
var curAttr Attr
|
||||
curX, curY := -1, -1
|
||||
|
||||
for y := 0; y < fb.H; y++ {
|
||||
rowOff := y * fb.W
|
||||
oldRowOff := y * old.W
|
||||
|
||||
// Find first and last changed column in this row.
|
||||
first, last := -1, -1
|
||||
for x := 0; x < fb.W; x++ {
|
||||
if fb.Cells[rowOff+x] != old.Cells[oldRowOff+x] {
|
||||
if first < 0 {
|
||||
first = x
|
||||
}
|
||||
last = x
|
||||
}
|
||||
}
|
||||
if first < 0 {
|
||||
continue // row unchanged
|
||||
}
|
||||
|
||||
// Move cursor if needed.
|
||||
if curX != first || curY != y {
|
||||
buf = appendCUP(buf, y, first)
|
||||
curX, curY = first, y
|
||||
}
|
||||
|
||||
// Write changed cells.
|
||||
for x := first; x <= last; x++ {
|
||||
c := &fb.Cells[rowOff+x]
|
||||
if c.Width == 0 {
|
||||
continue // continuation cell of a wide char
|
||||
}
|
||||
buf = appendAttrDiff(buf, &curAttr, &c.Attr)
|
||||
if c.Rune == 0 || c.Rune == ' ' {
|
||||
buf = append(buf, ' ')
|
||||
} else {
|
||||
buf = appendRune(buf, c.Rune)
|
||||
}
|
||||
curX += c.Width
|
||||
}
|
||||
}
|
||||
|
||||
// Reset attributes.
|
||||
if curAttr != (Attr{}) {
|
||||
buf = append(buf, "\033[m"...)
|
||||
}
|
||||
|
||||
// Reposition cursor and show it.
|
||||
buf = appendCUP(buf, fb.CurY, fb.CurX)
|
||||
if fb.CurVis {
|
||||
buf = append(buf, "\033[?25h"...)
|
||||
}
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
// fullRedraw produces ANSI to draw the entire screen from scratch.
|
||||
func (fb *Framebuffer) fullRedraw() []byte {
|
||||
var buf []byte
|
||||
buf = append(buf, "\033[?25l"...) // hide cursor
|
||||
buf = append(buf, "\033[H"...) // home
|
||||
buf = append(buf, "\033[2J"...) // clear screen
|
||||
buf = append(buf, "\033[m"...) // reset attrs
|
||||
|
||||
var curAttr Attr
|
||||
for y := 0; y < fb.H; y++ {
|
||||
if y > 0 {
|
||||
buf = append(buf, "\r\n"...)
|
||||
}
|
||||
rowOff := y * fb.W
|
||||
// Track trailing spaces to avoid sending them.
|
||||
lastNonSpace := -1
|
||||
for x := fb.W - 1; x >= 0; x-- {
|
||||
c := &fb.Cells[rowOff+x]
|
||||
if c.Rune != ' ' || c.Rune == 0 || c.Attr != (Attr{}) {
|
||||
lastNonSpace = x
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for x := 0; x <= lastNonSpace; x++ {
|
||||
c := &fb.Cells[rowOff+x]
|
||||
if c.Width == 0 {
|
||||
continue
|
||||
}
|
||||
buf = appendAttrDiff(buf, &curAttr, &c.Attr)
|
||||
if c.Rune == 0 || c.Rune == ' ' {
|
||||
buf = append(buf, ' ')
|
||||
} else {
|
||||
buf = appendRune(buf, c.Rune)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if curAttr != (Attr{}) {
|
||||
buf = append(buf, "\033[m"...)
|
||||
}
|
||||
buf = appendCUP(buf, fb.CurY, fb.CurX)
|
||||
if fb.CurVis {
|
||||
buf = append(buf, "\033[?25h"...)
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
// appendCUP appends a cursor position escape (1-indexed).
|
||||
func appendCUP(buf []byte, row, col int) []byte {
|
||||
buf = append(buf, "\033["...)
|
||||
buf = strconv.AppendInt(buf, int64(row+1), 10)
|
||||
buf = append(buf, ';')
|
||||
buf = strconv.AppendInt(buf, int64(col+1), 10)
|
||||
buf = append(buf, 'H')
|
||||
return buf
|
||||
}
|
||||
|
||||
// appendAttrDiff appends SGR sequences to transition from cur to next.
|
||||
func appendAttrDiff(buf []byte, cur, next *Attr) []byte {
|
||||
if *cur == *next {
|
||||
return buf
|
||||
}
|
||||
|
||||
// If attributes are being removed, reset first.
|
||||
needsReset := (cur.Bold && !next.Bold) ||
|
||||
(cur.Dim && !next.Dim) ||
|
||||
(cur.Italic && !next.Italic) ||
|
||||
(cur.Under && !next.Under) ||
|
||||
(cur.Blink && !next.Blink) ||
|
||||
(cur.Reverse && !next.Reverse) ||
|
||||
(cur.Strike && !next.Strike) ||
|
||||
(cur.FG.Type != ColorDefault && next.FG.Type == ColorDefault) ||
|
||||
(cur.BG.Type != ColorDefault && next.BG.Type == ColorDefault)
|
||||
|
||||
if needsReset {
|
||||
buf = append(buf, "\033[0"...)
|
||||
*cur = Attr{}
|
||||
} else {
|
||||
buf = append(buf, "\033["...)
|
||||
}
|
||||
|
||||
sep := byte(';')
|
||||
first := !needsReset
|
||||
|
||||
addParam := func(code string) {
|
||||
if !first {
|
||||
buf = append(buf, sep)
|
||||
}
|
||||
first = false
|
||||
buf = append(buf, code...)
|
||||
}
|
||||
|
||||
if next.Bold && !cur.Bold {
|
||||
addParam("1")
|
||||
}
|
||||
if next.Dim && !cur.Dim {
|
||||
addParam("2")
|
||||
}
|
||||
if next.Italic && !cur.Italic {
|
||||
addParam("3")
|
||||
}
|
||||
if next.Under && !cur.Under {
|
||||
addParam("4")
|
||||
}
|
||||
if next.Blink && !cur.Blink {
|
||||
addParam("5")
|
||||
}
|
||||
if next.Reverse && !cur.Reverse {
|
||||
addParam("7")
|
||||
}
|
||||
if next.Strike && !cur.Strike {
|
||||
addParam("9")
|
||||
}
|
||||
|
||||
if next.FG != cur.FG {
|
||||
buf = appendColor(buf, next.FG, true, &first, sep)
|
||||
}
|
||||
if next.BG != cur.BG {
|
||||
buf = appendColor(buf, next.BG, false, &first, sep)
|
||||
}
|
||||
|
||||
buf = append(buf, 'm')
|
||||
*cur = *next
|
||||
return buf
|
||||
}
|
||||
|
||||
// appendColor appends SGR parameters for a color.
|
||||
func appendColor(buf []byte, c Color, fg bool, first *bool, sep byte) []byte {
|
||||
addSep := func() {
|
||||
if !*first {
|
||||
buf = append(buf, sep)
|
||||
}
|
||||
*first = false
|
||||
}
|
||||
|
||||
switch c.Type {
|
||||
case ColorDefault:
|
||||
addSep()
|
||||
if fg {
|
||||
buf = append(buf, "39"...)
|
||||
} else {
|
||||
buf = append(buf, "49"...)
|
||||
}
|
||||
case ColorIndex:
|
||||
idx := c.Value
|
||||
if idx < 8 {
|
||||
addSep()
|
||||
if fg {
|
||||
buf = strconv.AppendInt(buf, int64(30+idx), 10)
|
||||
} else {
|
||||
buf = strconv.AppendInt(buf, int64(40+idx), 10)
|
||||
}
|
||||
} else if idx < 16 {
|
||||
addSep()
|
||||
if fg {
|
||||
buf = strconv.AppendInt(buf, int64(90+idx-8), 10)
|
||||
} else {
|
||||
buf = strconv.AppendInt(buf, int64(100+idx-8), 10)
|
||||
}
|
||||
} else {
|
||||
addSep()
|
||||
if fg {
|
||||
buf = append(buf, "38;5;"...)
|
||||
} else {
|
||||
buf = append(buf, "48;5;"...)
|
||||
}
|
||||
buf = strconv.AppendInt(buf, int64(idx), 10)
|
||||
}
|
||||
case ColorRGB:
|
||||
addSep()
|
||||
r := (c.Value >> 16) & 0xff
|
||||
g := (c.Value >> 8) & 0xff
|
||||
b := c.Value & 0xff
|
||||
if fg {
|
||||
buf = append(buf, "38;2;"...)
|
||||
} else {
|
||||
buf = append(buf, "48;2;"...)
|
||||
}
|
||||
buf = strconv.AppendInt(buf, int64(r), 10)
|
||||
buf = append(buf, ';')
|
||||
buf = strconv.AppendInt(buf, int64(g), 10)
|
||||
buf = append(buf, ';')
|
||||
buf = strconv.AppendInt(buf, int64(b), 10)
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
// appendRune appends a rune as UTF-8.
|
||||
func appendRune(buf []byte, r rune) []byte {
|
||||
var tmp [4]byte
|
||||
n := encodeRune(tmp[:], r)
|
||||
return append(buf, tmp[:n]...)
|
||||
}
|
||||
|
||||
// encodeRune is a minimal UTF-8 encoder avoiding the unicode/utf8 import.
|
||||
func encodeRune(buf []byte, r rune) int {
|
||||
switch {
|
||||
case r < 0x80:
|
||||
buf[0] = byte(r)
|
||||
return 1
|
||||
case r < 0x800:
|
||||
buf[0] = byte(0xc0 | (r >> 6))
|
||||
buf[1] = byte(0x80 | (r & 0x3f))
|
||||
return 2
|
||||
case r < 0x10000:
|
||||
buf[0] = byte(0xe0 | (r >> 12))
|
||||
buf[1] = byte(0x80 | ((r >> 6) & 0x3f))
|
||||
buf[2] = byte(0x80 | (r & 0x3f))
|
||||
return 3
|
||||
default:
|
||||
buf[0] = byte(0xf0 | (r >> 18))
|
||||
buf[1] = byte(0x80 | ((r >> 12) & 0x3f))
|
||||
buf[2] = byte(0x80 | ((r >> 6) & 0x3f))
|
||||
buf[3] = byte(0x80 | (r & 0x3f))
|
||||
return 4
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user