├── go.mod ├── .travis.yml ├── ioctl_other.go ├── doc.go ├── README.md ├── ioctl_posix.go ├── color.go ├── csi_test.go ├── LICENSE ├── vt_test.go ├── vt.go ├── vt_other.go ├── vt_posix.go ├── str_test.go ├── parse.go ├── csi.go ├── str.go └── state.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hinshun/vt10x 2 | 3 | go 1.14 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.10.2" 5 | - master 6 | -------------------------------------------------------------------------------- /ioctl_other.go: -------------------------------------------------------------------------------- 1 | // +build plan9 nacl windows 2 | 3 | package vt10x 4 | 5 | import ( 6 | "os" 7 | ) 8 | 9 | func ioctl(f *os.File, cmd, p uintptr) error { 10 | return nil 11 | } 12 | 13 | func ResizePty(*os.File) error { 14 | return nil 15 | } 16 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package terminal is a vt10x terminal emulation backend, influenced 3 | largely by st, rxvt, xterm, and iTerm as reference. Use it for terminal 4 | muxing, a terminal emulation frontend, or wherever else you need 5 | terminal emulation. 6 | 7 | In development, but very usable. 8 | */ 9 | package vt10x 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vt10x 2 | 3 | [![Build Status](https://travis-ci.org/hinshun/vt10x.svg?branch=master)](https://travis-ci.org/hinshun/vt10x) 4 | [![GoDoc](https://godoc.org/github.com/hinshun/vt10x?status.svg)](https://godoc.org/github.com/hinshun/vt10x) 5 | 6 | Package vt10x is a vt10x terminal emulation backend, influenced 7 | largely by st, rxvt, xterm, and iTerm as reference. Use it for terminal 8 | muxing, a terminal emulation frontend, or wherever else you need 9 | terminal emulation. 10 | -------------------------------------------------------------------------------- /ioctl_posix.go: -------------------------------------------------------------------------------- 1 | // +build linux darwin dragonfly solaris openbsd netbsd freebsd 2 | 3 | package vt10x 4 | 5 | import ( 6 | "os" 7 | "syscall" 8 | "unsafe" 9 | ) 10 | 11 | func ioctl(f *os.File, cmd, p uintptr) error { 12 | _, _, errno := syscall.Syscall( 13 | syscall.SYS_IOCTL, 14 | f.Fd(), 15 | syscall.TIOCSWINSZ, 16 | p) 17 | if errno != 0 { 18 | return syscall.Errno(errno) 19 | } 20 | return nil 21 | } 22 | 23 | func ResizePty(pty *os.File, cols, rows int) error { 24 | var w struct{ row, col, xpix, ypix uint16 } 25 | w.row = uint16(rows) 26 | w.col = uint16(cols) 27 | w.xpix = 16 * uint16(cols) 28 | w.ypix = 16 * uint16(rows) 29 | return ioctl(pty, syscall.TIOCSWINSZ, 30 | uintptr(unsafe.Pointer(&w))) 31 | } 32 | -------------------------------------------------------------------------------- /color.go: -------------------------------------------------------------------------------- 1 | package vt10x 2 | 3 | // ANSI color values 4 | const ( 5 | Black Color = iota 6 | Red 7 | Green 8 | Yellow 9 | Blue 10 | Magenta 11 | Cyan 12 | LightGrey 13 | DarkGrey 14 | LightRed 15 | LightGreen 16 | LightYellow 17 | LightBlue 18 | LightMagenta 19 | LightCyan 20 | White 21 | ) 22 | 23 | // Default colors are potentially distinct to allow for special behavior. 24 | // For example, a transparent background. Otherwise, the simple case is to 25 | // map default colors to another color. 26 | const ( 27 | DefaultFG Color = 1<<24 + iota 28 | DefaultBG 29 | DefaultCursor 30 | ) 31 | 32 | // Color maps to the ANSI colors [0, 16) and the xterm colors [16, 256). 33 | type Color uint32 34 | 35 | // ANSI returns true if Color is within [0, 16). 36 | func (c Color) ANSI() bool { 37 | return (c < 16) 38 | } 39 | -------------------------------------------------------------------------------- /csi_test.go: -------------------------------------------------------------------------------- 1 | package vt10x 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestCSIParse(t *testing.T) { 8 | var csi csiEscape 9 | csi.reset() 10 | csi.buf = []byte("s") 11 | csi.parse() 12 | if csi.mode != 's' || csi.arg(0, 17) != 17 || len(csi.args) != 0 { 13 | t.Fatal("CSI parse mismatch") 14 | } 15 | 16 | csi.reset() 17 | csi.buf = []byte("31T") 18 | csi.parse() 19 | if csi.mode != 'T' || csi.arg(0, 0) != 31 || len(csi.args) != 1 { 20 | t.Fatal("CSI parse mismatch") 21 | } 22 | 23 | csi.reset() 24 | csi.buf = []byte("48;2f") 25 | csi.parse() 26 | if csi.mode != 'f' || csi.arg(0, 0) != 48 || csi.arg(1, 0) != 2 || len(csi.args) != 2 { 27 | t.Fatal("CSI parse mismatch") 28 | } 29 | 30 | csi.reset() 31 | csi.buf = []byte("?25l") 32 | csi.parse() 33 | if csi.mode != 'l' || csi.arg(0, 0) != 25 || csi.priv != true || len(csi.args) != 1 { 34 | t.Fatal("CSI parse mismatch") 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013 James Gray 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without liitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and thismssion notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /vt_test.go: -------------------------------------------------------------------------------- 1 | package vt10x 2 | 3 | import ( 4 | "io" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func extractStr(term Terminal, x0, x1, row int) string { 10 | var s []rune 11 | for i := x0; i <= x1; i++ { 12 | attr := term.Cell(i, row) 13 | s = append(s, attr.Char) 14 | } 15 | return string(s) 16 | } 17 | 18 | func TestPlainChars(t *testing.T) { 19 | term := New() 20 | expected := "Hello world!" 21 | _, err := term.Write([]byte(expected)) 22 | if err != nil && err != io.EOF { 23 | t.Fatal(err) 24 | } 25 | actual := extractStr(term, 0, len(expected)-1, 0) 26 | if expected != actual { 27 | t.Fatal(actual) 28 | } 29 | } 30 | 31 | func TestNewline(t *testing.T) { 32 | term := New() 33 | expected := "Hello world!\n...and more." 34 | _, err := term.Write([]byte("\033[20h")) // set CRLF mode 35 | if err != nil && err != io.EOF { 36 | t.Fatal(err) 37 | } 38 | _, err = term.Write([]byte(expected)) 39 | if err != nil && err != io.EOF { 40 | t.Fatal(err) 41 | } 42 | 43 | split := strings.Split(expected, "\n") 44 | actual := extractStr(term, 0, len(split[0])-1, 0) 45 | actual += "\n" 46 | actual += extractStr(term, 0, len(split[1])-1, 1) 47 | if expected != actual { 48 | t.Fatal(actual) 49 | } 50 | 51 | // A newline with a color set should not make the next line that color, 52 | // which used to happen if it caused a scroll event. 53 | st := (term.(*terminal)) 54 | st.moveTo(0, st.rows-1) 55 | _, err = term.Write([]byte("\033[1;37m\n$ \033[m")) 56 | if err != nil && err != io.EOF { 57 | t.Fatal(err) 58 | } 59 | cur := term.Cursor() 60 | attr := term.Cell(cur.X, cur.Y) 61 | if attr.FG != DefaultFG { 62 | t.Fatal(st.cur.X, st.cur.Y, attr.FG, attr.BG) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /vt.go: -------------------------------------------------------------------------------- 1 | package vt10x 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | ) 9 | 10 | // Terminal represents the virtual terminal emulator. 11 | type Terminal interface { 12 | // View displays the virtual terminal. 13 | View 14 | 15 | // Write parses input and writes terminal changes to state. 16 | io.Writer 17 | 18 | // Parse blocks on read on pty or io.Reader, then parses sequences until 19 | // buffer empties. State is locked as soon as first rune is read, and unlocked 20 | // when buffer is empty. 21 | Parse(bf *bufio.Reader) error 22 | } 23 | 24 | // View represents the view of the virtual terminal emulator. 25 | type View interface { 26 | // String dumps the virtual terminal contents. 27 | fmt.Stringer 28 | 29 | // Size returns the size of the virtual terminal. 30 | Size() (cols, rows int) 31 | 32 | // Resize changes the size of the virtual terminal. 33 | Resize(cols, rows int) 34 | 35 | // Mode returns the current terminal mode.// 36 | Mode() ModeFlag 37 | 38 | // Title represents the title of the console window. 39 | Title() string 40 | 41 | // Cell returns the glyph containing the character code, foreground color, and 42 | // background color at position (x, y) relative to the top left of the terminal. 43 | Cell(x, y int) Glyph 44 | 45 | // Cursor returns the current position of the cursor. 46 | Cursor() Cursor 47 | 48 | // CursorVisible returns the visible state of the cursor. 49 | CursorVisible() bool 50 | 51 | // Lock locks the state object's mutex. 52 | Lock() 53 | 54 | // Unlock resets change flags and unlocks the state object's mutex. 55 | Unlock() 56 | } 57 | 58 | type TerminalOption func(*TerminalInfo) 59 | 60 | type TerminalInfo struct { 61 | w io.Writer 62 | cols, rows int 63 | } 64 | 65 | func WithWriter(w io.Writer) TerminalOption { 66 | return func(info *TerminalInfo) { 67 | info.w = w 68 | } 69 | } 70 | 71 | func WithSize(cols, rows int) TerminalOption { 72 | return func(info *TerminalInfo) { 73 | info.cols = cols 74 | info.rows = rows 75 | } 76 | } 77 | 78 | // New returns a new virtual terminal emulator. 79 | func New(opts ...TerminalOption) Terminal { 80 | info := TerminalInfo{ 81 | w: ioutil.Discard, 82 | cols: 80, 83 | rows: 24, 84 | } 85 | for _, opt := range opts { 86 | opt(&info) 87 | } 88 | return newTerminal(info) 89 | } 90 | -------------------------------------------------------------------------------- /vt_other.go: -------------------------------------------------------------------------------- 1 | // +build plan9 nacl windows 2 | 3 | package vt10x 4 | 5 | import ( 6 | "bufio" 7 | "bytes" 8 | "io" 9 | "unicode" 10 | "unicode/utf8" 11 | ) 12 | 13 | type terminal struct { 14 | *State 15 | } 16 | 17 | func newTerminal(info TerminalInfo) *terminal { 18 | t := &terminal{newState(info.w)} 19 | t.init(info.cols, info.rows) 20 | return t 21 | } 22 | 23 | func (t *terminal) init(cols, rows int) { 24 | t.numlock = true 25 | t.state = t.parse 26 | t.cur.Attr.FG = DefaultFG 27 | t.cur.Attr.BG = DefaultBG 28 | t.Resize(cols, rows) 29 | t.reset() 30 | } 31 | 32 | func (t *terminal) Write(p []byte) (int, error) { 33 | var written int 34 | r := bytes.NewReader(p) 35 | t.lock() 36 | defer t.unlock() 37 | for { 38 | c, sz, err := r.ReadRune() 39 | if err != nil { 40 | if err == io.EOF { 41 | break 42 | } 43 | return written, err 44 | } 45 | written += sz 46 | if c == unicode.ReplacementChar && sz == 1 { 47 | if r.Len() == 0 { 48 | // not enough bytes for a full rune 49 | return written - 1, nil 50 | } 51 | t.logln("invalid utf8 sequence") 52 | continue 53 | } 54 | t.put(c) 55 | } 56 | return written, nil 57 | } 58 | 59 | // TODO: add tests for expected blocking behavior 60 | func (t *terminal) Parse(br *bufio.Reader) error { 61 | var locked bool 62 | defer func() { 63 | if locked { 64 | t.unlock() 65 | } 66 | }() 67 | for { 68 | c, sz, err := br.ReadRune() 69 | if err != nil { 70 | return err 71 | } 72 | if c == unicode.ReplacementChar && sz == 1 { 73 | t.logln("invalid utf8 sequence") 74 | break 75 | } 76 | if !locked { 77 | t.lock() 78 | locked = true 79 | } 80 | 81 | // put rune for parsing and update state 82 | t.put(c) 83 | 84 | // break if our buffer is empty, or if buffer contains an 85 | // incomplete rune. 86 | n := br.Buffered() 87 | if n == 0 || (n < 4 && !fullRuneBuffered(br)) { 88 | break 89 | } 90 | } 91 | return nil 92 | } 93 | 94 | func fullRuneBuffered(br *bufio.Reader) bool { 95 | n := br.Buffered() 96 | buf, err := br.Peek(n) 97 | if err != nil { 98 | return false 99 | } 100 | return utf8.FullRune(buf) 101 | } 102 | 103 | func (t *terminal) Resize(cols, rows int) { 104 | t.lock() 105 | defer t.unlock() 106 | _ = t.resize(cols, rows) 107 | } 108 | -------------------------------------------------------------------------------- /vt_posix.go: -------------------------------------------------------------------------------- 1 | // +build linux darwin dragonfly solaris openbsd netbsd freebsd 2 | 3 | package vt10x 4 | 5 | import ( 6 | "bufio" 7 | "bytes" 8 | "io" 9 | "unicode" 10 | "unicode/utf8" 11 | ) 12 | 13 | type terminal struct { 14 | *State 15 | } 16 | 17 | func newTerminal(info TerminalInfo) *terminal { 18 | t := &terminal{newState(info.w)} 19 | t.init(info.cols, info.rows) 20 | return t 21 | } 22 | 23 | func (t *terminal) init(cols, rows int) { 24 | t.numlock = true 25 | t.state = t.parse 26 | t.cur.Attr.FG = DefaultFG 27 | t.cur.Attr.BG = DefaultBG 28 | t.Resize(cols, rows) 29 | t.reset() 30 | } 31 | 32 | // Write parses input and writes terminal changes to state. 33 | func (t *terminal) Write(p []byte) (int, error) { 34 | var written int 35 | r := bytes.NewReader(p) 36 | t.lock() 37 | defer t.unlock() 38 | for { 39 | c, sz, err := r.ReadRune() 40 | if err != nil { 41 | if err == io.EOF { 42 | break 43 | } 44 | return written, err 45 | } 46 | written += sz 47 | if c == unicode.ReplacementChar && sz == 1 { 48 | if r.Len() == 0 { 49 | // not enough bytes for a full rune 50 | return written - 1, nil 51 | } 52 | t.logln("invalid utf8 sequence") 53 | continue 54 | } 55 | t.put(c) 56 | } 57 | return written, nil 58 | } 59 | 60 | // TODO: add tests for expected blocking behavior 61 | func (t *terminal) Parse(br *bufio.Reader) error { 62 | var locked bool 63 | defer func() { 64 | if locked { 65 | t.unlock() 66 | } 67 | }() 68 | for { 69 | c, sz, err := br.ReadRune() 70 | if err != nil { 71 | return err 72 | } 73 | if c == unicode.ReplacementChar && sz == 1 { 74 | t.logln("invalid utf8 sequence") 75 | break 76 | } 77 | if !locked { 78 | t.lock() 79 | locked = true 80 | } 81 | 82 | // put rune for parsing and update state 83 | t.put(c) 84 | 85 | // break if our buffer is empty, or if buffer contains an 86 | // incomplete rune. 87 | n := br.Buffered() 88 | if n == 0 || (n < 4 && !fullRuneBuffered(br)) { 89 | break 90 | } 91 | } 92 | return nil 93 | } 94 | 95 | func fullRuneBuffered(br *bufio.Reader) bool { 96 | n := br.Buffered() 97 | buf, err := br.Peek(n) 98 | if err != nil { 99 | return false 100 | } 101 | return utf8.FullRune(buf) 102 | } 103 | 104 | func (t *terminal) Resize(cols, rows int) { 105 | t.lock() 106 | defer t.unlock() 107 | _ = t.resize(cols, rows) 108 | } 109 | -------------------------------------------------------------------------------- /str_test.go: -------------------------------------------------------------------------------- 1 | package vt10x 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSTRParse(t *testing.T) { 8 | var str strEscape 9 | str.reset() 10 | str.buf = []rune("0;some text") 11 | str.parse() 12 | if str.arg(0, 17) != 0 || str.argString(1, "") != "some text" { 13 | t.Fatal("STR parse mismatch") 14 | } 15 | } 16 | 17 | func TestParseColor(t *testing.T) { 18 | type testCase struct { 19 | name string 20 | input string 21 | r, g, b int 22 | } 23 | 24 | for _, tc := range []testCase{ 25 | { 26 | "rgb 4 bit zero", 27 | "rgb:0/0/0", 28 | 0, 0, 0, 29 | }, 30 | { 31 | "rgb 4 bit max", 32 | "rgb:f/f/f", 33 | 255, 255, 255, 34 | }, 35 | { 36 | "rgb 4 bit values", 37 | "rgb:1/2/3", 38 | 17, 34, 51, 39 | }, 40 | { 41 | "rgb 8 bit zero", 42 | "rgb:00/00/00", 43 | 0, 0, 0, 44 | }, 45 | { 46 | "rgb 8 bit max", 47 | "rgb:ff/ff/ff", 48 | 255, 255, 255, 49 | }, 50 | { 51 | "rgb 8 bit values", 52 | "rgb:11/22/33", 53 | 17, 34, 51, 54 | }, 55 | { 56 | "rgb 12 bit zero", 57 | "rgb:000/000/000", 58 | 0, 0, 0, 59 | }, 60 | { 61 | "rgb 12 bit max", 62 | "rgb:fff/fff/fff", 63 | 255, 255, 255, 64 | }, 65 | { 66 | "rgb 12 bit values", 67 | "rgb:111/222/333", 68 | 17, 34, 51, 69 | }, 70 | { 71 | "rgb 16 bit zero", 72 | "rgb:0000/0000/0000", 73 | 0, 0, 0, 74 | }, 75 | { 76 | "rgb 16 bit max", 77 | "rgb:ffff/ffff/ffff", 78 | 255, 255, 255, 79 | }, 80 | { 81 | "rgb 16 bit values", 82 | "rgb:1111/2222/3333", 83 | 17, 34, 51, 84 | }, 85 | { 86 | "rgb 16 bit values", 87 | "rgb:1111/2222/3333", 88 | 17, 34, 51, 89 | }, 90 | { 91 | "hash 4 bit zero", 92 | "#000", 93 | 0, 0, 0, 94 | }, 95 | { 96 | "hash 4 bit max", 97 | "#fff", 98 | 240, 240, 240, 99 | }, 100 | { 101 | "hash 4 bit values", 102 | "#123", 103 | 16, 32, 48, 104 | }, 105 | { 106 | "hash 8 bit zero", 107 | "#000000", 108 | 0, 0, 0, 109 | }, 110 | { 111 | "hash 8 bit max", 112 | "#ffffff", 113 | 255, 255, 255, 114 | }, 115 | { 116 | "hash 8 bit values", 117 | "#112233", 118 | 17, 34, 51, 119 | }, 120 | { 121 | "hash 12 bit zero", 122 | "#000000000", 123 | 0, 0, 0, 124 | }, 125 | { 126 | "hash 12 bit max", 127 | "#fffffffff", 128 | 255, 255, 255, 129 | }, 130 | { 131 | "hash 12 bit values", 132 | "#111222333", 133 | 17, 34, 51, 134 | }, 135 | { 136 | "hash 16 bit zero", 137 | "#000000000000", 138 | 0, 0, 0, 139 | }, 140 | { 141 | "hash 16 bit max", 142 | "#ffffffffffff", 143 | 255, 255, 255, 144 | }, 145 | { 146 | "hash 16 bit values", 147 | "#111122223333", 148 | 17, 34, 51, 149 | }, 150 | { 151 | "rgb upper case", 152 | "RGB:0/A/F", 153 | 0, 170, 255, 154 | }, 155 | { 156 | "hash upper case", 157 | "#FFF", 158 | 240, 240, 240, 159 | }, 160 | } { 161 | t.Run(tc.name, func(t *testing.T) { 162 | r, g, b, err := parseColor(tc.input) 163 | if err != nil { 164 | t.Fatalf("failed to parse color: %s", err) 165 | } 166 | 167 | if r != tc.r || g != tc.g || b != tc.b { 168 | t.Fatalf("expected (%d, %d, %d), got (%d, %d, %d)", tc.r, tc.g, tc.b, r, g, b) 169 | } 170 | }) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /parse.go: -------------------------------------------------------------------------------- 1 | package vt10x 2 | 3 | func isControlCode(c rune) bool { 4 | return c < 0x20 || c == 0177 5 | } 6 | 7 | func (t *State) parse(c rune) { 8 | t.logf("%q", string(c)) 9 | if isControlCode(c) { 10 | if t.handleControlCodes(c) || t.cur.Attr.Mode&attrGfx == 0 { 11 | return 12 | } 13 | } 14 | // TODO: update selection; see st.c:2450 15 | 16 | if t.mode&ModeWrap != 0 && t.cur.State&cursorWrapNext != 0 { 17 | t.lines[t.cur.Y][t.cur.X].Mode |= attrWrap 18 | t.newline(true) 19 | } 20 | 21 | if t.mode&ModeInsert != 0 && t.cur.X+1 < t.cols { 22 | // TODO: move shiz, look at st.c:2458 23 | t.logln("insert mode not implemented") 24 | } 25 | 26 | t.setChar(c, &t.cur.Attr, t.cur.X, t.cur.Y) 27 | if t.cur.X+1 < t.cols { 28 | t.moveTo(t.cur.X+1, t.cur.Y) 29 | } else { 30 | t.cur.State |= cursorWrapNext 31 | } 32 | } 33 | 34 | func (t *State) parseEsc(c rune) { 35 | if t.handleControlCodes(c) { 36 | return 37 | } 38 | next := t.parse 39 | t.logf("%q", string(c)) 40 | switch c { 41 | case '[': 42 | next = t.parseEscCSI 43 | case '#': 44 | next = t.parseEscTest 45 | case 'P', // DCS - Device Control String 46 | '_', // APC - Application Program Command 47 | '^', // PM - Privacy Message 48 | ']', // OSC - Operating System Command 49 | 'k': // old title set compatibility 50 | t.str.reset() 51 | t.str.typ = c 52 | next = t.parseEscStr 53 | case '(': // set primary charset G0 54 | next = t.parseEscAltCharset 55 | case ')', // set secondary charset G1 (ignored) 56 | '*', // set tertiary charset G2 (ignored) 57 | '+': // set quaternary charset G3 (ignored) 58 | case 'D': // IND - linefeed 59 | if t.cur.Y == t.bottom { 60 | t.scrollUp(t.top, 1) 61 | } else { 62 | t.moveTo(t.cur.X, t.cur.Y+1) 63 | } 64 | case 'E': // NEL - next line 65 | t.newline(true) 66 | case 'H': // HTS - horizontal tab stop 67 | t.tabs[t.cur.X] = true 68 | case 'M': // RI - reverse index 69 | if t.cur.Y == t.top { 70 | t.scrollDown(t.top, 1) 71 | } else { 72 | t.moveTo(t.cur.X, t.cur.Y-1) 73 | } 74 | case 'Z': // DECID - identify terminal 75 | // TODO: write to our writer our id 76 | case 'c': // RIS - reset to initial state 77 | t.reset() 78 | case '=': // DECPAM - application keypad 79 | t.mode |= ModeAppKeypad 80 | case '>': // DECPNM - normal keypad 81 | t.mode &^= ModeAppKeypad 82 | case '7': // DECSC - save cursor 83 | t.saveCursor() 84 | case '8': // DECRC - restore cursor 85 | t.restoreCursor() 86 | case '\\': // ST - stop 87 | default: 88 | t.logf("unknown ESC sequence '%c'\n", c) 89 | } 90 | t.state = next 91 | } 92 | 93 | func (t *State) parseEscCSI(c rune) { 94 | if t.handleControlCodes(c) { 95 | return 96 | } 97 | t.logf("%q", string(c)) 98 | if t.csi.put(byte(c)) { 99 | t.state = t.parse 100 | t.handleCSI() 101 | } 102 | } 103 | 104 | func (t *State) parseEscStr(c rune) { 105 | t.logf("%q", string(c)) 106 | switch c { 107 | case '\033': 108 | t.state = t.parseEscStrEnd 109 | case '\a': // backwards compatiblity to xterm 110 | t.state = t.parse 111 | t.handleSTR() 112 | default: 113 | t.str.put(c) 114 | } 115 | } 116 | 117 | func (t *State) parseEscStrEnd(c rune) { 118 | if t.handleControlCodes(c) { 119 | return 120 | } 121 | t.logf("%q", string(c)) 122 | t.state = t.parse 123 | if c == '\\' { 124 | t.handleSTR() 125 | } 126 | } 127 | 128 | func (t *State) parseEscAltCharset(c rune) { 129 | if t.handleControlCodes(c) { 130 | return 131 | } 132 | t.logf("%q", string(c)) 133 | switch c { 134 | case '0': // line drawing set 135 | t.cur.Attr.Mode |= attrGfx 136 | case 'B': // USASCII 137 | t.cur.Attr.Mode &^= attrGfx 138 | case 'A', // UK (ignored) 139 | '<', // multinational (ignored) 140 | '5', // Finnish (ignored) 141 | 'C', // Finnish (ignored) 142 | 'K': // German (ignored) 143 | default: 144 | t.logf("unknown alt. charset '%c'\n", c) 145 | } 146 | t.state = t.parse 147 | } 148 | 149 | func (t *State) parseEscTest(c rune) { 150 | if t.handleControlCodes(c) { 151 | return 152 | } 153 | // DEC screen alignment test 154 | if c == '8' { 155 | for y := 0; y < t.rows; y++ { 156 | for x := 0; x < t.cols; x++ { 157 | t.setChar('E', &t.cur.Attr, x, y) 158 | } 159 | } 160 | } 161 | t.state = t.parse 162 | } 163 | 164 | func (t *State) handleControlCodes(c rune) bool { 165 | if !isControlCode(c) { 166 | return false 167 | } 168 | switch c { 169 | // HT 170 | case '\t': 171 | t.putTab(true) 172 | // BS 173 | case '\b': 174 | t.moveTo(t.cur.X-1, t.cur.Y) 175 | // CR 176 | case '\r': 177 | t.moveTo(0, t.cur.Y) 178 | // LF, VT, LF 179 | case '\f', '\v', '\n': 180 | // go to first col if mode is set 181 | t.newline(t.mode&ModeCRLF != 0) 182 | // BEL 183 | case '\a': 184 | // TODO: emit sound 185 | // TODO: window alert if not focused 186 | // ESC 187 | case 033: 188 | t.csi.reset() 189 | t.state = t.parseEsc 190 | // SO, SI 191 | case 016, 017: 192 | // different charsets not supported. apps should use the correct 193 | // alt charset escapes, probably for line drawing 194 | // SUB, CAN 195 | case 032, 030: 196 | t.csi.reset() 197 | // ignore ENQ, NUL, XON, XOFF, DEL 198 | case 005, 000, 021, 023, 0177: 199 | default: 200 | return false 201 | } 202 | return true 203 | } 204 | -------------------------------------------------------------------------------- /csi.go: -------------------------------------------------------------------------------- 1 | package vt10x 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // CSI (Control Sequence Introducer) 10 | // ESC+[ 11 | type csiEscape struct { 12 | buf []byte 13 | args []int 14 | mode byte 15 | priv bool 16 | } 17 | 18 | func (c *csiEscape) reset() { 19 | c.buf = c.buf[:0] 20 | c.args = c.args[:0] 21 | c.mode = 0 22 | c.priv = false 23 | } 24 | 25 | func (c *csiEscape) put(b byte) bool { 26 | c.buf = append(c.buf, b) 27 | if b >= 0x40 && b <= 0x7E || len(c.buf) >= 256 { 28 | c.parse() 29 | return true 30 | } 31 | return false 32 | } 33 | 34 | func (c *csiEscape) parse() { 35 | c.mode = c.buf[len(c.buf)-1] 36 | if len(c.buf) == 1 { 37 | return 38 | } 39 | s := string(c.buf) 40 | c.args = c.args[:0] 41 | if s[0] == '?' { 42 | c.priv = true 43 | s = s[1:] 44 | } 45 | s = s[:len(s)-1] 46 | ss := strings.Split(s, ";") 47 | for _, p := range ss { 48 | i, err := strconv.Atoi(p) 49 | if err != nil { 50 | //t.logf("invalid CSI arg '%s'\n", p) 51 | break 52 | } 53 | c.args = append(c.args, i) 54 | } 55 | } 56 | 57 | func (c *csiEscape) arg(i, def int) int { 58 | if i >= len(c.args) || i < 0 { 59 | return def 60 | } 61 | return c.args[i] 62 | } 63 | 64 | // maxarg takes the maximum of arg(i, def) and def 65 | func (c *csiEscape) maxarg(i, def int) int { 66 | return max(c.arg(i, def), def) 67 | } 68 | 69 | func (t *State) handleCSI() { 70 | c := &t.csi 71 | switch c.mode { 72 | default: 73 | goto unknown 74 | case '@': // ICH - insert blank char 75 | t.insertBlanks(c.arg(0, 1)) 76 | case 'A': // CUU - cursor up 77 | t.moveTo(t.cur.X, t.cur.Y-c.maxarg(0, 1)) 78 | case 'B', 'e': // CUD, VPR - cursor down 79 | t.moveTo(t.cur.X, t.cur.Y+c.maxarg(0, 1)) 80 | case 'c': // DA - device attributes 81 | if c.arg(0, 0) == 0 { 82 | // TODO: write vt102 id 83 | } 84 | case 'C', 'a': // CUF, HPR - cursor forward 85 | t.moveTo(t.cur.X+c.maxarg(0, 1), t.cur.Y) 86 | case 'D': // CUB - cursor backward 87 | t.moveTo(t.cur.X-c.maxarg(0, 1), t.cur.Y) 88 | case 'E': // CNL - cursor down and first col 89 | t.moveTo(0, t.cur.Y+c.arg(0, 1)) 90 | case 'F': // CPL - cursor up and first col 91 | t.moveTo(0, t.cur.Y-c.arg(0, 1)) 92 | case 'g': // TBC - tabulation clear 93 | switch c.arg(0, 0) { 94 | // clear current tab stop 95 | case 0: 96 | t.tabs[t.cur.X] = false 97 | // clear all tabs 98 | case 3: 99 | for i := range t.tabs { 100 | t.tabs[i] = false 101 | } 102 | default: 103 | goto unknown 104 | } 105 | case 'G', '`': // CHA, HPA - Move to 106 | t.moveTo(c.arg(0, 1)-1, t.cur.Y) 107 | case 'H', 'f': // CUP, HVP - move to 108 | t.moveAbsTo(c.arg(1, 1)-1, c.arg(0, 1)-1) 109 | case 'I': // CHT - cursor forward tabulation tab stops 110 | n := c.arg(0, 1) 111 | for i := 0; i < n; i++ { 112 | t.putTab(true) 113 | } 114 | case 'J': // ED - clear screen 115 | // TODO: sel.ob.x = -1 116 | switch c.arg(0, 0) { 117 | case 0: // below 118 | t.clear(t.cur.X, t.cur.Y, t.cols-1, t.cur.Y) 119 | if t.cur.Y < t.rows-1 { 120 | t.clear(0, t.cur.Y+1, t.cols-1, t.rows-1) 121 | } 122 | case 1: // above 123 | if t.cur.Y > 1 { 124 | t.clear(0, 0, t.cols-1, t.cur.Y-1) 125 | } 126 | t.clear(0, t.cur.Y, t.cur.X, t.cur.Y) 127 | case 2: // all 128 | t.clear(0, 0, t.cols-1, t.rows-1) 129 | default: 130 | goto unknown 131 | } 132 | case 'K': // EL - clear line 133 | switch c.arg(0, 0) { 134 | case 0: // right 135 | t.clear(t.cur.X, t.cur.Y, t.cols-1, t.cur.Y) 136 | case 1: // left 137 | t.clear(0, t.cur.Y, t.cur.X, t.cur.Y) 138 | case 2: // all 139 | t.clear(0, t.cur.Y, t.cols-1, t.cur.Y) 140 | } 141 | case 'S': // SU - scroll lines up 142 | t.scrollUp(t.top, c.arg(0, 1)) 143 | case 'T': // SD - scroll lines down 144 | t.scrollDown(t.top, c.arg(0, 1)) 145 | case 'L': // IL - insert blank lines 146 | t.insertBlankLines(c.arg(0, 1)) 147 | case 'l': // RM - reset mode 148 | t.setMode(c.priv, false, c.args) 149 | case 'M': // DL - delete lines 150 | t.deleteLines(c.arg(0, 1)) 151 | case 'X': // ECH - erase chars 152 | t.clear(t.cur.X, t.cur.Y, t.cur.X+c.arg(0, 1)-1, t.cur.Y) 153 | case 'P': // DCH - delete chars 154 | t.deleteChars(c.arg(0, 1)) 155 | case 'Z': // CBT - cursor backward tabulation tab stops 156 | n := c.arg(0, 1) 157 | for i := 0; i < n; i++ { 158 | t.putTab(false) 159 | } 160 | case 'd': // VPA - move to 161 | t.moveAbsTo(t.cur.X, c.arg(0, 1)-1) 162 | case 'h': // SM - set terminal mode 163 | t.setMode(c.priv, true, c.args) 164 | case 'm': // SGR - terminal attribute (color) 165 | t.setAttr(c.args) 166 | case 'n': 167 | switch c.arg(0, 0) { 168 | case 5: // DSR - device status report 169 | t.w.Write([]byte("\033[0n")) 170 | case 6: // CPR - cursor position report 171 | t.w.Write([]byte(fmt.Sprintf("\033[%d;%dR", t.cur.Y+1, t.cur.X+1))) 172 | } 173 | case 'r': // DECSTBM - set scrolling region 174 | if c.priv { 175 | goto unknown 176 | } else { 177 | t.setScroll(c.arg(0, 1)-1, c.arg(1, t.rows)-1) 178 | t.moveAbsTo(0, 0) 179 | } 180 | case 's': // DECSC - save cursor position (ANSI.SYS) 181 | t.saveCursor() 182 | case 'u': // DECRC - restore cursor position (ANSI.SYS) 183 | t.restoreCursor() 184 | } 185 | return 186 | unknown: // TODO: get rid of this goto 187 | t.logf("unknown CSI sequence '%c'\n", c.mode) 188 | // TODO: c.dump() 189 | } 190 | -------------------------------------------------------------------------------- /str.go: -------------------------------------------------------------------------------- 1 | package vt10x 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // STR sequences are similar to CSI sequences, but have string arguments (and 12 | // as far as I can tell, don't really have a name; STR is the name I took from 13 | // suckless which I imagine comes from rxvt or xterm). 14 | type strEscape struct { 15 | typ rune 16 | buf []rune 17 | args []string 18 | } 19 | 20 | func (s *strEscape) reset() { 21 | s.typ = 0 22 | s.buf = s.buf[:0] 23 | s.args = nil 24 | } 25 | 26 | func (s *strEscape) put(c rune) { 27 | // TODO: improve allocs with an array backed slice; bench first 28 | if len(s.buf) < 256 { 29 | s.buf = append(s.buf, c) 30 | } 31 | // Going by st, it is better to remain silent when the STR sequence is not 32 | // ended so that it is apparent to users something is wrong. The length sanity 33 | // check ensures we don't absorb the entire stream into memory. 34 | // TODO: see what rxvt or xterm does 35 | } 36 | 37 | func (s *strEscape) parse() { 38 | s.args = strings.Split(string(s.buf), ";") 39 | } 40 | 41 | func (s *strEscape) arg(i, def int) int { 42 | if i >= len(s.args) || i < 0 { 43 | return def 44 | } 45 | i, err := strconv.Atoi(s.args[i]) 46 | if err != nil { 47 | return def 48 | } 49 | return i 50 | } 51 | 52 | func (s *strEscape) argString(i int, def string) string { 53 | if i >= len(s.args) || i < 0 { 54 | return def 55 | } 56 | return s.args[i] 57 | } 58 | 59 | func (t *State) handleSTR() { 60 | s := &t.str 61 | s.parse() 62 | 63 | switch s.typ { 64 | case ']': // OSC - operating system command 65 | var p *string 66 | switch d := s.arg(0, 0); d { 67 | case 0, 1, 2: 68 | title := s.argString(1, "") 69 | if title != "" { 70 | t.setTitle(title) 71 | } 72 | case 10: 73 | if len(s.args) < 2 { 74 | break 75 | } 76 | 77 | c := s.argString(1, "") 78 | p := &c 79 | if p != nil && *p == "?" { 80 | t.oscColorResponse(int(DefaultFG), 10) 81 | } else if err := t.setColorName(int(DefaultFG), p); err != nil { 82 | t.logf("invalid foreground color: %s\n", maybe(p)) 83 | } else { 84 | // TODO: redraw 85 | } 86 | case 11: 87 | if len(s.args) < 2 { 88 | break 89 | } 90 | 91 | c := s.argString(1, "") 92 | p := &c 93 | if p != nil && *p == "?" { 94 | t.oscColorResponse(int(DefaultBG), 11) 95 | } else if err := t.setColorName(int(DefaultBG), p); err != nil { 96 | t.logf("invalid cursor color: %s\n", maybe(p)) 97 | } else { 98 | // TODO: redraw 99 | } 100 | // case 12: 101 | // if len(s.args) < 2 { 102 | // break 103 | // } 104 | 105 | // c := s.argString(1, "") 106 | // p := &c 107 | // if p != nil && *p == "?" { 108 | // t.oscColorResponse(int(DefaultCursor), 12) 109 | // } else if err := t.setColorName(int(DefaultCursor), p); err != nil { 110 | // t.logf("invalid background color: %s\n", p) 111 | // } else { 112 | // // TODO: redraw 113 | // } 114 | case 4: // color set 115 | if len(s.args) < 3 { 116 | break 117 | } 118 | 119 | c := s.argString(2, "") 120 | p = &c 121 | fallthrough 122 | case 104: // color reset 123 | j := -1 124 | if len(s.args) > 1 { 125 | j = s.arg(1, 0) 126 | } 127 | if p != nil && *p == "?" { // report 128 | t.osc4ColorResponse(j) 129 | } else if err := t.setColorName(j, p); err != nil { 130 | if !(d == 104 && len(s.args) <= 1) { 131 | t.logf("invalid color j=%d, p=%s\n", j, maybe(p)) 132 | } 133 | } else { 134 | // TODO: redraw 135 | } 136 | default: 137 | t.logf("unknown OSC command %d\n", d) 138 | // TODO: s.dump() 139 | } 140 | case 'k': // old title set compatibility 141 | title := s.argString(0, "") 142 | if title != "" { 143 | t.setTitle(title) 144 | } 145 | default: 146 | // TODO: Ignore these codes instead of complain? 147 | // 'P': // DSC - device control string 148 | // '_': // APC - application program command 149 | // '^': // PM - privacy message 150 | 151 | t.logf("unhandled STR sequence '%c'\n", s.typ) 152 | // t.str.dump() 153 | } 154 | } 155 | 156 | func (t *State) setColorName(j int, p *string) error { 157 | if !between(j, 0, 1<<24) { 158 | return fmt.Errorf("invalid color value %d", j) 159 | } 160 | 161 | if p == nil { 162 | // restore color 163 | delete(t.colorOverride, Color(j)) 164 | } else { 165 | // set color 166 | r, g, b, err := parseColor(*p) 167 | if err != nil { 168 | return err 169 | } 170 | t.colorOverride[Color(j)] = Color(r<<16 | g<<8 | b) 171 | } 172 | 173 | return nil 174 | } 175 | 176 | func (t *State) oscColorResponse(j, num int) { 177 | if j < 0 { 178 | t.logf("failed to fetch osc color %d\n", j) 179 | return 180 | } 181 | 182 | k, ok := t.colorOverride[Color(j)] 183 | if ok { 184 | j = int(k) 185 | } 186 | 187 | r, g, b := rgb(j) 188 | t.w.Write([]byte(fmt.Sprintf("\033]%d;rgb:%02x%02x/%02x%02x/%02x%02x\007", num, r, r, g, g, b, b))) 189 | } 190 | 191 | func (t *State) osc4ColorResponse(j int) { 192 | if j < 0 { 193 | t.logf("failed to fetch osc4 color %d\n", j) 194 | return 195 | } 196 | 197 | k, ok := t.colorOverride[Color(j)] 198 | if ok { 199 | j = int(k) 200 | } 201 | 202 | r, g, b := rgb(j) 203 | t.w.Write([]byte(fmt.Sprintf("\033]4;%d;rgb:%02x%02x/%02x%02x/%02x%02x\007", j, r, r, g, g, b, b))) 204 | } 205 | 206 | func rgb(j int) (r, g, b int) { 207 | return (j >> 16) & 0xff, (j >> 8) & 0xff, j & 0xff 208 | } 209 | 210 | var ( 211 | RGBPattern = regexp.MustCompile(`^([\da-f]{1})\/([\da-f]{1})\/([\da-f]{1})$|^([\da-f]{2})\/([\da-f]{2})\/([\da-f]{2})$|^([\da-f]{3})\/([\da-f]{3})\/([\da-f]{3})$|^([\da-f]{4})\/([\da-f]{4})\/([\da-f]{4})$`) 212 | HashPattern = regexp.MustCompile(`[\da-f]`) 213 | ) 214 | 215 | func parseColor(p string) (r, g, b int, err error) { 216 | if len(p) == 0 { 217 | err = fmt.Errorf("empty color spec") 218 | return 219 | } 220 | 221 | low := strings.ToLower(p) 222 | if strings.HasPrefix(low, "rgb:") { 223 | low = low[4:] 224 | sm := RGBPattern.FindAllStringSubmatch(low, -1) 225 | if len(sm) != 1 || len(sm[0]) == 0 { 226 | err = fmt.Errorf("invalid rgb color spec: %s", p) 227 | return 228 | } 229 | m := sm[0] 230 | 231 | var base float64 232 | if len(m[1]) > 0 { 233 | base = 15 234 | } else if len(m[4]) > 0 { 235 | base = 255 236 | } else if len(m[7]) > 0 { 237 | base = 4095 238 | } else { 239 | base = 65535 240 | } 241 | 242 | r64, err := strconv.ParseInt(firstNonEmpty(m[1], m[4], m[7], m[10]), 16, 0) 243 | if err != nil { 244 | return r, g, b, err 245 | } 246 | 247 | g64, err := strconv.ParseInt(firstNonEmpty(m[2], m[5], m[8], m[11]), 16, 0) 248 | if err != nil { 249 | return r, g, b, err 250 | } 251 | 252 | b64, err := strconv.ParseInt(firstNonEmpty(m[3], m[6], m[9], m[12]), 16, 0) 253 | if err != nil { 254 | return r, g, b, err 255 | } 256 | 257 | r = int(math.Round(float64(r64) / base * 255)) 258 | g = int(math.Round(float64(g64) / base * 255)) 259 | b = int(math.Round(float64(b64) / base * 255)) 260 | return r, g, b, nil 261 | } else if strings.HasPrefix(low, "#") { 262 | low = low[1:] 263 | m := HashPattern.FindAllString(low, -1) 264 | if !oneOf(len(m), []int{3, 6, 9, 12}) { 265 | err = fmt.Errorf("invalid hash color spec: %s", p) 266 | return 267 | } 268 | 269 | adv := len(low) / 3 270 | for i := 0; i < 3; i++ { 271 | c, err := strconv.ParseInt(low[adv*i:adv*i+adv], 16, 0) 272 | if err != nil { 273 | return r, g, b, err 274 | } 275 | 276 | var v int64 277 | switch adv { 278 | case 1: 279 | v = c << 4 280 | case 2: 281 | v = c 282 | case 3: 283 | v = c >> 4 284 | default: 285 | v = c >> 8 286 | } 287 | 288 | switch i { 289 | case 0: 290 | r = int(v) 291 | case 1: 292 | g = int(v) 293 | case 2: 294 | b = int(v) 295 | } 296 | } 297 | return 298 | } else { 299 | err = fmt.Errorf("invalid color spec: %s", p) 300 | return 301 | } 302 | } 303 | 304 | func maybe(p *string) string { 305 | if p == nil { 306 | return "" 307 | } 308 | return *p 309 | } 310 | 311 | func firstNonEmpty(strs ...string) string { 312 | if len(strs) == 0 { 313 | return "" 314 | } 315 | for _, str := range strs { 316 | if len(str) > 0 { 317 | return str 318 | } 319 | } 320 | return strs[len(strs)-1] 321 | } 322 | 323 | func oneOf(v int, is []int) bool { 324 | for _, i := range is { 325 | if v == i { 326 | return true 327 | } 328 | } 329 | return false 330 | } 331 | -------------------------------------------------------------------------------- /state.go: -------------------------------------------------------------------------------- 1 | package vt10x 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "sync" 7 | ) 8 | 9 | const ( 10 | tabspaces = 8 11 | ) 12 | 13 | const ( 14 | attrReverse = 1 << iota 15 | attrUnderline 16 | attrBold 17 | attrGfx 18 | attrItalic 19 | attrBlink 20 | attrWrap 21 | ) 22 | 23 | const ( 24 | cursorDefault = 1 << iota 25 | cursorWrapNext 26 | cursorOrigin 27 | ) 28 | 29 | // ModeFlag represents various terminal mode states. 30 | type ModeFlag uint32 31 | 32 | // Terminal modes 33 | const ( 34 | ModeWrap ModeFlag = 1 << iota 35 | ModeInsert 36 | ModeAppKeypad 37 | ModeAltScreen 38 | ModeCRLF 39 | ModeMouseButton 40 | ModeMouseMotion 41 | ModeReverse 42 | ModeKeyboardLock 43 | ModeHide 44 | ModeEcho 45 | ModeAppCursor 46 | ModeMouseSgr 47 | Mode8bit 48 | ModeBlink 49 | ModeFBlink 50 | ModeFocus 51 | ModeMouseX10 52 | ModeMouseMany 53 | ModeMouseMask = ModeMouseButton | ModeMouseMotion | ModeMouseX10 | ModeMouseMany 54 | ) 55 | 56 | // ChangeFlag represents possible state changes of the terminal. 57 | type ChangeFlag uint32 58 | 59 | // Terminal changes to occur in VT.ReadState 60 | const ( 61 | ChangedScreen ChangeFlag = 1 << iota 62 | ChangedTitle 63 | ) 64 | 65 | type Glyph struct { 66 | Char rune 67 | Mode int16 68 | FG, BG Color 69 | } 70 | 71 | type line []Glyph 72 | 73 | type Cursor struct { 74 | Attr Glyph 75 | X, Y int 76 | State uint8 77 | } 78 | 79 | type parseState func(c rune) 80 | 81 | // State represents the terminal emulation state. Use Lock/Unlock 82 | // methods to synchronize data access with VT. 83 | type State struct { 84 | DebugLogger *log.Logger 85 | 86 | w io.Writer 87 | mu sync.Mutex 88 | changed ChangeFlag 89 | cols, rows int 90 | lines []line 91 | altLines []line 92 | dirty []bool // line dirtiness 93 | anydirty bool 94 | cur, curSaved Cursor 95 | top, bottom int // scroll limits 96 | mode ModeFlag 97 | state parseState 98 | str strEscape 99 | csi csiEscape 100 | numlock bool 101 | tabs []bool 102 | title string 103 | colorOverride map[Color]Color 104 | } 105 | 106 | func newState(w io.Writer) *State { 107 | return &State{ 108 | w: w, 109 | colorOverride: make(map[Color]Color), 110 | } 111 | } 112 | 113 | func (t *State) logf(format string, args ...interface{}) { 114 | if t.DebugLogger != nil { 115 | t.DebugLogger.Printf(format, args...) 116 | } 117 | } 118 | 119 | func (t *State) logln(s string) { 120 | if t.DebugLogger != nil { 121 | t.DebugLogger.Println(s) 122 | } 123 | } 124 | 125 | func (t *State) lock() { 126 | t.mu.Lock() 127 | } 128 | 129 | func (t *State) unlock() { 130 | t.mu.Unlock() 131 | } 132 | 133 | // Lock locks the state object's mutex. 134 | func (t *State) Lock() { 135 | t.mu.Lock() 136 | } 137 | 138 | // Unlock resets change flags and unlocks the state object's mutex. 139 | func (t *State) Unlock() { 140 | t.resetChanges() 141 | t.mu.Unlock() 142 | } 143 | 144 | // Cell returns the glyph containing the character code, foreground color, and 145 | // background color at position (x, y) relative to the top left of the terminal. 146 | func (t *State) Cell(x, y int) Glyph { 147 | cell := t.lines[y][x] 148 | fg, ok := t.colorOverride[cell.FG] 149 | if ok { 150 | cell.FG = fg 151 | } 152 | bg, ok := t.colorOverride[cell.BG] 153 | if ok { 154 | cell.BG = bg 155 | } 156 | return cell 157 | } 158 | 159 | // Cursor returns the current position of the cursor. 160 | func (t *State) Cursor() Cursor { 161 | return t.cur 162 | } 163 | 164 | // CursorVisible returns the visible state of the cursor. 165 | func (t *State) CursorVisible() bool { 166 | return t.mode&ModeHide == 0 167 | } 168 | 169 | // Mode returns the current terminal mode. 170 | func (t *State) Mode() ModeFlag { 171 | return t.mode 172 | } 173 | 174 | // Title returns the current title set via the tty. 175 | func (t *State) Title() string { 176 | return t.title 177 | } 178 | 179 | /* 180 | // ChangeMask returns a bitfield of changes that have occured by VT. 181 | func (t *State) ChangeMask() ChangeFlag { 182 | return t.changed 183 | } 184 | */ 185 | 186 | // Changed returns true if change has occured. 187 | func (t *State) Changed(change ChangeFlag) bool { 188 | return t.changed&change != 0 189 | } 190 | 191 | // resetChanges resets the change mask and dirtiness. 192 | func (t *State) resetChanges() { 193 | for i := range t.dirty { 194 | t.dirty[i] = false 195 | } 196 | t.anydirty = false 197 | t.changed = 0 198 | } 199 | 200 | func (t *State) saveCursor() { 201 | t.curSaved = t.cur 202 | } 203 | 204 | func (t *State) restoreCursor() { 205 | t.cur = t.curSaved 206 | t.moveTo(t.cur.X, t.cur.Y) 207 | } 208 | 209 | func (t *State) put(c rune) { 210 | t.state(c) 211 | } 212 | 213 | func (t *State) putTab(forward bool) { 214 | x := t.cur.X 215 | if forward { 216 | if x == t.cols { 217 | return 218 | } 219 | for x++; x < t.cols && !t.tabs[x]; x++ { 220 | } 221 | } else { 222 | if x == 0 { 223 | return 224 | } 225 | for x--; x > 0 && !t.tabs[x]; x-- { 226 | } 227 | } 228 | t.moveTo(x, t.cur.Y) 229 | } 230 | 231 | func (t *State) newline(firstCol bool) { 232 | y := t.cur.Y 233 | if y == t.bottom { 234 | cur := t.cur 235 | t.cur = t.defaultCursor() 236 | t.scrollUp(t.top, 1) 237 | t.cur = cur 238 | } else { 239 | y++ 240 | } 241 | if firstCol { 242 | t.moveTo(0, y) 243 | } else { 244 | t.moveTo(t.cur.X, y) 245 | } 246 | } 247 | 248 | // table from st, which in turn is from rxvt :) 249 | var gfxCharTable = [62]rune{ 250 | '↑', '↓', '→', '←', '█', '▚', '☃', // A - G 251 | 0, 0, 0, 0, 0, 0, 0, 0, // H - O 252 | 0, 0, 0, 0, 0, 0, 0, 0, // P - W 253 | 0, 0, 0, 0, 0, 0, 0, ' ', // X - _ 254 | '◆', '▒', '␉', '␌', '␍', '␊', '°', '±', // ` - g 255 | '␤', '␋', '┘', '┐', '┌', '└', '┼', '⎺', // h - o 256 | '⎻', '─', '⎼', '⎽', '├', '┤', '┴', '┬', // p - w 257 | '│', '≤', '≥', 'π', '≠', '£', '·', // x - ~ 258 | } 259 | 260 | func (t *State) setChar(c rune, attr *Glyph, x, y int) { 261 | if attr.Mode&attrGfx != 0 { 262 | if c >= 0x41 && c <= 0x7e && gfxCharTable[c-0x41] != 0 { 263 | c = gfxCharTable[c-0x41] 264 | } 265 | } 266 | t.changed |= ChangedScreen 267 | t.dirty[y] = true 268 | t.lines[y][x] = *attr 269 | t.lines[y][x].Char = c 270 | //if t.options.BrightBold && attr.Mode&attrBold != 0 && attr.FG < 8 { 271 | if attr.Mode&attrBold != 0 && attr.FG < 8 { 272 | t.lines[y][x].FG = attr.FG + 8 273 | } 274 | if attr.Mode&attrReverse != 0 { 275 | t.lines[y][x].FG = attr.BG 276 | t.lines[y][x].BG = attr.FG 277 | } 278 | } 279 | 280 | func (t *State) defaultCursor() Cursor { 281 | c := Cursor{} 282 | c.Attr.FG = DefaultFG 283 | c.Attr.BG = DefaultBG 284 | return c 285 | } 286 | 287 | func (t *State) reset() { 288 | t.cur = t.defaultCursor() 289 | t.saveCursor() 290 | for i := range t.tabs { 291 | t.tabs[i] = false 292 | } 293 | for i := tabspaces; i < len(t.tabs); i += tabspaces { 294 | t.tabs[i] = true 295 | } 296 | t.top = 0 297 | t.bottom = t.rows - 1 298 | t.mode = ModeWrap 299 | t.clear(0, 0, t.rows-1, t.cols-1) 300 | t.moveTo(0, 0) 301 | } 302 | 303 | // TODO: definitely can improve allocs 304 | func (t *State) resize(cols, rows int) bool { 305 | if cols == t.cols && rows == t.rows { 306 | return false 307 | } 308 | if cols < 1 || rows < 1 { 309 | return false 310 | } 311 | slide := t.cur.Y - rows + 1 312 | if slide > 0 { 313 | copy(t.lines, t.lines[slide:slide+rows]) 314 | copy(t.altLines, t.altLines[slide:slide+rows]) 315 | } 316 | 317 | lines, altLines, tabs := t.lines, t.altLines, t.tabs 318 | t.lines = make([]line, rows) 319 | t.altLines = make([]line, rows) 320 | t.dirty = make([]bool, rows) 321 | t.tabs = make([]bool, cols) 322 | 323 | minrows := min(rows, t.rows) 324 | mincols := min(cols, t.cols) 325 | t.changed |= ChangedScreen 326 | for i := 0; i < rows; i++ { 327 | t.dirty[i] = true 328 | t.lines[i] = make(line, cols) 329 | t.altLines[i] = make(line, cols) 330 | } 331 | for i := 0; i < minrows; i++ { 332 | copy(t.lines[i], lines[i]) 333 | copy(t.altLines[i], altLines[i]) 334 | } 335 | copy(t.tabs, tabs) 336 | if cols > t.cols { 337 | i := t.cols - 1 338 | for i > 0 && !tabs[i] { 339 | i-- 340 | } 341 | for i += tabspaces; i < len(tabs); i += tabspaces { 342 | tabs[i] = true 343 | } 344 | } 345 | 346 | t.cols = cols 347 | t.rows = rows 348 | t.setScroll(0, rows-1) 349 | t.moveTo(t.cur.X, t.cur.Y) 350 | for i := 0; i < 2; i++ { 351 | if mincols < cols && minrows > 0 { 352 | t.clear(mincols, 0, cols-1, minrows-1) 353 | } 354 | if cols > 0 && minrows < rows { 355 | t.clear(0, minrows, cols-1, rows-1) 356 | } 357 | t.swapScreen() 358 | } 359 | return slide > 0 360 | } 361 | 362 | func (t *State) clear(x0, y0, x1, y1 int) { 363 | if x0 > x1 { 364 | x0, x1 = x1, x0 365 | } 366 | if y0 > y1 { 367 | y0, y1 = y1, y0 368 | } 369 | x0 = clamp(x0, 0, t.cols-1) 370 | x1 = clamp(x1, 0, t.cols-1) 371 | y0 = clamp(y0, 0, t.rows-1) 372 | y1 = clamp(y1, 0, t.rows-1) 373 | t.changed |= ChangedScreen 374 | for y := y0; y <= y1; y++ { 375 | t.dirty[y] = true 376 | for x := x0; x <= x1; x++ { 377 | t.lines[y][x] = t.cur.Attr 378 | t.lines[y][x].Char = ' ' 379 | } 380 | } 381 | } 382 | 383 | func (t *State) clearAll() { 384 | t.clear(0, 0, t.cols-1, t.rows-1) 385 | } 386 | 387 | func (t *State) moveAbsTo(x, y int) { 388 | if t.cur.State&cursorOrigin != 0 { 389 | y += t.top 390 | } 391 | t.moveTo(x, y) 392 | } 393 | 394 | func (t *State) moveTo(x, y int) { 395 | var miny, maxy int 396 | if t.cur.State&cursorOrigin != 0 { 397 | miny = t.top 398 | maxy = t.bottom 399 | } else { 400 | miny = 0 401 | maxy = t.rows - 1 402 | } 403 | x = clamp(x, 0, t.cols-1) 404 | y = clamp(y, miny, maxy) 405 | t.changed |= ChangedScreen 406 | t.cur.State &^= cursorWrapNext 407 | t.cur.X = x 408 | t.cur.Y = y 409 | } 410 | 411 | func (t *State) swapScreen() { 412 | t.lines, t.altLines = t.altLines, t.lines 413 | t.mode ^= ModeAltScreen 414 | t.dirtyAll() 415 | } 416 | 417 | func (t *State) dirtyAll() { 418 | t.changed |= ChangedScreen 419 | for y := 0; y < t.rows; y++ { 420 | t.dirty[y] = true 421 | } 422 | } 423 | 424 | func (t *State) setScroll(top, bottom int) { 425 | top = clamp(top, 0, t.rows-1) 426 | bottom = clamp(bottom, 0, t.rows-1) 427 | if top > bottom { 428 | top, bottom = bottom, top 429 | } 430 | t.top = top 431 | t.bottom = bottom 432 | } 433 | 434 | func min(a, b int) int { 435 | if a < b { 436 | return a 437 | } 438 | return b 439 | } 440 | 441 | func max(a, b int) int { 442 | if a > b { 443 | return a 444 | } 445 | return b 446 | } 447 | 448 | func clamp(val, min, max int) int { 449 | if val < min { 450 | return min 451 | } else if val > max { 452 | return max 453 | } 454 | return val 455 | } 456 | 457 | func between(val, min, max int) bool { 458 | if val < min || val > max { 459 | return false 460 | } 461 | return true 462 | } 463 | 464 | func (t *State) scrollDown(orig, n int) { 465 | n = clamp(n, 0, t.bottom-orig+1) 466 | t.clear(0, t.bottom-n+1, t.cols-1, t.bottom) 467 | t.changed |= ChangedScreen 468 | for i := t.bottom; i >= orig+n; i-- { 469 | t.lines[i], t.lines[i-n] = t.lines[i-n], t.lines[i] 470 | t.dirty[i] = true 471 | t.dirty[i-n] = true 472 | } 473 | 474 | // TODO: selection scroll 475 | } 476 | 477 | func (t *State) scrollUp(orig, n int) { 478 | n = clamp(n, 0, t.bottom-orig+1) 479 | t.clear(0, orig, t.cols-1, orig+n-1) 480 | t.changed |= ChangedScreen 481 | for i := orig; i <= t.bottom-n; i++ { 482 | t.lines[i], t.lines[i+n] = t.lines[i+n], t.lines[i] 483 | t.dirty[i] = true 484 | t.dirty[i+n] = true 485 | } 486 | 487 | // TODO: selection scroll 488 | } 489 | 490 | func (t *State) modMode(set bool, bit ModeFlag) { 491 | if set { 492 | t.mode |= bit 493 | } else { 494 | t.mode &^= bit 495 | } 496 | } 497 | 498 | func (t *State) setMode(priv bool, set bool, args []int) { 499 | if priv { 500 | for _, a := range args { 501 | switch a { 502 | case 1: // DECCKM - cursor key 503 | t.modMode(set, ModeAppCursor) 504 | case 5: // DECSCNM - reverse video 505 | mode := t.mode 506 | t.modMode(set, ModeReverse) 507 | if mode != t.mode { 508 | // TODO: redraw 509 | } 510 | case 6: // DECOM - origin 511 | if set { 512 | t.cur.State |= cursorOrigin 513 | } else { 514 | t.cur.State &^= cursorOrigin 515 | } 516 | t.moveAbsTo(0, 0) 517 | case 7: // DECAWM - auto wrap 518 | t.modMode(set, ModeWrap) 519 | // IGNORED: 520 | case 0, // error 521 | 2, // DECANM - ANSI/VT52 522 | 3, // DECCOLM - column 523 | 4, // DECSCLM - scroll 524 | 8, // DECARM - auto repeat 525 | 18, // DECPFF - printer feed 526 | 19, // DECPEX - printer extent 527 | 42, // DECNRCM - national characters 528 | 12: // att610 - start blinking cursor 529 | break 530 | case 25: // DECTCEM - text cursor enable mode 531 | t.modMode(!set, ModeHide) 532 | case 9: // X10 mouse compatibility mode 533 | t.modMode(false, ModeMouseMask) 534 | t.modMode(set, ModeMouseX10) 535 | case 1000: // report button press 536 | t.modMode(false, ModeMouseMask) 537 | t.modMode(set, ModeMouseButton) 538 | case 1002: // report motion on button press 539 | t.modMode(false, ModeMouseMask) 540 | t.modMode(set, ModeMouseMotion) 541 | case 1003: // enable all mouse motions 542 | t.modMode(false, ModeMouseMask) 543 | t.modMode(set, ModeMouseMany) 544 | case 1004: // send focus events to tty 545 | t.modMode(set, ModeFocus) 546 | case 1006: // extended reporting mode 547 | t.modMode(set, ModeMouseSgr) 548 | case 1034: 549 | t.modMode(set, Mode8bit) 550 | case 1049, // = 1047 and 1048 551 | 47, 1047: 552 | alt := t.mode&ModeAltScreen != 0 553 | if alt { 554 | t.clear(0, 0, t.cols-1, t.rows-1) 555 | } 556 | if !set || !alt { 557 | t.swapScreen() 558 | } 559 | if a != 1049 { 560 | break 561 | } 562 | fallthrough 563 | case 1048: 564 | if set { 565 | t.saveCursor() 566 | } else { 567 | t.restoreCursor() 568 | } 569 | case 1001: 570 | // mouse highlight mode; can hang the terminal by design when 571 | // implemented 572 | case 1005: 573 | // utf8 mouse mode; will confuse applications not supporting 574 | // utf8 and luit 575 | case 1015: 576 | // urxvt mangled mouse mode; incompatiblt and can be mistaken 577 | // for other control codes 578 | default: 579 | t.logf("unknown private set/reset mode %d\n", a) 580 | } 581 | } 582 | } else { 583 | for _, a := range args { 584 | switch a { 585 | case 0: // Error (ignored) 586 | case 2: // KAM - keyboard action 587 | t.modMode(set, ModeKeyboardLock) 588 | case 4: // IRM - insertion-replacement 589 | t.modMode(set, ModeInsert) 590 | t.logln("insert mode not implemented") 591 | case 12: // SRM - send/receive 592 | t.modMode(set, ModeEcho) 593 | case 20: // LNM - linefeed/newline 594 | t.modMode(set, ModeCRLF) 595 | case 34: 596 | t.logln("right-to-left mode not implemented") 597 | case 96: 598 | t.logln("right-to-left copy mode not implemented") 599 | default: 600 | t.logf("unknown set/reset mode %d\n", a) 601 | } 602 | } 603 | } 604 | } 605 | 606 | func (t *State) setAttr(attr []int) { 607 | if len(attr) == 0 { 608 | attr = []int{0} 609 | } 610 | for i := 0; i < len(attr); i++ { 611 | a := attr[i] 612 | switch a { 613 | case 0: 614 | t.cur.Attr.Mode &^= attrReverse | attrUnderline | attrBold | attrItalic | attrBlink 615 | t.cur.Attr.FG = DefaultFG 616 | t.cur.Attr.BG = DefaultBG 617 | case 1: 618 | t.cur.Attr.Mode |= attrBold 619 | case 3: 620 | t.cur.Attr.Mode |= attrItalic 621 | case 4: 622 | t.cur.Attr.Mode |= attrUnderline 623 | case 5, 6: // slow, rapid blink 624 | t.cur.Attr.Mode |= attrBlink 625 | case 7: 626 | t.cur.Attr.Mode |= attrReverse 627 | case 21, 22: 628 | t.cur.Attr.Mode &^= attrBold 629 | case 23: 630 | t.cur.Attr.Mode &^= attrItalic 631 | case 24: 632 | t.cur.Attr.Mode &^= attrUnderline 633 | case 25, 26: 634 | t.cur.Attr.Mode &^= attrBlink 635 | case 27: 636 | t.cur.Attr.Mode &^= attrReverse 637 | case 38: 638 | if i+2 < len(attr) && attr[i+1] == 5 { 639 | i += 2 640 | if between(attr[i], 0, 255) { 641 | t.cur.Attr.FG = Color(attr[i]) 642 | } else { 643 | t.logf("bad fgcolor %d\n", attr[i]) 644 | } 645 | } else if i+4 < len(attr) && attr[i+1] == 2 { 646 | i += 4 647 | r, g, b := attr[i-2], attr[i-1], attr[i] 648 | if !between(r, 0, 255) || !between(g, 0, 255) || !between(b, 0, 255) { 649 | t.logf("bad fg rgb color (%d,%d,%d)\n", r, g, b) 650 | } else { 651 | t.cur.Attr.FG = Color(r<<16 | g<<8 | b) 652 | } 653 | } else { 654 | t.logf("gfx attr %d unknown\n", a) 655 | } 656 | case 39: 657 | t.cur.Attr.FG = DefaultFG 658 | case 48: 659 | if i+2 < len(attr) && attr[i+1] == 5 { 660 | i += 2 661 | if between(attr[i], 0, 255) { 662 | t.cur.Attr.BG = Color(attr[i]) 663 | } else { 664 | t.logf("bad bgcolor %d\n", attr[i]) 665 | } 666 | } else if i+4 < len(attr) && attr[i+1] == 2 { 667 | i += 4 668 | r, g, b := attr[i-2], attr[i-1], attr[i] 669 | if !between(r, 0, 255) || !between(g, 0, 255) || !between(b, 0, 255) { 670 | t.logf("bad bg rgb color (%d,%d,%d)\n", r, g, b) 671 | } else { 672 | t.cur.Attr.BG = Color(r<<16 | g<<8 | b) 673 | } 674 | } else { 675 | t.logf("gfx attr %d unknown\n", a) 676 | } 677 | case 49: 678 | t.cur.Attr.BG = DefaultBG 679 | default: 680 | if between(a, 30, 37) { 681 | t.cur.Attr.FG = Color(a - 30) 682 | } else if between(a, 40, 47) { 683 | t.cur.Attr.BG = Color(a - 40) 684 | } else if between(a, 90, 97) { 685 | t.cur.Attr.FG = Color(a - 90 + 8) 686 | } else if between(a, 100, 107) { 687 | t.cur.Attr.BG = Color(a - 100 + 8) 688 | } else { 689 | t.logf("gfx attr %d unknown\n", a) 690 | } 691 | } 692 | } 693 | } 694 | 695 | func (t *State) insertBlanks(n int) { 696 | src := t.cur.X 697 | dst := src + n 698 | size := t.cols - dst 699 | t.changed |= ChangedScreen 700 | t.dirty[t.cur.Y] = true 701 | 702 | if dst >= t.cols { 703 | t.clear(t.cur.X, t.cur.Y, t.cols-1, t.cur.Y) 704 | } else { 705 | copy(t.lines[t.cur.Y][dst:dst+size], t.lines[t.cur.Y][src:src+size]) 706 | t.clear(src, t.cur.Y, dst-1, t.cur.Y) 707 | } 708 | } 709 | 710 | func (t *State) insertBlankLines(n int) { 711 | if t.cur.Y < t.top || t.cur.Y > t.bottom { 712 | return 713 | } 714 | t.scrollDown(t.cur.Y, n) 715 | } 716 | 717 | func (t *State) deleteLines(n int) { 718 | if t.cur.Y < t.top || t.cur.Y > t.bottom { 719 | return 720 | } 721 | t.scrollUp(t.cur.Y, n) 722 | } 723 | 724 | func (t *State) deleteChars(n int) { 725 | src := t.cur.X + n 726 | dst := t.cur.X 727 | size := t.cols - src 728 | t.changed |= ChangedScreen 729 | t.dirty[t.cur.Y] = true 730 | 731 | if src >= t.cols { 732 | t.clear(t.cur.X, t.cur.Y, t.cols-1, t.cur.Y) 733 | } else { 734 | copy(t.lines[t.cur.Y][dst:dst+size], t.lines[t.cur.Y][src:src+size]) 735 | t.clear(t.cols-n, t.cur.Y, t.cols-1, t.cur.Y) 736 | } 737 | } 738 | 739 | func (t *State) setTitle(title string) { 740 | t.changed |= ChangedTitle 741 | t.title = title 742 | } 743 | 744 | func (t *State) Size() (cols, rows int) { 745 | return t.cols, t.rows 746 | } 747 | 748 | func (t *State) String() string { 749 | t.Lock() 750 | defer t.Unlock() 751 | 752 | var view []rune 753 | for y := 0; y < t.rows; y++ { 754 | for x := 0; x < t.cols; x++ { 755 | attr := t.Cell(x, y) 756 | view = append(view, attr.Char) 757 | } 758 | view = append(view, '\n') 759 | } 760 | 761 | return string(view) 762 | } 763 | --------------------------------------------------------------------------------