1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209
|
//go:build !windows
// +build !windows
package terminal
import (
"bufio"
"bytes"
"fmt"
"io"
"regexp"
"strconv"
)
var COORDINATE_SYSTEM_BEGIN Short = 1
var dsrPattern = regexp.MustCompile(`\x1b\[(\d+);(\d+)R$`)
type Cursor struct {
In FileReader
Out FileWriter
}
// Up moves the cursor n cells to up.
func (c *Cursor) Up(n int) error {
_, err := fmt.Fprintf(c.Out, "\x1b[%dA", n)
return err
}
// Down moves the cursor n cells to down.
func (c *Cursor) Down(n int) error {
_, err := fmt.Fprintf(c.Out, "\x1b[%dB", n)
return err
}
// Forward moves the cursor n cells to right.
func (c *Cursor) Forward(n int) error {
_, err := fmt.Fprintf(c.Out, "\x1b[%dC", n)
return err
}
// Back moves the cursor n cells to left.
func (c *Cursor) Back(n int) error {
_, err := fmt.Fprintf(c.Out, "\x1b[%dD", n)
return err
}
// NextLine moves cursor to beginning of the line n lines down.
func (c *Cursor) NextLine(n int) error {
if err := c.Down(1); err != nil {
return err
}
return c.HorizontalAbsolute(0)
}
// PreviousLine moves cursor to beginning of the line n lines up.
func (c *Cursor) PreviousLine(n int) error {
if err := c.Up(1); err != nil {
return err
}
return c.HorizontalAbsolute(0)
}
// HorizontalAbsolute moves cursor horizontally to x.
func (c *Cursor) HorizontalAbsolute(x int) error {
_, err := fmt.Fprintf(c.Out, "\x1b[%dG", x)
return err
}
// Show shows the cursor.
func (c *Cursor) Show() error {
_, err := fmt.Fprint(c.Out, "\x1b[?25h")
return err
}
// Hide hide the cursor.
func (c *Cursor) Hide() error {
_, err := fmt.Fprint(c.Out, "\x1b[?25l")
return err
}
// move moves the cursor to a specific x,y location.
func (c *Cursor) move(x int, y int) error {
_, err := fmt.Fprintf(c.Out, "\x1b[%d;%df", x, y)
return err
}
// Save saves the current position
func (c *Cursor) Save() error {
_, err := fmt.Fprint(c.Out, "\x1b7")
return err
}
// Restore restores the saved position of the cursor
func (c *Cursor) Restore() error {
_, err := fmt.Fprint(c.Out, "\x1b8")
return err
}
// for comparability purposes between windows
// in unix we need to print out a new line on some terminals
func (c *Cursor) MoveNextLine(cur *Coord, terminalSize *Coord) error {
if cur.Y == terminalSize.Y {
if _, err := fmt.Fprintln(c.Out); err != nil {
return err
}
}
return c.NextLine(1)
}
// Location returns the current location of the cursor in the terminal
func (c *Cursor) Location(buf *bytes.Buffer) (*Coord, error) {
// ANSI escape sequence for DSR - Device Status Report
// https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
if _, err := fmt.Fprint(c.Out, "\x1b[6n"); err != nil {
return nil, err
}
// There may be input in Stdin prior to CursorLocation so make sure we don't
// drop those bytes.
var loc []int
var match string
for loc == nil {
// 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(c.In)
text, err := reader.ReadSlice(byte('R'))
if err != nil {
return nil, err
}
loc = dsrPattern.FindStringIndex(string(text))
if loc == nil {
// After reading slice to byte 'R', the bufio Reader may have read more
// bytes into its internal buffer which will be discarded on next ReadSlice.
// We create a temporary buffer to read the remaining buffered slice and
// write them to output buffer.
buffered := make([]byte, reader.Buffered())
_, err = io.ReadFull(reader, buffered)
if err != nil {
return nil, err
}
// Stdin contains R that doesn't match DSR, so pass the bytes along to
// output buffer.
buf.Write(text)
buf.Write(buffered)
} else {
// Write the non-matching leading bytes to output buffer.
buf.Write(text[:loc[0]])
// Save the matching bytes to extract the row and column of the cursor.
match = string(text[loc[0]:loc[1]])
}
}
matches := dsrPattern.FindStringSubmatch(string(match))
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
}
row, err := strconv.Atoi(matches[1])
if err != nil {
return nil, err
}
return &Coord{Short(col), Short(row)}, nil
}
func (cur Coord) CursorIsAtLineEnd(size *Coord) bool {
return cur.X == size.X
}
func (cur Coord) CursorIsAtLineBegin() bool {
return cur.X == COORDINATE_SYSTEM_BEGIN
}
// Size returns the height and width of the terminal.
func (c *Cursor) Size(buf *bytes.Buffer) (*Coord, error) {
// the general approach here is to move the cursor to the very bottom
// of the terminal, ask for the current location and then move the
// cursor back where we started
// hide the cursor (so it doesn't blink when getting the size of the terminal)
c.Hide()
defer c.Show()
// save the current location of the cursor
c.Save()
defer c.Restore()
// move the cursor to the very bottom of the terminal
c.move(999, 999)
// ask for the current location
bottom, err := c.Location(buf)
if err != nil {
return nil, err
}
// since the bottom was calculated in the lower right corner, it
// is the dimensions we are looking for
return bottom, nil
}
|