New Upstream Release - golang-github-hinshun-vt10x
Ready changes
Summary
Merged new upstream version: 0.0~git20220301.5011da4 (was: 0.0~git20180809.d55458d+ds1).
Resulting package
Built on 2022-03-14T17:50 (took 9m49s)
The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:
apt install -t fresh-releases golang-github-hinshun-vt10x-dev
Lintian Result
Diff
diff --git a/Gopkg.lock b/Gopkg.lock
deleted file mode 100644
index c17e0c6..0000000
--- a/Gopkg.lock
+++ /dev/null
@@ -1,96 +0,0 @@
-# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
-
-
-[[projects]]
- branch = "master"
- name = "github.com/Netflix/go-expect"
- packages = ["."]
- revision = "c93bf25de8e869da25cf26bcd2932b36141f61ae"
-
-[[projects]]
- name = "github.com/davecgh/go-spew"
- packages = ["spew"]
- revision = "346938d642f2ec3594ed81d874461961cd0faa76"
- version = "v1.1.0"
-
-[[projects]]
- branch = "master"
- name = "github.com/gdamore/encoding"
- packages = ["."]
- revision = "b23993cbb6353f0e6aa98d0ee318a34728f628b9"
-
-[[projects]]
- branch = "master"
- name = "github.com/gdamore/tcell"
- packages = [
- ".",
- "terminfo"
- ]
- revision = "b3cebc399d6f98536af845ed8a5144ab586f6759"
-
-[[projects]]
- name = "github.com/kr/pty"
- packages = ["."]
- revision = "282ce0e5322c82529687d609ee670fac7c7d917c"
- version = "v1.1.1"
-
-[[projects]]
- name = "github.com/lucasb-eyer/go-colorful"
- packages = ["."]
- revision = "345fbb3dbcdb252d9985ee899a84963c0fa24c82"
- version = "v1.0"
-
-[[projects]]
- name = "github.com/mattn/go-runewidth"
- packages = ["."]
- revision = "9e777a8366cce605130a531d2cd6363d07ad7317"
- version = "v0.0.2"
-
-[[projects]]
- name = "github.com/pmezard/go-difflib"
- packages = ["difflib"]
- revision = "792786c7400a136282c1664665ae0a8db921c6c2"
- version = "v1.0.0"
-
-[[projects]]
- name = "github.com/stretchr/testify"
- packages = [
- "assert",
- "require"
- ]
- revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71"
- version = "v1.2.1"
-
-[[projects]]
- branch = "master"
- name = "golang.org/x/crypto"
- packages = ["ssh/terminal"]
- revision = "8ac0e0d97ce45cd83d1d7243c060cb8461dda5e9"
-
-[[projects]]
- branch = "master"
- name = "golang.org/x/sys"
- packages = [
- "unix",
- "windows"
- ]
- revision = "9527bec2660bd847c050fda93a0f0c6dee0800bb"
-
-[[projects]]
- name = "golang.org/x/text"
- packages = [
- "encoding",
- "encoding/internal/identifier",
- "internal/gen",
- "transform",
- "unicode/cldr"
- ]
- revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
- version = "v0.3.0"
-
-[solve-meta]
- analyzer-name = "dep"
- analyzer-version = 1
- inputs-digest = "bd24f29ad753e2f861768c734d71fbbc4e0380b5d7b43b6b101c01274cabcedb"
- solver-name = "gps-cdcl"
- solver-version = 1
diff --git a/Gopkg.toml b/Gopkg.toml
deleted file mode 100644
index f63b1c2..0000000
--- a/Gopkg.toml
+++ /dev/null
@@ -1,38 +0,0 @@
-# Gopkg.toml example
-#
-# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
-# for detailed Gopkg.toml documentation.
-#
-# required = ["github.com/user/thing/cmd/thing"]
-# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
-#
-# [[constraint]]
-# name = "github.com/user/project"
-# version = "1.0.0"
-#
-# [[constraint]]
-# name = "github.com/user/project2"
-# branch = "dev"
-# source = "github.com/myfork/project2"
-#
-# [[override]]
-# name = "github.com/x/y"
-# version = "2.4.0"
-#
-# [prune]
-# non-go = false
-# go-tests = true
-# unused-packages = true
-
-
-[[constraint]]
- name = "github.com/kr/pty"
- version = "1.1.1"
-
-[[constraint]]
- name = "github.com/stretchr/testify"
- version = "1.2.1"
-
-[prune]
- go-tests = true
- unused-packages = true
diff --git a/cmd/dump/main.go b/cmd/dump/main.go
deleted file mode 100644
index 1f9d415..0000000
--- a/cmd/dump/main.go
+++ /dev/null
@@ -1,62 +0,0 @@
-package main
-
-import (
- "fmt"
- "os"
- "os/exec"
- "time"
-
- "github.com/hinshun/vt10x"
- "github.com/kr/pty"
-)
-
-func main() {
- err := run()
- if err != nil {
- fmt.Fprintf(os.Stderr, "%v\n", err)
- os.Exit(1)
- }
-}
-
-func run() error {
- ptm, pts, err := pty.Open()
- if err != nil {
- return err
- }
- defer pts.Close()
- defer ptm.Close()
-
- c := exec.Command(os.Getenv("SHELL"))
- c.Stdout = pts
- c.Stdin = pts
- c.Stderr = pts
-
- var state vt10x.State
- term, err := vt10x.Create(&state, ptm)
- if err != nil {
- return err
- }
- defer term.Close()
-
- rows, cols := state.Size()
- vt10x.ResizePty(ptm, cols, rows)
-
- go func() {
- for {
- err := term.Parse()
- if err != nil {
- fmt.Fprintln(os.Stderr, err)
- break
- }
- }
- }()
-
- err = c.Start()
- if err != nil {
- return err
- }
-
- time.Sleep(time.Second)
- fmt.Println(state.String())
- return nil
-}
diff --git a/cmd/goterm/main.go b/cmd/goterm/main.go
deleted file mode 100644
index 2311bfe..0000000
--- a/cmd/goterm/main.go
+++ /dev/null
@@ -1,129 +0,0 @@
-package main
-
-import (
- "fmt"
- "io"
- "os"
- "os/exec"
-
- "github.com/gdamore/tcell"
- "github.com/hinshun/vt10x"
- "github.com/kr/pty"
-)
-
-func main() {
- err := goterm()
- if err != nil {
- fmt.Fprintf(os.Stderr, "%v\n", err)
- os.Exit(1)
- }
-}
-
-func goterm() error {
- cmd := exec.Command(os.Getenv("SHELL"), "-i")
- ptm, err := pty.Start(cmd)
- if err != nil {
- return err
- }
-
- // f, err := os.OpenFile("debug.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
- // if err != nil {
- // return err
- // }
- // state := vt10x.State{
- // DebugLogger: log.New(f, "", log.LstdFlags),
- // }
- var state vt10x.State
- term, err := vt10x.Create(&state, ptm)
- if err != nil {
- return err
- }
- defer term.Close()
-
- s, err := tcell.NewScreen()
- if err != nil {
- return err
- }
- defer s.Fini()
-
- err = s.Init()
- if err != nil {
- return err
- }
-
- width, height := s.Size()
- vt10x.ResizePty(ptm, width, height)
- term.Resize(width, height)
-
- endc := make(chan bool)
- updatec := make(chan struct{}, 1)
- go func() {
- defer close(endc)
- for {
- err := term.Parse()
- if err != nil {
- fmt.Fprintln(os.Stderr, err)
- break
- }
- select {
- case updatec <- struct{}{}:
- default:
- }
- }
- }()
-
- go func() {
- io.Copy(ptm, os.Stdin)
- }()
-
- eventc := make(chan tcell.Event, 4)
- go func() {
- for {
- eventc <- s.PollEvent()
- }
- }()
-
- for {
- select {
- case event := <-eventc:
- switch ev := event.(type) {
- case *tcell.EventResize:
- width, height = ev.Size()
- vt10x.ResizePty(ptm, width, height)
- term.Resize(width, height)
- s.Sync()
- }
- case <-endc:
- return nil
- case <-updatec:
- update(s, &state, width, height)
- }
- }
-}
-
-func update(s tcell.Screen, state *vt10x.State, w, h int) {
- state.Lock()
- defer state.Unlock()
- for y := 0; y < h; y++ {
- for x := 0; x < w; x++ {
- c, fg, bg := state.Cell(x, y)
-
- style := tcell.StyleDefault
- if fg != vt10x.DefaultFG {
- style = style.Foreground(tcell.Color(fg))
- }
- if bg != vt10x.DefaultBG {
- style = style.Background(tcell.Color(bg))
- }
-
- s.SetContent(x, y, c, nil, style)
- }
- }
- if state.CursorVisible() {
- curx, cury := state.Cursor()
- s.ShowCursor(curx, cury)
- } else {
- s.HideCursor()
- }
- s.Show()
-}
diff --git a/color.go b/color.go
index 4ce0d8f..c16d3ac 100644
--- a/color.go
+++ b/color.go
@@ -24,12 +24,13 @@ const (
// For example, a transparent background. Otherwise, the simple case is to
// map default colors to another color.
const (
- DefaultFG Color = 0xff80 + iota
+ DefaultFG Color = 1<<24 + iota
DefaultBG
+ DefaultCursor
)
// Color maps to the ANSI colors [0, 16) and the xterm colors [16, 256).
-type Color uint16
+type Color uint32
// ANSI returns true if Color is within [0, 16).
func (c Color) ANSI() bool {
diff --git a/csi.go b/csi.go
index 138cff9..f5df174 100644
--- a/csi.go
+++ b/csi.go
@@ -74,26 +74,26 @@ func (t *State) handleCSI() {
case '@': // ICH - insert <n> blank char
t.insertBlanks(c.arg(0, 1))
case 'A': // CUU - cursor <n> up
- t.moveTo(t.cur.x, t.cur.y-c.maxarg(0, 1))
+ t.moveTo(t.cur.X, t.cur.Y-c.maxarg(0, 1))
case 'B', 'e': // CUD, VPR - cursor <n> down
- t.moveTo(t.cur.x, t.cur.y+c.maxarg(0, 1))
+ t.moveTo(t.cur.X, t.cur.Y+c.maxarg(0, 1))
case 'c': // DA - device attributes
if c.arg(0, 0) == 0 {
// TODO: write vt102 id
}
case 'C', 'a': // CUF, HPR - cursor <n> forward
- t.moveTo(t.cur.x+c.maxarg(0, 1), t.cur.y)
+ t.moveTo(t.cur.X+c.maxarg(0, 1), t.cur.Y)
case 'D': // CUB - cursor <n> backward
- t.moveTo(t.cur.x-c.maxarg(0, 1), t.cur.y)
+ t.moveTo(t.cur.X-c.maxarg(0, 1), t.cur.Y)
case 'E': // CNL - cursor <n> down and first col
- t.moveTo(0, t.cur.y+c.arg(0, 1))
+ t.moveTo(0, t.cur.Y+c.arg(0, 1))
case 'F': // CPL - cursor <n> up and first col
- t.moveTo(0, t.cur.y-c.arg(0, 1))
+ t.moveTo(0, t.cur.Y-c.arg(0, 1))
case 'g': // TBC - tabulation clear
switch c.arg(0, 0) {
// clear current tab stop
case 0:
- t.tabs[t.cur.x] = false
+ t.tabs[t.cur.X] = false
// clear all tabs
case 3:
for i := range t.tabs {
@@ -103,7 +103,7 @@ func (t *State) handleCSI() {
goto unknown
}
case 'G', '`': // CHA, HPA - Move to <col>
- t.moveTo(c.arg(0, 1)-1, t.cur.y)
+ t.moveTo(c.arg(0, 1)-1, t.cur.Y)
case 'H', 'f': // CUP, HVP - move to <row> <col>
t.moveAbsTo(c.arg(1, 1)-1, c.arg(0, 1)-1)
case 'I': // CHT - cursor forward tabulation <n> tab stops
@@ -115,15 +115,15 @@ func (t *State) handleCSI() {
// TODO: sel.ob.x = -1
switch c.arg(0, 0) {
case 0: // below
- t.clear(t.cur.x, t.cur.y, t.cols-1, t.cur.y)
- if t.cur.y < t.rows-1 {
- t.clear(0, t.cur.y+1, t.cols-1, t.rows-1)
+ t.clear(t.cur.X, t.cur.Y, t.cols-1, t.cur.Y)
+ if t.cur.Y < t.rows-1 {
+ t.clear(0, t.cur.Y+1, t.cols-1, t.rows-1)
}
case 1: // above
- if t.cur.y > 1 {
- t.clear(0, 0, t.cols-1, t.cur.y-1)
+ if t.cur.Y > 1 {
+ t.clear(0, 0, t.cols-1, t.cur.Y-1)
}
- t.clear(0, t.cur.y, t.cur.x, t.cur.y)
+ t.clear(0, t.cur.Y, t.cur.X, t.cur.Y)
case 2: // all
t.clear(0, 0, t.cols-1, t.rows-1)
default:
@@ -132,11 +132,11 @@ func (t *State) handleCSI() {
case 'K': // EL - clear line
switch c.arg(0, 0) {
case 0: // right
- t.clear(t.cur.x, t.cur.y, t.cols-1, t.cur.y)
+ t.clear(t.cur.X, t.cur.Y, t.cols-1, t.cur.Y)
case 1: // left
- t.clear(0, t.cur.y, t.cur.x, t.cur.y)
+ t.clear(0, t.cur.Y, t.cur.X, t.cur.Y)
case 2: // all
- t.clear(0, t.cur.y, t.cols-1, t.cur.y)
+ t.clear(0, t.cur.Y, t.cols-1, t.cur.Y)
}
case 'S': // SU - scroll <n> lines up
t.scrollUp(t.top, c.arg(0, 1))
@@ -149,7 +149,7 @@ func (t *State) handleCSI() {
case 'M': // DL - delete <n> lines
t.deleteLines(c.arg(0, 1))
case 'X': // ECH - erase <n> chars
- t.clear(t.cur.x, t.cur.y, t.cur.x+c.arg(0, 1)-1, t.cur.y)
+ t.clear(t.cur.X, t.cur.Y, t.cur.X+c.arg(0, 1)-1, t.cur.Y)
case 'P': // DCH - delete <n> chars
t.deleteChars(c.arg(0, 1))
case 'Z': // CBT - cursor backward tabulation <n> tab stops
@@ -158,7 +158,7 @@ func (t *State) handleCSI() {
t.putTab(false)
}
case 'd': // VPA - move to <row>
- t.moveAbsTo(t.cur.x, c.arg(0, 1)-1)
+ t.moveAbsTo(t.cur.X, c.arg(0, 1)-1)
case 'h': // SM - set terminal mode
t.setMode(c.priv, true, c.args)
case 'm': // SGR - terminal attribute (color)
@@ -168,7 +168,7 @@ func (t *State) handleCSI() {
case 5: // DSR - device status report
t.w.Write([]byte("\033[0n"))
case 6: // CPR - cursor position report
- t.w.Write([]byte(fmt.Sprintf("\033[%d;%dR", t.cur.y+1, t.cur.x+1)))
+ t.w.Write([]byte(fmt.Sprintf("\033[%d;%dR", t.cur.Y+1, t.cur.X+1)))
}
case 'r': // DECSTBM - set scrolling region
if c.priv {
diff --git a/debian/changelog b/debian/changelog
index c732932..93e561e 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+golang-github-hinshun-vt10x (0.0~git20220301.5011da4-1) UNRELEASED; urgency=low
+
+ * New upstream snapshot.
+
+ -- Debian Janitor <janitor@jelmer.uk> Mon, 14 Mar 2022 17:47:39 -0000
+
golang-github-hinshun-vt10x (0.0~git20180809.d55458d+ds1-2) unstable; urgency=medium
* Source-only upload for testing eligibility.
diff --git a/expect.go b/expect.go
deleted file mode 100644
index 1c1c646..0000000
--- a/expect.go
+++ /dev/null
@@ -1,29 +0,0 @@
-package vt10x
-
-import (
- expect "github.com/Netflix/go-expect"
- "github.com/kr/pty"
-)
-
-// NewVT10XConsole returns a new expect.Console that multiplexes the
-// Stdin/Stdout to a VT10X terminal, allowing Console to interact with an
-// application sending ANSI escape sequences.
-func NewVT10XConsole(opts ...expect.ConsoleOpt) (*expect.Console, *State, error) {
- ptm, pts, err := pty.Open()
- if err != nil {
- return nil, nil, err
- }
-
- var state State
- term, err := Create(&state, pts)
- if err != nil {
- return nil, nil, err
- }
-
- c, err := expect.NewConsole(append(opts, expect.WithStdin(ptm), expect.WithStdout(term), expect.WithCloser(pts, ptm, term))...)
- if err != nil {
- return nil, nil, err
- }
-
- return c, &state, nil
-}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..11095c6
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module github.com/hinshun/vt10x
+
+go 1.14
diff --git a/parse.go b/parse.go
index 51a1980..0b84145 100644
--- a/parse.go
+++ b/parse.go
@@ -7,27 +7,27 @@ func isControlCode(c rune) bool {
func (t *State) parse(c rune) {
t.logf("%q", string(c))
if isControlCode(c) {
- if t.handleControlCodes(c) || t.cur.attr.mode&attrGfx == 0 {
+ if t.handleControlCodes(c) || t.cur.Attr.Mode&attrGfx == 0 {
return
}
}
// TODO: update selection; see st.c:2450
- if t.mode&ModeWrap != 0 && t.cur.state&cursorWrapNext != 0 {
- t.lines[t.cur.y][t.cur.x].mode |= attrWrap
+ if t.mode&ModeWrap != 0 && t.cur.State&cursorWrapNext != 0 {
+ t.lines[t.cur.Y][t.cur.X].Mode |= attrWrap
t.newline(true)
}
- if t.mode&ModeInsert != 0 && t.cur.x+1 < t.cols {
+ if t.mode&ModeInsert != 0 && t.cur.X+1 < t.cols {
// TODO: move shiz, look at st.c:2458
t.logln("insert mode not implemented")
}
- t.setChar(c, &t.cur.attr, t.cur.x, t.cur.y)
- if t.cur.x+1 < t.cols {
- t.moveTo(t.cur.x+1, t.cur.y)
+ t.setChar(c, &t.cur.Attr, t.cur.X, t.cur.Y)
+ if t.cur.X+1 < t.cols {
+ t.moveTo(t.cur.X+1, t.cur.Y)
} else {
- t.cur.state |= cursorWrapNext
+ t.cur.State |= cursorWrapNext
}
}
@@ -56,20 +56,20 @@ func (t *State) parseEsc(c rune) {
'*', // set tertiary charset G2 (ignored)
'+': // set quaternary charset G3 (ignored)
case 'D': // IND - linefeed
- if t.cur.y == t.bottom {
+ if t.cur.Y == t.bottom {
t.scrollUp(t.top, 1)
} else {
- t.moveTo(t.cur.x, t.cur.y+1)
+ t.moveTo(t.cur.X, t.cur.Y+1)
}
case 'E': // NEL - next line
t.newline(true)
case 'H': // HTS - horizontal tab stop
- t.tabs[t.cur.x] = true
+ t.tabs[t.cur.X] = true
case 'M': // RI - reverse index
- if t.cur.y == t.top {
+ if t.cur.Y == t.top {
t.scrollDown(t.top, 1)
} else {
- t.moveTo(t.cur.x, t.cur.y-1)
+ t.moveTo(t.cur.X, t.cur.Y-1)
}
case 'Z': // DECID - identify terminal
// TODO: write to our writer our id
@@ -132,9 +132,9 @@ func (t *State) parseEscAltCharset(c rune) {
t.logf("%q", string(c))
switch c {
case '0': // line drawing set
- t.cur.attr.mode |= attrGfx
+ t.cur.Attr.Mode |= attrGfx
case 'B': // USASCII
- t.cur.attr.mode &^= attrGfx
+ t.cur.Attr.Mode &^= attrGfx
case 'A', // UK (ignored)
'<', // multinational (ignored)
'5', // Finnish (ignored)
@@ -154,7 +154,7 @@ func (t *State) parseEscTest(c rune) {
if c == '8' {
for y := 0; y < t.rows; y++ {
for x := 0; x < t.cols; x++ {
- t.setChar('E', &t.cur.attr, x, y)
+ t.setChar('E', &t.cur.Attr, x, y)
}
}
}
@@ -171,10 +171,10 @@ func (t *State) handleControlCodes(c rune) bool {
t.putTab(true)
// BS
case '\b':
- t.moveTo(t.cur.x-1, t.cur.y)
+ t.moveTo(t.cur.X-1, t.cur.Y)
// CR
case '\r':
- t.moveTo(0, t.cur.y)
+ t.moveTo(0, t.cur.Y)
// LF, VT, LF
case '\f', '\v', '\n':
// go to first col if mode is set
diff --git a/state.go b/state.go
index 9e8dd3e..b2b4078 100644
--- a/state.go
+++ b/state.go
@@ -62,18 +62,18 @@ const (
ChangedTitle
)
-type glyph struct {
- c rune
- mode int16
- fg, bg Color
+type Glyph struct {
+ Char rune
+ Mode int16
+ FG, BG Color
}
-type line []glyph
+type line []Glyph
-type cursor struct {
- attr glyph
- x, y int
- state uint8
+type Cursor struct {
+ Attr Glyph
+ X, Y int
+ State uint8
}
type parseState func(c rune)
@@ -91,7 +91,7 @@ type State struct {
altLines []line
dirty []bool // line dirtiness
anydirty bool
- cur, curSaved cursor
+ cur, curSaved Cursor
top, bottom int // scroll limits
mode ModeFlag
state parseState
@@ -100,6 +100,14 @@ type State struct {
numlock bool
tabs []bool
title string
+ colorOverride map[Color]Color
+}
+
+func newState(w io.Writer) *State {
+ return &State{
+ w: w,
+ colorOverride: make(map[Color]Color),
+ }
}
func (t *State) logf(format string, args ...interface{}) {
@@ -133,15 +141,24 @@ func (t *State) Unlock() {
t.mu.Unlock()
}
-// Cell returns the character code, foreground color, and background
-// color at position (x, y) relative to the top left of the terminal.
-func (t *State) Cell(x, y int) (ch rune, fg Color, bg Color) {
- return t.lines[y][x].c, Color(t.lines[y][x].fg), Color(t.lines[y][x].bg)
+// Cell returns the glyph containing the character code, foreground color, and
+// background color at position (x, y) relative to the top left of the terminal.
+func (t *State) Cell(x, y int) Glyph {
+ cell := t.lines[y][x]
+ fg, ok := t.colorOverride[cell.FG]
+ if ok {
+ cell.FG = fg
+ }
+ bg, ok := t.colorOverride[cell.BG]
+ if ok {
+ cell.BG = bg
+ }
+ return cell
}
// Cursor returns the current position of the cursor.
-func (t *State) Cursor() (int, int) {
- return t.cur.x, t.cur.y
+func (t *State) Cursor() Cursor {
+ return t.cur
}
// CursorVisible returns the visible state of the cursor.
@@ -149,9 +166,9 @@ func (t *State) CursorVisible() bool {
return t.mode&ModeHide == 0
}
-// Mode tests if mode is currently set.
-func (t *State) Mode(mode ModeFlag) bool {
- return t.mode&mode != 0
+// Mode returns the current terminal mode.
+func (t *State) Mode() ModeFlag {
+ return t.mode
}
// Title returns the current title set via the tty.
@@ -186,7 +203,7 @@ func (t *State) saveCursor() {
func (t *State) restoreCursor() {
t.cur = t.curSaved
- t.moveTo(t.cur.x, t.cur.y)
+ t.moveTo(t.cur.X, t.cur.Y)
}
func (t *State) put(c rune) {
@@ -194,7 +211,7 @@ func (t *State) put(c rune) {
}
func (t *State) putTab(forward bool) {
- x := t.cur.x
+ x := t.cur.X
if forward {
if x == t.cols {
return
@@ -208,11 +225,11 @@ func (t *State) putTab(forward bool) {
for x--; x > 0 && !t.tabs[x]; x-- {
}
}
- t.moveTo(x, t.cur.y)
+ t.moveTo(x, t.cur.Y)
}
func (t *State) newline(firstCol bool) {
- y := t.cur.y
+ y := t.cur.Y
if y == t.bottom {
cur := t.cur
t.cur = t.defaultCursor()
@@ -224,7 +241,7 @@ func (t *State) newline(firstCol bool) {
if firstCol {
t.moveTo(0, y)
} else {
- t.moveTo(t.cur.x, y)
+ t.moveTo(t.cur.X, y)
}
}
@@ -240,8 +257,8 @@ var gfxCharTable = [62]rune{
'│', '≤', '≥', 'π', '≠', '£', '·', // x - ~
}
-func (t *State) setChar(c rune, attr *glyph, x, y int) {
- if attr.mode&attrGfx != 0 {
+func (t *State) setChar(c rune, attr *Glyph, x, y int) {
+ if attr.Mode&attrGfx != 0 {
if c >= 0x41 && c <= 0x7e && gfxCharTable[c-0x41] != 0 {
c = gfxCharTable[c-0x41]
}
@@ -249,21 +266,21 @@ func (t *State) setChar(c rune, attr *glyph, x, y int) {
t.changed |= ChangedScreen
t.dirty[y] = true
t.lines[y][x] = *attr
- t.lines[y][x].c = c
- //if t.options.BrightBold && attr.mode&attrBold != 0 && attr.fg < 8 {
- if attr.mode&attrBold != 0 && attr.fg < 8 {
- t.lines[y][x].fg = attr.fg + 8
+ t.lines[y][x].Char = c
+ //if t.options.BrightBold && attr.Mode&attrBold != 0 && attr.FG < 8 {
+ if attr.Mode&attrBold != 0 && attr.FG < 8 {
+ t.lines[y][x].FG = attr.FG + 8
}
- if attr.mode&attrReverse != 0 {
- t.lines[y][x].fg = attr.bg
- t.lines[y][x].bg = attr.fg
+ if attr.Mode&attrReverse != 0 {
+ t.lines[y][x].FG = attr.BG
+ t.lines[y][x].BG = attr.FG
}
}
-func (t *State) defaultCursor() cursor {
- c := cursor{}
- c.attr.fg = DefaultFG
- c.attr.bg = DefaultBG
+func (t *State) defaultCursor() Cursor {
+ c := Cursor{}
+ c.Attr.FG = DefaultFG
+ c.Attr.BG = DefaultBG
return c
}
@@ -291,7 +308,7 @@ func (t *State) resize(cols, rows int) bool {
if cols < 1 || rows < 1 {
return false
}
- slide := t.cur.y - rows + 1
+ slide := t.cur.Y - rows + 1
if slide > 0 {
copy(t.lines, t.lines[slide:slide+rows])
copy(t.altLines, t.altLines[slide:slide+rows])
@@ -329,7 +346,7 @@ func (t *State) resize(cols, rows int) bool {
t.cols = cols
t.rows = rows
t.setScroll(0, rows-1)
- t.moveTo(t.cur.x, t.cur.y)
+ t.moveTo(t.cur.X, t.cur.Y)
for i := 0; i < 2; i++ {
if mincols < cols && minrows > 0 {
t.clear(mincols, 0, cols-1, minrows-1)
@@ -357,8 +374,8 @@ func (t *State) clear(x0, y0, x1, y1 int) {
for y := y0; y <= y1; y++ {
t.dirty[y] = true
for x := x0; x <= x1; x++ {
- t.lines[y][x] = t.cur.attr
- t.lines[y][x].c = ' '
+ t.lines[y][x] = t.cur.Attr
+ t.lines[y][x].Char = ' '
}
}
}
@@ -368,7 +385,7 @@ func (t *State) clearAll() {
}
func (t *State) moveAbsTo(x, y int) {
- if t.cur.state&cursorOrigin != 0 {
+ if t.cur.State&cursorOrigin != 0 {
y += t.top
}
t.moveTo(x, y)
@@ -376,7 +393,7 @@ func (t *State) moveAbsTo(x, y int) {
func (t *State) moveTo(x, y int) {
var miny, maxy int
- if t.cur.state&cursorOrigin != 0 {
+ if t.cur.State&cursorOrigin != 0 {
miny = t.top
maxy = t.bottom
} else {
@@ -386,9 +403,9 @@ func (t *State) moveTo(x, y int) {
x = clamp(x, 0, t.cols-1)
y = clamp(y, miny, maxy)
t.changed |= ChangedScreen
- t.cur.state &^= cursorWrapNext
- t.cur.x = x
- t.cur.y = y
+ t.cur.State &^= cursorWrapNext
+ t.cur.X = x
+ t.cur.Y = y
}
func (t *State) swapScreen() {
@@ -492,9 +509,9 @@ func (t *State) setMode(priv bool, set bool, args []int) {
}
case 6: // DECOM - origin
if set {
- t.cur.state |= cursorOrigin
+ t.cur.State |= cursorOrigin
} else {
- t.cur.state &^= cursorOrigin
+ t.cur.State &^= cursorOrigin
}
t.moveAbsTo(0, 0)
case 7: // DECAWM - auto wrap
@@ -594,64 +611,80 @@ func (t *State) setAttr(attr []int) {
a := attr[i]
switch a {
case 0:
- t.cur.attr.mode &^= attrReverse | attrUnderline | attrBold | attrItalic | attrBlink
- t.cur.attr.fg = DefaultFG
- t.cur.attr.bg = DefaultBG
+ t.cur.Attr.Mode &^= attrReverse | attrUnderline | attrBold | attrItalic | attrBlink
+ t.cur.Attr.FG = DefaultFG
+ t.cur.Attr.BG = DefaultBG
case 1:
- t.cur.attr.mode |= attrBold
+ t.cur.Attr.Mode |= attrBold
case 3:
- t.cur.attr.mode |= attrItalic
+ t.cur.Attr.Mode |= attrItalic
case 4:
- t.cur.attr.mode |= attrUnderline
+ t.cur.Attr.Mode |= attrUnderline
case 5, 6: // slow, rapid blink
- t.cur.attr.mode |= attrBlink
+ t.cur.Attr.Mode |= attrBlink
case 7:
- t.cur.attr.mode |= attrReverse
+ t.cur.Attr.Mode |= attrReverse
case 21, 22:
- t.cur.attr.mode &^= attrBold
+ t.cur.Attr.Mode &^= attrBold
case 23:
- t.cur.attr.mode &^= attrItalic
+ t.cur.Attr.Mode &^= attrItalic
case 24:
- t.cur.attr.mode &^= attrUnderline
+ t.cur.Attr.Mode &^= attrUnderline
case 25, 26:
- t.cur.attr.mode &^= attrBlink
+ t.cur.Attr.Mode &^= attrBlink
case 27:
- t.cur.attr.mode &^= attrReverse
+ t.cur.Attr.Mode &^= attrReverse
case 38:
if i+2 < len(attr) && attr[i+1] == 5 {
i += 2
if between(attr[i], 0, 255) {
- t.cur.attr.fg = Color(attr[i])
+ t.cur.Attr.FG = Color(attr[i])
} else {
t.logf("bad fgcolor %d\n", attr[i])
}
+ } else if i+4 < len(attr) && attr[i+1] == 2 {
+ i += 4
+ r, g, b := attr[i-2], attr[i-1], attr[i]
+ if !between(r, 0, 255) || !between(g, 0, 255) || !between(b, 0, 255) {
+ t.logf("bad fg rgb color (%d,%d,%d)\n", r, g, b)
+ } else {
+ t.cur.Attr.FG = Color(r<<16 | g<<8 | b)
+ }
} else {
t.logf("gfx attr %d unknown\n", a)
}
case 39:
- t.cur.attr.fg = DefaultFG
+ t.cur.Attr.FG = DefaultFG
case 48:
if i+2 < len(attr) && attr[i+1] == 5 {
i += 2
if between(attr[i], 0, 255) {
- t.cur.attr.bg = Color(attr[i])
+ t.cur.Attr.BG = Color(attr[i])
} else {
t.logf("bad bgcolor %d\n", attr[i])
}
+ } else if i+4 < len(attr) && attr[i+1] == 2 {
+ i += 4
+ r, g, b := attr[i-2], attr[i-1], attr[i]
+ if !between(r, 0, 255) || !between(g, 0, 255) || !between(b, 0, 255) {
+ t.logf("bad bg rgb color (%d,%d,%d)\n", r, g, b)
+ } else {
+ t.cur.Attr.BG = Color(r<<16 | g<<8 | b)
+ }
} else {
t.logf("gfx attr %d unknown\n", a)
}
case 49:
- t.cur.attr.bg = DefaultBG
+ t.cur.Attr.BG = DefaultBG
default:
if between(a, 30, 37) {
- t.cur.attr.fg = Color(a - 30)
+ t.cur.Attr.FG = Color(a - 30)
} else if between(a, 40, 47) {
- t.cur.attr.bg = Color(a - 40)
+ t.cur.Attr.BG = Color(a - 40)
} else if between(a, 90, 97) {
- t.cur.attr.fg = Color(a - 90 + 8)
+ t.cur.Attr.FG = Color(a - 90 + 8)
} else if between(a, 100, 107) {
- t.cur.attr.bg = Color(a - 100 + 8)
+ t.cur.Attr.BG = Color(a - 100 + 8)
} else {
t.logf("gfx attr %d unknown\n", a)
}
@@ -660,46 +693,46 @@ func (t *State) setAttr(attr []int) {
}
func (t *State) insertBlanks(n int) {
- src := t.cur.x
+ src := t.cur.X
dst := src + n
size := t.cols - dst
t.changed |= ChangedScreen
- t.dirty[t.cur.y] = true
+ t.dirty[t.cur.Y] = true
if dst >= t.cols {
- t.clear(t.cur.x, t.cur.y, t.cols-1, t.cur.y)
+ t.clear(t.cur.X, t.cur.Y, t.cols-1, t.cur.Y)
} else {
- copy(t.lines[t.cur.y][dst:dst+size], t.lines[t.cur.y][src:src+size])
- t.clear(src, t.cur.y, dst-1, t.cur.y)
+ copy(t.lines[t.cur.Y][dst:dst+size], t.lines[t.cur.Y][src:src+size])
+ t.clear(src, t.cur.Y, dst-1, t.cur.Y)
}
}
func (t *State) insertBlankLines(n int) {
- if t.cur.y < t.top || t.cur.y > t.bottom {
+ if t.cur.Y < t.top || t.cur.Y > t.bottom {
return
}
- t.scrollDown(t.cur.y, n)
+ t.scrollDown(t.cur.Y, n)
}
func (t *State) deleteLines(n int) {
- if t.cur.y < t.top || t.cur.y > t.bottom {
+ if t.cur.Y < t.top || t.cur.Y > t.bottom {
return
}
- t.scrollUp(t.cur.y, n)
+ t.scrollUp(t.cur.Y, n)
}
func (t *State) deleteChars(n int) {
- src := t.cur.x + n
- dst := t.cur.x
+ src := t.cur.X + n
+ dst := t.cur.X
size := t.cols - src
t.changed |= ChangedScreen
- t.dirty[t.cur.y] = true
+ t.dirty[t.cur.Y] = true
if src >= t.cols {
- t.clear(t.cur.x, t.cur.y, t.cols-1, t.cur.y)
+ t.clear(t.cur.X, t.cur.Y, t.cols-1, t.cur.Y)
} else {
- copy(t.lines[t.cur.y][dst:dst+size], t.lines[t.cur.y][src:src+size])
- t.clear(t.cols-n, t.cur.y, t.cols-1, t.cur.y)
+ copy(t.lines[t.cur.Y][dst:dst+size], t.lines[t.cur.Y][src:src+size])
+ t.clear(t.cols-n, t.cur.Y, t.cols-1, t.cur.Y)
}
}
@@ -708,8 +741,8 @@ func (t *State) setTitle(title string) {
t.title = title
}
-func (t *State) Size() (rows int, cols int) {
- return t.rows, t.cols
+func (t *State) Size() (cols, rows int) {
+ return t.cols, t.rows
}
func (t *State) String() string {
@@ -719,8 +752,8 @@ func (t *State) String() string {
var view []rune
for y := 0; y < t.rows; y++ {
for x := 0; x < t.cols; x++ {
- c, _, _ := t.Cell(x, y)
- view = append(view, c)
+ attr := t.Cell(x, y)
+ view = append(view, attr.Char)
}
view = append(view, '\n')
}
diff --git a/str.go b/str.go
index c8ca50c..2a42b04 100644
--- a/str.go
+++ b/str.go
@@ -1,6 +1,9 @@
package vt10x
import (
+ "fmt"
+ "math"
+ "regexp"
"strconv"
"strings"
)
@@ -59,20 +62,77 @@ func (t *State) handleSTR() {
switch s.typ {
case ']': // OSC - operating system command
+ var p *string
switch d := s.arg(0, 0); d {
case 0, 1, 2:
title := s.argString(1, "")
if title != "" {
t.setTitle(title)
}
+ case 10:
+ if len(s.args) < 2 {
+ break
+ }
+
+ c := s.argString(1, "")
+ p := &c
+ if p != nil && *p == "?" {
+ t.oscColorResponse(int(DefaultFG), 10)
+ } else if err := t.setColorName(int(DefaultFG), p); err != nil {
+ t.logf("invalid foreground color: %s\n", maybe(p))
+ } else {
+ // TODO: redraw
+ }
+ case 11:
+ if len(s.args) < 2 {
+ break
+ }
+
+ c := s.argString(1, "")
+ p := &c
+ if p != nil && *p == "?" {
+ t.oscColorResponse(int(DefaultBG), 11)
+ } else if err := t.setColorName(int(DefaultBG), p); err != nil {
+ t.logf("invalid cursor color: %s\n", maybe(p))
+ } else {
+ // TODO: redraw
+ }
+ // case 12:
+ // if len(s.args) < 2 {
+ // break
+ // }
+
+ // c := s.argString(1, "")
+ // p := &c
+ // if p != nil && *p == "?" {
+ // t.oscColorResponse(int(DefaultCursor), 12)
+ // } else if err := t.setColorName(int(DefaultCursor), p); err != nil {
+ // t.logf("invalid background color: %s\n", p)
+ // } else {
+ // // TODO: redraw
+ // }
case 4: // color set
if len(s.args) < 3 {
break
}
- // setcolorname(s.arg(1, 0), s.argString(2, ""))
+
+ c := s.argString(2, "")
+ p = &c
+ fallthrough
case 104: // color reset
- // TODO: complain about invalid color, redraw, etc.
- // setcolorname(s.arg(1, 0), nil)
+ j := -1
+ if len(s.args) > 1 {
+ j = s.arg(1, 0)
+ }
+ if p != nil && *p == "?" { // report
+ t.osc4ColorResponse(j)
+ } else if err := t.setColorName(j, p); err != nil {
+ if !(d == 104 && len(s.args) <= 1) {
+ t.logf("invalid color j=%d, p=%s\n", j, maybe(p))
+ }
+ } else {
+ // TODO: redraw
+ }
default:
t.logf("unknown OSC command %d\n", d)
// TODO: s.dump()
@@ -92,3 +152,179 @@ func (t *State) handleSTR() {
// t.str.dump()
}
}
+
+func (t *State) setColorName(j int, p *string) error {
+ if !between(j, 0, 1<<24) {
+ return fmt.Errorf("invalid color value %d", j)
+ }
+
+ if p == nil {
+ // restore color
+ delete(t.colorOverride, Color(j))
+ } else {
+ // set color
+ r, g, b, err := parseColor(*p)
+ if err != nil {
+ return err
+ }
+ t.colorOverride[Color(j)] = Color(r<<16 | g<<8 | b)
+ }
+
+ return nil
+}
+
+func (t *State) oscColorResponse(j, num int) {
+ if j < 0 {
+ t.logf("failed to fetch osc color %d\n", j)
+ return
+ }
+
+ k, ok := t.colorOverride[Color(j)]
+ if ok {
+ j = int(k)
+ }
+
+ r, g, b := rgb(j)
+ t.w.Write([]byte(fmt.Sprintf("\033]%d;rgb:%02x%02x/%02x%02x/%02x%02x\007", num, r, r, g, g, b, b)))
+}
+
+func (t *State) osc4ColorResponse(j int) {
+ if j < 0 {
+ t.logf("failed to fetch osc4 color %d\n", j)
+ return
+ }
+
+ k, ok := t.colorOverride[Color(j)]
+ if ok {
+ j = int(k)
+ }
+
+ r, g, b := rgb(j)
+ t.w.Write([]byte(fmt.Sprintf("\033]4;%d;rgb:%02x%02x/%02x%02x/%02x%02x\007", j, r, r, g, g, b, b)))
+}
+
+func rgb(j int) (r, g, b int) {
+ return (j >> 16) & 0xff, (j >> 8) & 0xff, j & 0xff
+}
+
+var (
+ 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})$`)
+ HashPattern = regexp.MustCompile(`[\da-f]`)
+)
+
+func parseColor(p string) (r, g, b int, err error) {
+ if len(p) == 0 {
+ err = fmt.Errorf("empty color spec")
+ return
+ }
+
+ low := strings.ToLower(p)
+ if strings.HasPrefix(low, "rgb:") {
+ low = low[4:]
+ sm := RGBPattern.FindAllStringSubmatch(low, -1)
+ if len(sm) != 1 || len(sm[0]) == 0 {
+ err = fmt.Errorf("invalid rgb color spec: %s", p)
+ return
+ }
+ m := sm[0]
+
+ var base float64
+ if len(m[1]) > 0 {
+ base = 15
+ } else if len(m[4]) > 0 {
+ base = 255
+ } else if len(m[7]) > 0 {
+ base = 4095
+ } else {
+ base = 65535
+ }
+
+ r64, err := strconv.ParseInt(firstNonEmpty(m[1], m[4], m[7], m[10]), 16, 0)
+ if err != nil {
+ return r, g, b, err
+ }
+
+ g64, err := strconv.ParseInt(firstNonEmpty(m[2], m[5], m[8], m[11]), 16, 0)
+ if err != nil {
+ return r, g, b, err
+ }
+
+ b64, err := strconv.ParseInt(firstNonEmpty(m[3], m[6], m[9], m[12]), 16, 0)
+ if err != nil {
+ return r, g, b, err
+ }
+
+ r = int(math.Round(float64(r64) / base * 255))
+ g = int(math.Round(float64(g64) / base * 255))
+ b = int(math.Round(float64(b64) / base * 255))
+ return r, g, b, nil
+ } else if strings.HasPrefix(low, "#") {
+ low = low[1:]
+ m := HashPattern.FindAllString(low, -1)
+ if !oneOf(len(m), []int{3, 6, 9, 12}) {
+ err = fmt.Errorf("invalid hash color spec: %s", p)
+ return
+ }
+
+ adv := len(low) / 3
+ for i := 0; i < 3; i++ {
+ c, err := strconv.ParseInt(low[adv*i:adv*i+adv], 16, 0)
+ if err != nil {
+ return r, g, b, err
+ }
+
+ var v int64
+ switch adv {
+ case 1:
+ v = c << 4
+ case 2:
+ v = c
+ case 3:
+ v = c >> 4
+ default:
+ v = c >> 8
+ }
+
+ switch i {
+ case 0:
+ r = int(v)
+ case 1:
+ g = int(v)
+ case 2:
+ b = int(v)
+ }
+ }
+ return
+ } else {
+ err = fmt.Errorf("invalid color spec: %s", p)
+ return
+ }
+}
+
+func maybe(p *string) string {
+ if p == nil {
+ return "<nil>"
+ }
+ return *p
+}
+
+func firstNonEmpty(strs ...string) string {
+ if len(strs) == 0 {
+ return ""
+ }
+ for _, str := range strs {
+ if len(str) > 0 {
+ return str
+ }
+ }
+ return strs[len(strs)-1]
+}
+
+func oneOf(v int, is []int) bool {
+ for _, i := range is {
+ if v == i {
+ return true
+ }
+ }
+ return false
+}
diff --git a/str_test.go b/str_test.go
index 578f901..974f15e 100644
--- a/str_test.go
+++ b/str_test.go
@@ -13,3 +13,160 @@ func TestSTRParse(t *testing.T) {
t.Fatal("STR parse mismatch")
}
}
+
+func TestParseColor(t *testing.T) {
+ type testCase struct {
+ name string
+ input string
+ r, g, b int
+ }
+
+ for _, tc := range []testCase{
+ {
+ "rgb 4 bit zero",
+ "rgb:0/0/0",
+ 0, 0, 0,
+ },
+ {
+ "rgb 4 bit max",
+ "rgb:f/f/f",
+ 255, 255, 255,
+ },
+ {
+ "rgb 4 bit values",
+ "rgb:1/2/3",
+ 17, 34, 51,
+ },
+ {
+ "rgb 8 bit zero",
+ "rgb:00/00/00",
+ 0, 0, 0,
+ },
+ {
+ "rgb 8 bit max",
+ "rgb:ff/ff/ff",
+ 255, 255, 255,
+ },
+ {
+ "rgb 8 bit values",
+ "rgb:11/22/33",
+ 17, 34, 51,
+ },
+ {
+ "rgb 12 bit zero",
+ "rgb:000/000/000",
+ 0, 0, 0,
+ },
+ {
+ "rgb 12 bit max",
+ "rgb:fff/fff/fff",
+ 255, 255, 255,
+ },
+ {
+ "rgb 12 bit values",
+ "rgb:111/222/333",
+ 17, 34, 51,
+ },
+ {
+ "rgb 16 bit zero",
+ "rgb:0000/0000/0000",
+ 0, 0, 0,
+ },
+ {
+ "rgb 16 bit max",
+ "rgb:ffff/ffff/ffff",
+ 255, 255, 255,
+ },
+ {
+ "rgb 16 bit values",
+ "rgb:1111/2222/3333",
+ 17, 34, 51,
+ },
+ {
+ "rgb 16 bit values",
+ "rgb:1111/2222/3333",
+ 17, 34, 51,
+ },
+ {
+ "hash 4 bit zero",
+ "#000",
+ 0, 0, 0,
+ },
+ {
+ "hash 4 bit max",
+ "#fff",
+ 240, 240, 240,
+ },
+ {
+ "hash 4 bit values",
+ "#123",
+ 16, 32, 48,
+ },
+ {
+ "hash 8 bit zero",
+ "#000000",
+ 0, 0, 0,
+ },
+ {
+ "hash 8 bit max",
+ "#ffffff",
+ 255, 255, 255,
+ },
+ {
+ "hash 8 bit values",
+ "#112233",
+ 17, 34, 51,
+ },
+ {
+ "hash 12 bit zero",
+ "#000000000",
+ 0, 0, 0,
+ },
+ {
+ "hash 12 bit max",
+ "#fffffffff",
+ 255, 255, 255,
+ },
+ {
+ "hash 12 bit values",
+ "#111222333",
+ 17, 34, 51,
+ },
+ {
+ "hash 16 bit zero",
+ "#000000000000",
+ 0, 0, 0,
+ },
+ {
+ "hash 16 bit max",
+ "#ffffffffffff",
+ 255, 255, 255,
+ },
+ {
+ "hash 16 bit values",
+ "#111122223333",
+ 17, 34, 51,
+ },
+ {
+ "rgb upper case",
+ "RGB:0/A/F",
+ 0, 170, 255,
+ },
+ {
+ "hash upper case",
+ "#FFF",
+ 240, 240, 240,
+ },
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ r, g, b, err := parseColor(tc.input)
+ if err != nil {
+ t.Fatalf("failed to parse color: %s", err)
+ }
+
+ if r != tc.r || g != tc.g || b != tc.b {
+ t.Fatalf("expected (%d, %d, %d), got (%d, %d, %d)", tc.r, tc.g, tc.b, r, g, b)
+ }
+ })
+ }
+}
diff --git a/vt.go b/vt.go
new file mode 100644
index 0000000..c4e58dd
--- /dev/null
+++ b/vt.go
@@ -0,0 +1,89 @@
+package vt10x
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "io/ioutil"
+)
+
+// Terminal represents the virtual terminal emulator.
+type Terminal interface {
+ // View displays the virtual terminal.
+ View
+
+ // Write parses input and writes terminal changes to state.
+ io.Writer
+
+ // Parse blocks on read on pty or io.Reader, then parses sequences until
+ // buffer empties. State is locked as soon as first rune is read, and unlocked
+ // when buffer is empty.
+ Parse(bf *bufio.Reader) error
+}
+
+// View represents the view of the virtual terminal emulator.
+type View interface {
+ // String dumps the virtual terminal contents.
+ fmt.Stringer
+
+ // Size returns the size of the virtual terminal.
+ Size() (cols, rows int)
+
+ // Resize changes the size of the virtual terminal.
+ Resize(cols, rows int)
+
+ // Mode returns the current terminal mode.//
+ Mode() ModeFlag
+
+ // Title represents the title of the console window.
+ Title() string
+
+ // Cell returns the glyph containing the character code, foreground color, and
+ // background color at position (x, y) relative to the top left of the terminal.
+ Cell(x, y int) Glyph
+
+ // Cursor returns the current position of the cursor.
+ Cursor() Cursor
+
+ // CursorVisible returns the visible state of the cursor.
+ CursorVisible() bool
+
+ // Lock locks the state object's mutex.
+ Lock()
+
+ // Unlock resets change flags and unlocks the state object's mutex.
+ Unlock()
+}
+
+type TerminalOption func(*TerminalInfo)
+
+type TerminalInfo struct {
+ w io.Writer
+ cols, rows int
+}
+
+func WithWriter(w io.Writer) TerminalOption {
+ return func(info *TerminalInfo) {
+ info.w = w
+ }
+}
+
+func WithSize(cols, rows int) TerminalOption {
+ return func(info *TerminalInfo) {
+ info.cols = cols
+ info.rows = rows
+ }
+}
+
+// New returns a new virtual terminal emulator.
+func New(opts ...TerminalOption) Terminal {
+ info := TerminalInfo{
+ w: ioutil.Discard,
+ cols: 80,
+ rows: 24,
+ }
+ for _, opt := range opts {
+ opt(&info)
+ }
+ return newTerminal(info)
+}
diff --git a/vt_other.go b/vt_other.go
index 6d4b500..c9d364e 100644
--- a/vt_other.go
+++ b/vt_other.go
@@ -6,47 +6,34 @@ import (
"bufio"
"bytes"
"io"
- "os"
"unicode"
"unicode/utf8"
)
-// VT represents the virtual terminal emulator.
-type VT struct {
- dest *State
- rwc io.ReadWriteCloser
- br *bufio.Reader
- pty *os.File
+type terminal struct {
+ *State
}
-// Create initializes a virtual terminal emulator with the target state
-// and io.ReadWriteCloser input.
-func Create(state *State, rwc io.ReadWriteCloser) (*VT, error) {
- t := &VT{
- dest: state,
- rwc: rwc,
- }
- t.init()
- return t, nil
+func newTerminal(info TerminalInfo) *terminal {
+ t := &terminal{newState(info.w)}
+ t.init(info.cols, info.rows)
+ return t
}
-func (t *VT) init() {
- t.br = bufio.NewReader(t.rwc)
- t.dest.w = t.rwc
- t.dest.numlock = true
- t.dest.state = t.dest.parse
- t.dest.cur.attr.fg = DefaultFG
- t.dest.cur.attr.bg = DefaultBG
- t.Resize(80, 24)
- t.dest.reset()
+func (t *terminal) init(cols, rows int) {
+ t.numlock = true
+ t.state = t.parse
+ t.cur.Attr.FG = DefaultFG
+ t.cur.Attr.BG = DefaultBG
+ t.Resize(cols, rows)
+ t.reset()
}
-// Write parses input and writes terminal changes to state.
-func (t *VT) Write(p []byte) (int, error) {
+func (t *terminal) Write(p []byte) (int, error) {
var written int
r := bytes.NewReader(p)
- t.dest.lock()
- defer t.dest.unlock()
+ t.lock()
+ defer t.unlock()
for {
c, sz, err := r.ReadRune()
if err != nil {
@@ -61,51 +48,43 @@ func (t *VT) Write(p []byte) (int, error) {
// not enough bytes for a full rune
return written - 1, nil
}
- t.dest.logln("invalid utf8 sequence")
+ t.logln("invalid utf8 sequence")
continue
}
- t.dest.put(c)
+ t.put(c)
}
return written, nil
}
-// Close closes the io.ReadWriteCloser.
-func (t *VT) Close() error {
- return t.rwc.Close()
-}
-
-// Parse blocks on read on io.ReadWriteCloser, then parses sequences until
-// buffer empties. State is locked as soon as first rune is read, and unlocked
-// when buffer is empty.
// TODO: add tests for expected blocking behavior
-func (t *VT) Parse() error {
+func (t *terminal) Parse(br *bufio.Reader) error {
var locked bool
defer func() {
if locked {
- t.dest.unlock()
+ t.unlock()
}
}()
for {
- c, sz, err := t.br.ReadRune()
+ c, sz, err := br.ReadRune()
if err != nil {
return err
}
if c == unicode.ReplacementChar && sz == 1 {
- t.dest.logln("invalid utf8 sequence")
+ t.logln("invalid utf8 sequence")
break
}
if !locked {
- t.dest.lock()
+ t.lock()
locked = true
}
// put rune for parsing and update state
- t.dest.put(c)
+ t.put(c)
// break if our buffer is empty, or if buffer contains an
// incomplete rune.
- n := t.br.Buffered()
- if n == 0 || (n < 4 && !fullRuneBuffered(t.br)) {
+ n := br.Buffered()
+ if n == 0 || (n < 4 && !fullRuneBuffered(br)) {
break
}
}
@@ -121,9 +100,8 @@ func fullRuneBuffered(br *bufio.Reader) bool {
return utf8.FullRune(buf)
}
-// Resize reports new size to pty and updates state.
-func (t *VT) Resize(cols, rows int) {
- t.dest.lock()
- defer t.dest.unlock()
- _ = t.dest.resize(cols, rows)
+func (t *terminal) Resize(cols, rows int) {
+ t.lock()
+ defer t.unlock()
+ _ = t.resize(cols, rows)
}
diff --git a/vt_posix.go b/vt_posix.go
index 94e4d00..80644f4 100644
--- a/vt_posix.go
+++ b/vt_posix.go
@@ -10,41 +10,31 @@ import (
"unicode/utf8"
)
-// VT represents the virtual terminal emulator.
-type VT struct {
- dest *State
- rwc io.ReadWriteCloser
- br *bufio.Reader
+type terminal struct {
+ *State
}
-// Create initializes a virtual terminal emulator with the target state
-// and io.ReadWriteCloser input.
-func Create(state *State, rwc io.ReadWriteCloser) (*VT, error) {
- t := &VT{
- dest: state,
- rwc: rwc,
- }
- t.init()
- return t, nil
+func newTerminal(info TerminalInfo) *terminal {
+ t := &terminal{newState(info.w)}
+ t.init(info.cols, info.rows)
+ return t
}
-func (t *VT) init() {
- t.br = bufio.NewReader(t.rwc)
- t.dest.w = t.rwc
- t.dest.numlock = true
- t.dest.state = t.dest.parse
- t.dest.cur.attr.fg = DefaultFG
- t.dest.cur.attr.bg = DefaultBG
- t.Resize(80, 24)
- t.dest.reset()
+func (t *terminal) init(cols, rows int) {
+ t.numlock = true
+ t.state = t.parse
+ t.cur.Attr.FG = DefaultFG
+ t.cur.Attr.BG = DefaultBG
+ t.Resize(cols, rows)
+ t.reset()
}
// Write parses input and writes terminal changes to state.
-func (t *VT) Write(p []byte) (int, error) {
+func (t *terminal) Write(p []byte) (int, error) {
var written int
r := bytes.NewReader(p)
- t.dest.lock()
- defer t.dest.unlock()
+ t.lock()
+ defer t.unlock()
for {
c, sz, err := r.ReadRune()
if err != nil {
@@ -59,51 +49,43 @@ func (t *VT) Write(p []byte) (int, error) {
// not enough bytes for a full rune
return written - 1, nil
}
- t.dest.logln("invalid utf8 sequence")
+ t.logln("invalid utf8 sequence")
continue
}
- t.dest.put(c)
+ t.put(c)
}
return written, nil
}
-// Close closes the io.ReadWriteCloser.
-func (t *VT) Close() error {
- return t.rwc.Close()
-}
-
-// Parse blocks on read on pty or io.ReadCloser, then parses sequences until
-// buffer empties. State is locked as soon as first rune is read, and unlocked
-// when buffer is empty.
// TODO: add tests for expected blocking behavior
-func (t *VT) Parse() error {
+func (t *terminal) Parse(br *bufio.Reader) error {
var locked bool
defer func() {
if locked {
- t.dest.unlock()
+ t.unlock()
}
}()
for {
- c, sz, err := t.br.ReadRune()
+ c, sz, err := br.ReadRune()
if err != nil {
return err
}
if c == unicode.ReplacementChar && sz == 1 {
- t.dest.logln("invalid utf8 sequence")
+ t.logln("invalid utf8 sequence")
break
}
if !locked {
- t.dest.lock()
+ t.lock()
locked = true
}
// put rune for parsing and update state
- t.dest.put(c)
+ t.put(c)
// break if our buffer is empty, or if buffer contains an
// incomplete rune.
- n := t.br.Buffered()
- if n == 0 || (n < 4 && !fullRuneBuffered(t.br)) {
+ n := br.Buffered()
+ if n == 0 || (n < 4 && !fullRuneBuffered(br)) {
break
}
}
@@ -119,9 +101,8 @@ func fullRuneBuffered(br *bufio.Reader) bool {
return utf8.FullRune(buf)
}
-// Resize reports new size to pty and updates state.
-func (t *VT) Resize(cols, rows int) {
- t.dest.lock()
- defer t.dest.unlock()
- _ = t.dest.resize(cols, rows)
+func (t *terminal) Resize(cols, rows int) {
+ t.lock()
+ defer t.unlock()
+ _ = t.resize(cols, rows)
}
diff --git a/vt_test.go b/vt_test.go
index 3533248..505492a 100644
--- a/vt_test.go
+++ b/vt_test.go
@@ -1,53 +1,37 @@
package vt10x
import (
- "bufio"
- "fmt"
"io"
- "os"
- "regexp"
- "strconv"
"strings"
"testing"
-
- "github.com/stretchr/testify/require"
- "golang.org/x/crypto/ssh/terminal"
)
-func extractStr(t *State, x0, x1, row int) string {
+func extractStr(term Terminal, x0, x1, row int) string {
var s []rune
for i := x0; i <= x1; i++ {
- c, _, _ := t.Cell(i, row)
- s = append(s, c)
+ attr := term.Cell(i, row)
+ s = append(s, attr.Char)
}
return string(s)
}
func TestPlainChars(t *testing.T) {
- var st State
- term, err := Create(&st, nil)
- if err != nil {
- t.Fatal(err)
- }
+ term := New()
expected := "Hello world!"
- _, err = term.Write([]byte(expected))
+ _, err := term.Write([]byte(expected))
if err != nil && err != io.EOF {
t.Fatal(err)
}
- actual := extractStr(&st, 0, len(expected)-1, 0)
+ actual := extractStr(term, 0, len(expected)-1, 0)
if expected != actual {
t.Fatal(actual)
}
}
func TestNewline(t *testing.T) {
- var st State
- term, err := Create(&st, nil)
- if err != nil {
- t.Fatal(err)
- }
+ term := New()
expected := "Hello world!\n...and more."
- _, err = term.Write([]byte("\033[20h")) // set CRLF mode
+ _, err := term.Write([]byte("\033[20h")) // set CRLF mode
if err != nil && err != io.EOF {
t.Fatal(err)
}
@@ -57,85 +41,24 @@ func TestNewline(t *testing.T) {
}
split := strings.Split(expected, "\n")
- actual := extractStr(&st, 0, len(split[0])-1, 0)
+ actual := extractStr(term, 0, len(split[0])-1, 0)
actual += "\n"
- actual += extractStr(&st, 0, len(split[1])-1, 1)
+ actual += extractStr(term, 0, len(split[1])-1, 1)
if expected != actual {
t.Fatal(actual)
}
// A newline with a color set should not make the next line that color,
// which used to happen if it caused a scroll event.
+ st := (term.(*terminal))
st.moveTo(0, st.rows-1)
_, err = term.Write([]byte("\033[1;37m\n$ \033[m"))
if err != nil && err != io.EOF {
t.Fatal(err)
}
- _, fg, bg := st.Cell(st.Cursor())
- if fg != DefaultFG {
- t.Fatal(st.cur.x, st.cur.y, fg, bg)
- }
-}
-
-var (
- dsrPattern = regexp.MustCompile(`(\d+);(\d+)`)
-)
-
-type Coord struct {
- row int
- col int
-}
-
-func TestVTCPR(t *testing.T) {
- c, _, err := NewVT10XConsole()
- require.NoError(t, err)
- defer c.Close()
-
- go func() {
- c.ExpectEOF()
- }()
-
- coord, err := cpr(c.Tty())
- require.NoError(t, err)
- require.Equal(t, 1, coord.row)
- require.Equal(t, 1, coord.col)
-}
-
-// cpr is an example application that requests for the cursor position report.
-func cpr(tty *os.File) (*Coord, error) {
- oldState, err := terminal.MakeRaw(int(tty.Fd()))
- if err != nil {
- return nil, err
- }
-
- defer terminal.Restore(int(tty.Fd()), oldState)
-
- // ANSI escape sequence for DSR - Device Status Report
- // https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
- fmt.Fprint(tty, "\x1b[6n")
-
- // Reports the cursor position (CPR) to the application as (as though typed at
- // the keyboard) ESC[n;mR, where n is the row and m is the column.
- reader := bufio.NewReader(tty)
- text, err := reader.ReadSlice('R')
- if err != nil {
- return nil, err
- }
-
- matches := dsrPattern.FindStringSubmatch(string(text))
- if len(matches) != 3 {
- return nil, fmt.Errorf("incorrect number of matches: %d", len(matches))
- }
-
- col, err := strconv.Atoi(matches[2])
- if err != nil {
- return nil, err
+ cur := term.Cursor()
+ attr := term.Cell(cur.X, cur.Y)
+ if attr.FG != DefaultFG {
+ t.Fatal(st.cur.X, st.cur.Y, attr.FG, attr.BG)
}
-
- row, err := strconv.Atoi(matches[1])
- if err != nil {
- return nil, err
- }
-
- return &Coord{row, col}, nil
}