github.com/xyproto/u-root@v6.0.1-0.20200302025726-5528e0c77a3c+incompatible/cmds/core/elvish/edit/tty/writer.go (about)

     1  package tty
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"os"
     7  
     8  	"github.com/u-root/u-root/cmds/core/elvish/edit/ui"
     9  )
    10  
    11  var logWriterDetail = false
    12  
    13  type Writer interface {
    14  	// CurrentBuffer returns the current buffer.
    15  	CurrentBuffer() *ui.Buffer
    16  	// ResetCurrentBuffer resets the current buffer.
    17  	ResetCurrentBuffer()
    18  	// CommitBuffer updates the terminal display to reflect current buffer.
    19  	CommitBuffer(bufNoti, buf *ui.Buffer, fullRefresh bool) error
    20  }
    21  
    22  // writer renders the editor UI.
    23  type writer struct {
    24  	file   *os.File
    25  	curBuf *ui.Buffer
    26  }
    27  
    28  func NewWriter(f *os.File) Writer {
    29  	return &writer{f, &ui.Buffer{}}
    30  }
    31  
    32  // CurrentBuffer returns the current buffer.
    33  func (w *writer) CurrentBuffer() *ui.Buffer {
    34  	return w.curBuf
    35  }
    36  
    37  // ResetCurrentBuffer resets the current buffer.
    38  func (w *writer) ResetCurrentBuffer() {
    39  	w.curBuf = &ui.Buffer{}
    40  }
    41  
    42  // deltaPos calculates the escape sequence needed to move the cursor from one
    43  // position to another. It use relative movements to move to the destination
    44  // line and absolute movement to move to the destination column.
    45  func deltaPos(from, to ui.Pos) []byte {
    46  	buf := new(bytes.Buffer)
    47  	if from.Line < to.Line {
    48  		// move down
    49  		fmt.Fprintf(buf, "\033[%dB", to.Line-from.Line)
    50  	} else if from.Line > to.Line {
    51  		// move up
    52  		fmt.Fprintf(buf, "\033[%dA", from.Line-to.Line)
    53  	}
    54  	fmt.Fprint(buf, "\r")
    55  	if to.Col > 0 {
    56  		fmt.Fprintf(buf, "\033[%dC", to.Col)
    57  	}
    58  	return buf.Bytes()
    59  }
    60  
    61  // CommitBuffer updates the terminal display to reflect current buffer.
    62  func (w *writer) CommitBuffer(bufNoti, buf *ui.Buffer, fullRefresh bool) error {
    63  	if buf.Width != w.curBuf.Width && w.curBuf.Lines != nil {
    64  		// Width change, force full refresh
    65  		w.curBuf.Lines = nil
    66  		fullRefresh = true
    67  	}
    68  
    69  	bytesBuf := new(bytes.Buffer)
    70  
    71  	// Hide cursor.
    72  	bytesBuf.WriteString("\033[?25l")
    73  
    74  	// Rewind cursor
    75  	if pLine := w.curBuf.Dot.Line; pLine > 0 {
    76  		fmt.Fprintf(bytesBuf, "\033[%dA", pLine)
    77  	}
    78  	bytesBuf.WriteString("\r")
    79  
    80  	if fullRefresh {
    81  		// Do an erase.
    82  		bytesBuf.WriteString("\033[J")
    83  	}
    84  
    85  	// style of last written cell.
    86  	style := ""
    87  
    88  	switchStyle := func(newstyle string) {
    89  		if newstyle != style {
    90  			fmt.Fprintf(bytesBuf, "\033[0;%sm", newstyle)
    91  			style = newstyle
    92  		}
    93  	}
    94  
    95  	writeCells := func(cs []ui.Cell) {
    96  		for _, c := range cs {
    97  			if c.Width > 0 {
    98  				switchStyle(c.Style)
    99  			}
   100  			bytesBuf.WriteString(c.Text)
   101  		}
   102  	}
   103  
   104  	if bufNoti != nil {
   105  		if logWriterDetail {
   106  			logger.Printf("going to write %d lines of notifications", len(bufNoti.Lines))
   107  		}
   108  
   109  		// Write notifications
   110  		for _, line := range bufNoti.Lines {
   111  			writeCells(line)
   112  			switchStyle("")
   113  			bytesBuf.WriteString("\033[K\n")
   114  		}
   115  		// XXX Hacky.
   116  		if len(w.curBuf.Lines) > 0 {
   117  			w.curBuf.Lines = w.curBuf.Lines[1:]
   118  		}
   119  	}
   120  
   121  	if logWriterDetail {
   122  		logger.Printf("going to write %d lines, oldBuf had %d", len(buf.Lines), len(w.curBuf.Lines))
   123  	}
   124  
   125  	for i, line := range buf.Lines {
   126  		if i > 0 {
   127  			bytesBuf.WriteString("\n")
   128  		}
   129  		var j int // First column where buf and oldBuf differ
   130  		// No need to update current line
   131  		if !fullRefresh && i < len(w.curBuf.Lines) {
   132  			var eq bool
   133  			if eq, j = ui.CompareCells(line, w.curBuf.Lines[i]); eq {
   134  				continue
   135  			}
   136  		}
   137  		// Move to the first differing column if necessary.
   138  		firstCol := ui.CellsWidth(line[:j])
   139  		if firstCol != 0 {
   140  			fmt.Fprintf(bytesBuf, "\033[%dC", firstCol)
   141  		}
   142  		// Erase the rest of the line if necessary.
   143  		if !fullRefresh && i < len(w.curBuf.Lines) && j < len(w.curBuf.Lines[i]) {
   144  			switchStyle("")
   145  			bytesBuf.WriteString("\033[K")
   146  		}
   147  		writeCells(line[j:])
   148  	}
   149  	if len(w.curBuf.Lines) > len(buf.Lines) && !fullRefresh {
   150  		// If the old buffer is higher, erase old content.
   151  		// Note that we cannot simply write \033[J, because if the cursor is
   152  		// just over the last column -- which is precisely the case if we have a
   153  		// rprompt, \033[J will also erase the last column.
   154  		switchStyle("")
   155  		bytesBuf.WriteString("\n\033[J\033[A")
   156  	}
   157  	switchStyle("")
   158  	cursor := buf.Cursor()
   159  	bytesBuf.Write(deltaPos(cursor, buf.Dot))
   160  
   161  	// Show cursor.
   162  	bytesBuf.WriteString("\033[?25h")
   163  
   164  	if logWriterDetail {
   165  		logger.Printf("going to write %q", bytesBuf.String())
   166  	}
   167  
   168  	_, err := w.file.Write(bytesBuf.Bytes())
   169  	if err != nil {
   170  		return err
   171  	}
   172  
   173  	w.curBuf = buf
   174  	return nil
   175  }