github.com/markusbkk/elvish@v0.0.0-20231204143114-91dc52438621/pkg/cli/term/writer.go (about) 1 package term 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 ) 8 9 var logWriterDetail = false 10 11 // Writer represents the output to a terminal. 12 type Writer interface { 13 // Buffer returns the current buffer. 14 Buffer() *Buffer 15 // ResetBuffer resets the current buffer. 16 ResetBuffer() 17 // UpdateBuffer updates the terminal display to reflect current buffer. 18 UpdateBuffer(bufNoti, buf *Buffer, fullRefresh bool) error 19 // ClearScreen clears the terminal screen and places the cursor at the top 20 // left corner. 21 ClearScreen() 22 // ShowCursor shows the cursor. 23 ShowCursor() 24 // HideCursor hides the cursor. 25 HideCursor() 26 } 27 28 // writer renders the editor UI. 29 type writer struct { 30 file io.Writer 31 curBuf *Buffer 32 } 33 34 // NewWriter returns a Writer that writes VT100 sequences to the given io.Writer. 35 func NewWriter(f io.Writer) Writer { 36 return &writer{f, &Buffer{}} 37 } 38 39 func (w *writer) Buffer() *Buffer { 40 return w.curBuf 41 } 42 43 func (w *writer) ResetBuffer() { 44 w.curBuf = &Buffer{} 45 } 46 47 // deltaPos calculates the escape sequence needed to move the cursor from one 48 // position to another. It use relative movements to move to the destination 49 // line and absolute movement to move to the destination column. 50 func deltaPos(from, to Pos) []byte { 51 buf := new(bytes.Buffer) 52 if from.Line < to.Line { 53 // move down 54 fmt.Fprintf(buf, "\033[%dB", to.Line-from.Line) 55 } else if from.Line > to.Line { 56 // move up 57 fmt.Fprintf(buf, "\033[%dA", from.Line-to.Line) 58 } 59 fmt.Fprint(buf, "\r") 60 if to.Col > 0 { 61 fmt.Fprintf(buf, "\033[%dC", to.Col) 62 } 63 return buf.Bytes() 64 } 65 66 const ( 67 hideCursor = "\033[?25l" 68 showCursor = "\033[?25h" 69 ) 70 71 // UpdateBuffer updates the terminal display to reflect current buffer. 72 func (w *writer) UpdateBuffer(bufNoti, buf *Buffer, fullRefresh bool) error { 73 if buf.Width != w.curBuf.Width && w.curBuf.Lines != nil { 74 // Width change, force full refresh 75 w.curBuf.Lines = nil 76 fullRefresh = true 77 } 78 79 bytesBuf := new(bytes.Buffer) 80 81 bytesBuf.WriteString(hideCursor) 82 83 // Rewind cursor 84 if pLine := w.curBuf.Dot.Line; pLine > 0 { 85 fmt.Fprintf(bytesBuf, "\033[%dA", pLine) 86 } 87 bytesBuf.WriteString("\r") 88 89 if fullRefresh { 90 // Erase from here. We may be in the top right corner of the screen; if 91 // we simply do an erase here, tmux will save the current screen in the 92 // scrollback buffer (presumably as a heuristics to detect full-screen 93 // applications), but that is not something we want. So we write a space 94 // first, and then erase, before rewinding back. 95 // 96 // Source code for tmux behavior: 97 // https://github.com/tmux/tmux/blob/5f5f029e3b3a782dc616778739b2801b00b17c0e/screen-write.c#L1139 98 bytesBuf.WriteString(" \033[J\r") 99 } 100 101 // style of last written cell. 102 style := "" 103 104 switchStyle := func(newstyle string) { 105 if newstyle != style { 106 fmt.Fprintf(bytesBuf, "\033[0;%sm", newstyle) 107 style = newstyle 108 } 109 } 110 111 writeCells := func(cs []Cell) { 112 for _, c := range cs { 113 switchStyle(c.Style) 114 bytesBuf.WriteString(c.Text) 115 } 116 } 117 118 if bufNoti != nil { 119 if logWriterDetail { 120 logger.Printf("going to write %d lines of notifications", len(bufNoti.Lines)) 121 } 122 123 // Write notifications 124 for _, line := range bufNoti.Lines { 125 writeCells(line) 126 switchStyle("") 127 bytesBuf.WriteString("\033[K\n") 128 } 129 // TODO(xiaq): This is hacky; try to improve it. 130 if len(w.curBuf.Lines) > 0 { 131 w.curBuf.Lines = w.curBuf.Lines[1:] 132 } 133 } 134 135 if logWriterDetail { 136 logger.Printf("going to write %d lines, oldBuf had %d", len(buf.Lines), len(w.curBuf.Lines)) 137 } 138 139 for i, line := range buf.Lines { 140 if i > 0 { 141 bytesBuf.WriteString("\n") 142 } 143 var j int // First column where buf and oldBuf differ 144 // No need to update current line 145 if !fullRefresh && i < len(w.curBuf.Lines) { 146 var eq bool 147 if eq, j = CompareCells(line, w.curBuf.Lines[i]); eq { 148 continue 149 } 150 } 151 // Move to the first differing column if necessary. 152 firstCol := CellsWidth(line[:j]) 153 if firstCol != 0 { 154 fmt.Fprintf(bytesBuf, "\033[%dC", firstCol) 155 } 156 // Erase the rest of the line if necessary. 157 if !fullRefresh && i < len(w.curBuf.Lines) && j < len(w.curBuf.Lines[i]) { 158 switchStyle("") 159 bytesBuf.WriteString("\033[K") 160 } 161 writeCells(line[j:]) 162 } 163 if len(w.curBuf.Lines) > len(buf.Lines) && !fullRefresh { 164 // If the old buffer is higher, erase old content. 165 // Note that we cannot simply write \033[J, because if the cursor is 166 // just over the last column -- which is precisely the case if we have a 167 // rprompt, \033[J will also erase the last column. 168 switchStyle("") 169 bytesBuf.WriteString("\n\033[J\033[A") 170 } 171 switchStyle("") 172 cursor := buf.Cursor() 173 bytesBuf.Write(deltaPos(cursor, buf.Dot)) 174 175 // Show cursor. 176 bytesBuf.WriteString(showCursor) 177 178 if logWriterDetail { 179 logger.Printf("going to write %q", bytesBuf.String()) 180 } 181 182 _, err := w.file.Write(bytesBuf.Bytes()) 183 if err != nil { 184 return err 185 } 186 187 w.curBuf = buf 188 return nil 189 } 190 191 func (w *writer) HideCursor() { 192 fmt.Fprint(w.file, hideCursor) 193 } 194 195 func (w *writer) ShowCursor() { 196 fmt.Fprint(w.file, showCursor) 197 } 198 199 func (w *writer) ClearScreen() { 200 fmt.Fprint(w.file, 201 "\033[H", // move cursor to the top left corner 202 "\033[2J", // clear entire buffer 203 ) 204 }