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  }