blob: 2bd8eb8459bf54d9d17d0c1748a880442287eae2 [file] [log] [blame]
// Copyright 2017 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package display
import (
"bufio"
"errors"
"fmt"
"os"
"strconv"
"strings"
"golang.org/x/crypto/ssh/terminal"
)
// Term is used to control a terminal.
type Term interface {
// check verifies that the current terminal provides required functionality.
check() error
// newHandle returns a handle that can be used to perform a series of operations.
newHandle() termHandle
}
// termHandle is used to perform a series of operations on a terminal.
// When the handle is closed, the terminal's original state is restored.
// Most functions return the handle itself to simplify chaining commands.
type termHandle interface {
// close closes the handle (e.g. taking the terminal out of raw mode).
// It must be called on completion of the operations.
// It returns the first error encountered, if any.
close() error
// writeString writes text to the terminal starting at the current cursor position.
writeString(s string) termHandle
// reverse configures the terminal to write inverted text.
reverse() termHandle
// clearAttr clears all currently-set display attributes. This only refers to reverse.
clearAttr() termHandle
// cursorVisible shows or hides the cursor.
cursorVisible(vis bool) termHandle
// lineWrap controls if lines are automatically wrapped or not.
lineWrap(wrap bool) termHandle
// clearLine clears the entire current line.
clearLine() termHandle
// clearToBottom clears from the current row to the bottom row.
clearToBottom() termHandle
// scroll scrolls the current scroll region. Positive values push the existing
// text upwards.
scroll(lines int) termHandle
// setScrollRegion sets the region of the screen to vertically scroll.
// start and end are 1-indexed rows. A negative start or end resets the region
// so the whole screen will be scrolled.
setScrollRegion(start, end int) termHandle
// setCursorPos moves the cursor to the requested position. row and col are 1-indexed.
setCursorPos(row, col int) termHandle
// saveCursorPos saves the current cursor position for a later call to restoreCursorPos.
saveCursorPos() termHandle
// restoreCursorPos restores the cursor position from an earlier call to saveCursorPos.
restoreCursorPos() termHandle
// getCursorPos gets the current 1-indexed cursor coordinates from the terminal.
getCursorPos() (row, col int, err error)
// getSize returns the dimensions of the terminal window.
getSize() (rows, cols int, err error)
}
// VT100Term controls a VT100 terminal.
// TODO(derat): Extract escape sequences from the system terminfo database instead of hardcoding them.
type VT100Term struct{}
func (t *VT100Term) check() error {
if !terminal.IsTerminal(int(os.Stdin.Fd())) {
return errors.New("stdin isn't a terminal")
}
if os.Getenv("TMUX") != "" {
return errors.New("tmux doesn't support VT100 scrolling sequences")
}
return nil
}
func (t *VT100Term) newHandle() termHandle {
c := &vt100TermHandle{}
c.origState, c.rawErr = terminal.MakeRaw(int(os.Stdin.Fd()))
return c
}
type vt100TermHandle struct {
origState *terminal.State // state before going into raw mode
rawErr error // error seen when going into raw mode
opErr error // first error seen from some other operation
}
func (th *vt100TermHandle) close() error {
if th.rawErr != nil {
return th.rawErr
}
resErr := terminal.Restore(int(os.Stdin.Fd()), th.origState)
if th.opErr != nil {
return th.opErr
}
return resErr
}
func (th *vt100TermHandle) writeString(s string) termHandle {
if th.opErr == nil {
_, th.opErr = os.Stdout.WriteString(s)
}
return th
}
func (th *vt100TermHandle) writeEscSeq(s string) {
// Some occasionally-conflicting resources for escape codes:
// https://en.wikipedia.org/wiki/ANSI_escape_code
// http://www.termsys.demon.co.uk/vtansi.htm
// http://ascii-table.com/ansi-escape-sequences-vt-100.php
if th.opErr == nil {
_, th.opErr = os.Stdout.WriteString("\033[" + s)
}
}
func (th *vt100TermHandle) reverse() termHandle {
th.writeEscSeq("7m")
return th
}
func (th *vt100TermHandle) clearAttr() termHandle {
th.writeEscSeq("0m")
return th
}
func (th *vt100TermHandle) cursorVisible(vis bool) termHandle {
if vis {
th.writeEscSeq("?25h")
} else {
th.writeEscSeq("?25l")
}
return th
}
func (th *vt100TermHandle) lineWrap(wrap bool) termHandle {
if wrap {
th.writeEscSeq("?7h")
} else {
th.writeEscSeq("?7l")
}
return th
}
func (th *vt100TermHandle) clearLine() termHandle {
th.writeEscSeq("2K")
return th
}
func (th *vt100TermHandle) clearToBottom() termHandle {
th.writeEscSeq("J")
return th
}
func (th *vt100TermHandle) scroll(lines int) termHandle {
// TODO(derat): Find an alternative for tmux.
if lines > 0 {
th.writeEscSeq(fmt.Sprintf("%dS", lines))
} else if lines < 0 {
th.writeEscSeq(fmt.Sprintf("%dT", -lines))
}
return th
}
func (th *vt100TermHandle) setScrollRegion(start, end int) termHandle {
if start < 0 || end < 0 {
th.writeEscSeq("r")
} else {
th.writeEscSeq(fmt.Sprintf("%d;%dr", start, end))
}
return th
}
func (th *vt100TermHandle) setCursorPos(row, col int) termHandle {
th.writeEscSeq(fmt.Sprintf("%d;%df", row, col))
return th
}
func (th *vt100TermHandle) saveCursorPos() termHandle {
th.writeEscSeq("s")
return th
}
func (th *vt100TermHandle) restoreCursorPos() termHandle {
th.writeEscSeq("u")
return th
}
func (th *vt100TermHandle) readCursorPos() (row, col int, err error) {
b, err := bufio.NewReader(os.Stdin).ReadBytes('R')
if err != nil {
return 0, 0, err
}
if len(b) < 6 || b[0] != '\033' || b[1] != '[' {
return 0, 0, fmt.Errorf("didn't get expected escape code or data (read %v)", b)
}
p := strings.Split(string(b[2:len(b)-1]), ";")
if len(p) != 2 {
return 0, 0, fmt.Errorf("didn't get expected ROW;COLUMN data (read %v)", b)
}
d := make([]int, 2)
for i := 0; i < 2; i++ {
v, err := strconv.ParseInt(p[i], 10, 32)
if err != nil {
return 0, 0, fmt.Errorf("failed to parse cursor coordinate from %q", p[i])
}
d[i] = int(v)
}
return d[0], d[1], nil
}
func (th *vt100TermHandle) getCursorPos() (row, col int, err error) {
// When the terminal sees this, it should write "<ESC>[{ROW};{COLUMN}R".
if th.writeEscSeq("6n"); th.opErr != nil {
return 0, 0, err
}
row, col, th.opErr = th.readCursorPos()
return row, col, th.opErr
}
func (th *vt100TermHandle) getSize() (rows, cols int, err error) {
th.saveCursorPos()
defer th.restoreCursorPos()
// Try to warp way out to the bottom right and see where we end up.
th.setCursorPos(999, 999)
return th.getCursorPos()
}