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
|
package term
import (
"bytes"
"fmt"
"io"
)
var logWriterDetail = false
// Writer represents the output to a terminal.
type Writer interface {
// Buffer returns the current buffer.
Buffer() *Buffer
// ResetBuffer resets the current buffer.
ResetBuffer()
// UpdateBuffer updates the terminal display to reflect current buffer.
UpdateBuffer(bufNoti, buf *Buffer, fullRefresh bool) error
// ClearScreen clears the terminal screen and places the cursor at the top
// left corner.
ClearScreen()
// ShowCursor shows the cursor.
ShowCursor()
// HideCursor hides the cursor.
HideCursor()
}
// writer renders the editor UI.
type writer struct {
file io.Writer
curBuf *Buffer
}
// NewWriter returns a Writer that writes VT100 sequences to the given io.Writer.
func NewWriter(f io.Writer) Writer {
return &writer{f, &Buffer{}}
}
func (w *writer) Buffer() *Buffer {
return w.curBuf
}
func (w *writer) ResetBuffer() {
w.curBuf = &Buffer{}
}
// deltaPos calculates the escape sequence needed to move the cursor from one
// position to another. It use relative movements to move to the destination
// line and absolute movement to move to the destination column.
func deltaPos(from, to Pos) []byte {
buf := new(bytes.Buffer)
if from.Line < to.Line {
// move down
fmt.Fprintf(buf, "\033[%dB", to.Line-from.Line)
} else if from.Line > to.Line {
// move up
fmt.Fprintf(buf, "\033[%dA", from.Line-to.Line)
}
fmt.Fprint(buf, "\r")
if to.Col > 0 {
fmt.Fprintf(buf, "\033[%dC", to.Col)
}
return buf.Bytes()
}
const (
hideCursor = "\033[?25l"
showCursor = "\033[?25h"
)
// UpdateBuffer updates the terminal display to reflect current buffer.
func (w *writer) UpdateBuffer(bufNoti, buf *Buffer, fullRefresh bool) error {
if buf.Width != w.curBuf.Width && w.curBuf.Lines != nil {
// Width change, force full refresh
w.curBuf.Lines = nil
fullRefresh = true
}
bytesBuf := new(bytes.Buffer)
bytesBuf.WriteString(hideCursor)
// Rewind cursor
if pLine := w.curBuf.Dot.Line; pLine > 0 {
fmt.Fprintf(bytesBuf, "\033[%dA", pLine)
}
bytesBuf.WriteString("\r")
if fullRefresh {
// Erase from here. We may be in the top right corner of the screen; if
// we simply do an erase here, tmux will save the current screen in the
// scrollback buffer (presumably as a heuristics to detect full-screen
// applications), but that is not something we want. So we write a space
// first, and then erase, before rewinding back.
//
// Source code for tmux behavior:
// https://github.com/tmux/tmux/blob/5f5f029e3b3a782dc616778739b2801b00b17c0e/screen-write.c#L1139
bytesBuf.WriteString(" \033[J\r")
}
// style of last written cell.
style := ""
switchStyle := func(newstyle string) {
if newstyle != style {
fmt.Fprintf(bytesBuf, "\033[0;%sm", newstyle)
style = newstyle
}
}
writeCells := func(cs []Cell) {
for _, c := range cs {
switchStyle(c.Style)
bytesBuf.WriteString(c.Text)
}
}
if bufNoti != nil {
if logWriterDetail {
logger.Printf("going to write %d lines of notifications", len(bufNoti.Lines))
}
// Write notifications
for _, line := range bufNoti.Lines {
writeCells(line)
switchStyle("")
bytesBuf.WriteString("\033[K\n")
}
// TODO(xiaq): This is hacky; try to improve it.
if len(w.curBuf.Lines) > 0 {
w.curBuf.Lines = w.curBuf.Lines[1:]
}
}
if logWriterDetail {
logger.Printf("going to write %d lines, oldBuf had %d", len(buf.Lines), len(w.curBuf.Lines))
}
for i, line := range buf.Lines {
if i > 0 {
bytesBuf.WriteString("\n")
}
var j int // First column where buf and oldBuf differ
// No need to update current line
if !fullRefresh && i < len(w.curBuf.Lines) {
var eq bool
if eq, j = CompareCells(line, w.curBuf.Lines[i]); eq {
continue
}
}
// Move to the first differing column if necessary.
firstCol := CellsWidth(line[:j])
if firstCol != 0 {
fmt.Fprintf(bytesBuf, "\033[%dC", firstCol)
}
// Erase the rest of the line if necessary.
if !fullRefresh && i < len(w.curBuf.Lines) && j < len(w.curBuf.Lines[i]) {
switchStyle("")
bytesBuf.WriteString("\033[K")
}
writeCells(line[j:])
}
if len(w.curBuf.Lines) > len(buf.Lines) && !fullRefresh {
// If the old buffer is higher, erase old content.
// Note that we cannot simply write \033[J, because if the cursor is
// just over the last column -- which is precisely the case if we have a
// rprompt, \033[J will also erase the last column.
switchStyle("")
bytesBuf.WriteString("\n\033[J\033[A")
}
switchStyle("")
cursor := buf.Cursor()
bytesBuf.Write(deltaPos(cursor, buf.Dot))
// Show cursor.
bytesBuf.WriteString(showCursor)
if logWriterDetail {
logger.Printf("going to write %q", bytesBuf.String())
}
_, err := w.file.Write(bytesBuf.Bytes())
if err != nil {
return err
}
w.curBuf = buf
return nil
}
func (w *writer) HideCursor() {
fmt.Fprint(w.file, hideCursor)
}
func (w *writer) ShowCursor() {
fmt.Fprint(w.file, showCursor)
}
func (w *writer) ClearScreen() {
fmt.Fprint(w.file,
"\033[H", // move cursor to the top left corner
"\033[2J", // clear entire buffer
)
}
|