github.com/hazelops/ize@v1.1.12-0.20230915191306-97d7c0e48f11/pkg/terminal/display.go (about)

     1  package terminal
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"strings"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/briandowns/spinner"
    13  	"github.com/containerd/console"
    14  	"github.com/lab47/vterm/parser"
    15  	"github.com/lab47/vterm/screen"
    16  	"github.com/lab47/vterm/state"
    17  	"github.com/morikuni/aec"
    18  )
    19  
    20  var spinnerSet = spinner.CharSets[11]
    21  
    22  type DisplayEntry struct {
    23  	d       *Display
    24  	line    uint
    25  	index   int
    26  	indent  int
    27  	spinner bool
    28  	text    string
    29  	status  string
    30  
    31  	body []string
    32  
    33  	next *DisplayEntry
    34  }
    35  
    36  type Display struct {
    37  	mu      sync.Mutex
    38  	Entries []*DisplayEntry
    39  
    40  	w       io.Writer
    41  	newEnt  chan *DisplayEntry
    42  	updates chan *DisplayEntry
    43  	resize  chan struct{} // sent to when an entry has resized itself.
    44  	line    uint
    45  	width   int
    46  
    47  	wg       sync.WaitGroup
    48  	spinning int
    49  }
    50  
    51  func NewDisplay(ctx context.Context, w io.Writer) *Display {
    52  	d := &Display{
    53  		w:       w,
    54  		width:   80,
    55  		updates: make(chan *DisplayEntry),
    56  		resize:  make(chan struct{}),
    57  		newEnt:  make(chan *DisplayEntry),
    58  	}
    59  
    60  	if f, ok := w.(*os.File); ok {
    61  		if c, err := console.ConsoleFromFile(f); err == nil {
    62  			if sz, err := c.Size(); err == nil {
    63  				if sz.Width >= 10 {
    64  					d.width = int(sz.Width) - 1
    65  				}
    66  			}
    67  		}
    68  	}
    69  
    70  	d.wg.Add(1)
    71  	go func() {
    72  		defer d.wg.Done()
    73  		d.Display(ctx)
    74  	}()
    75  
    76  	return d
    77  }
    78  
    79  func (d *Display) Close() error {
    80  	d.wg.Wait()
    81  	return nil
    82  }
    83  
    84  func (d *Display) flushAll() {
    85  	d.mu.Lock()
    86  	defer d.mu.Unlock()
    87  
    88  	for range d.Entries {
    89  		fmt.Fprintln(d.w, "")
    90  	}
    91  
    92  	d.line = uint(len(d.Entries))
    93  }
    94  
    95  func (d *Display) renderEntry(ent *DisplayEntry, spin int) {
    96  	b := aec.EmptyBuilder
    97  
    98  	diff := d.line - ent.line
    99  
   100  	text := strings.TrimRight(ent.text, " \t\n")
   101  
   102  	if len(text) >= d.width {
   103  		text = text[:d.width-1]
   104  	}
   105  
   106  	prefix := ""
   107  	if ent.spinner {
   108  		prefix = spinnerSet[spin] + " "
   109  	}
   110  
   111  	var statusColor *aec.Builder
   112  	if ent.status != "" {
   113  		icon, ok := statusIcons[ent.status]
   114  		if !ok {
   115  			icon = ent.status
   116  		}
   117  
   118  		if len(prefix) > 0 {
   119  			prefix = prefix + " " + icon + " "
   120  		} else {
   121  			prefix = icon + " "
   122  		}
   123  
   124  		if codes, ok := colorStatus[ent.status]; ok {
   125  			statusColor = b.With(codes...)
   126  		}
   127  	}
   128  
   129  	line := fmt.Sprintf("%s%s%s",
   130  		b.
   131  			Up(diff).
   132  			Column(0).
   133  			EraseLine(aec.EraseModes.All).
   134  			ANSI,
   135  		prefix,
   136  		text,
   137  	)
   138  
   139  	if statusColor != nil {
   140  		line = statusColor.ANSI.Apply(line)
   141  	}
   142  
   143  	fmt.Fprint(d.w, line)
   144  
   145  	for _, body := range ent.body {
   146  		fmt.Fprintf(d.w, "%s%s",
   147  			b.
   148  				Down(1).
   149  				Column(0).
   150  				ANSI,
   151  			body,
   152  		)
   153  		diff--
   154  	}
   155  
   156  	fmt.Fprintf(d.w, "%s",
   157  		b.
   158  			Down(diff).
   159  			Column(0).
   160  			ANSI,
   161  	)
   162  }
   163  
   164  func (d *Display) Display(ctx context.Context) {
   165  	// d.flushAll()
   166  
   167  	ticker := time.NewTicker(time.Second / 6)
   168  
   169  	var spin int
   170  
   171  	for {
   172  		select {
   173  		case <-ctx.Done():
   174  			return
   175  		case <-ticker.C:
   176  			spin++
   177  			if spin >= len(spinnerSet) {
   178  				spin = 0
   179  			}
   180  
   181  			d.mu.Lock()
   182  			update := d.spinning > 0
   183  
   184  			if !update {
   185  				d.mu.Unlock()
   186  				continue
   187  			}
   188  
   189  			for _, ent := range d.Entries {
   190  				if !ent.spinner {
   191  					continue
   192  				}
   193  
   194  				d.renderEntry(ent, spin)
   195  			}
   196  
   197  			d.mu.Unlock()
   198  		case ent := <-d.newEnt:
   199  			d.mu.Lock()
   200  			ent.line = d.line
   201  			d.Entries = append(d.Entries, ent)
   202  			d.line++
   203  			d.line += uint(len(ent.body))
   204  			fmt.Fprintln(d.w, "")
   205  			for i := 0; i < len(ent.body); i++ {
   206  				fmt.Fprintln(d.w, "")
   207  			}
   208  
   209  			d.mu.Unlock()
   210  
   211  		case ent := <-d.updates:
   212  			d.mu.Lock()
   213  			d.renderEntry(ent, spin)
   214  			d.mu.Unlock()
   215  		case <-d.resize:
   216  			d.mu.Lock()
   217  
   218  			var newLine uint
   219  
   220  			for _, ent := range d.Entries {
   221  				newLine++
   222  				newLine += uint(len(ent.body))
   223  			}
   224  
   225  			diff := newLine - d.line
   226  
   227  			// TODO should we support shrinking?
   228  			if diff > 0 {
   229  				// Pad down
   230  				for i := uint(0); i < diff; i++ {
   231  					fmt.Fprintln(d.w, "")
   232  				}
   233  
   234  				d.line = newLine
   235  
   236  				var cnt uint
   237  
   238  				for _, ent := range d.Entries {
   239  					ent.line = cnt
   240  					cnt++
   241  					cnt += uint(len(ent.body))
   242  
   243  					d.renderEntry(ent, spin)
   244  				}
   245  			}
   246  
   247  			d.mu.Unlock()
   248  		}
   249  	}
   250  }
   251  
   252  func (d *Display) NewStatus(indent int) *DisplayEntry {
   253  	de := &DisplayEntry{
   254  		d:      d,
   255  		indent: indent,
   256  	}
   257  
   258  	d.newEnt <- de
   259  
   260  	return de
   261  }
   262  
   263  func (d *Display) NewStatusWithBody(indent, lines int) *DisplayEntry {
   264  	de := &DisplayEntry{
   265  		d:      d,
   266  		indent: indent,
   267  		body:   make([]string, lines),
   268  	}
   269  
   270  	d.newEnt <- de
   271  
   272  	return de
   273  }
   274  
   275  func (e *DisplayEntry) StartSpinner() {
   276  	e.d.mu.Lock()
   277  
   278  	e.spinner = true
   279  	e.d.spinning++
   280  
   281  	e.d.mu.Unlock()
   282  
   283  	e.d.updates <- e
   284  }
   285  
   286  func (e *DisplayEntry) StopSpinner() {
   287  	e.d.mu.Lock()
   288  
   289  	e.spinner = false
   290  	e.d.spinning--
   291  
   292  	e.d.mu.Unlock()
   293  
   294  	e.d.updates <- e
   295  }
   296  
   297  func (e *DisplayEntry) SetStatus(status string) {
   298  	e.d.mu.Lock()
   299  	defer e.d.mu.Unlock()
   300  
   301  	e.status = status
   302  }
   303  
   304  func (e *DisplayEntry) Update(str string, args ...interface{}) {
   305  	e.d.mu.Lock()
   306  	e.text = fmt.Sprintf(str, args...)
   307  	e.d.mu.Unlock()
   308  
   309  	e.d.updates <- e
   310  }
   311  
   312  func (e *DisplayEntry) SetBody(line int, data string) {
   313  	e.d.mu.Lock()
   314  
   315  	var resize bool
   316  
   317  	if line >= len(e.body) {
   318  		nb := make([]string, line+1)
   319  
   320  		for i, s := range e.body {
   321  			nb[i] = s
   322  		}
   323  
   324  		e.body = nb
   325  		resize = true
   326  	}
   327  
   328  	e.body[line] = data
   329  	e.d.mu.Unlock()
   330  
   331  	if resize {
   332  		e.d.resize <- struct{}{}
   333  	}
   334  
   335  	e.d.updates <- e
   336  }
   337  
   338  type Term struct {
   339  	ent    *DisplayEntry
   340  	scr    *screen.Screen
   341  	w      io.Writer
   342  	ctx    context.Context
   343  	cancel func()
   344  
   345  	output [][]rune
   346  
   347  	wg       sync.WaitGroup
   348  	parseErr error
   349  }
   350  
   351  func (t *Term) DamageDone(r state.Rect, cr screen.CellReader) error {
   352  	for row := r.Start.Row; row <= r.End.Row; row++ {
   353  		for col := r.Start.Col; col <= r.End.Col; col++ {
   354  			cell := cr.GetCell(row, col)
   355  
   356  			if cell == nil {
   357  				t.output[row][col] = ' '
   358  			} else {
   359  				val, _ := cell.Value()
   360  
   361  				if val == 0 {
   362  					t.output[row][col] = ' '
   363  				} else {
   364  					t.output[row][col] = val
   365  				}
   366  			}
   367  		}
   368  	}
   369  
   370  	for row := r.Start.Row; row <= r.End.Row; row++ {
   371  		b := aec.EmptyBuilder
   372  		blue := b.LightBlueF()
   373  		t.ent.SetBody(row, fmt.Sprintf(" │ %s%s%s", blue.ANSI, string(t.output[row]), aec.Reset))
   374  	}
   375  
   376  	return nil
   377  }
   378  
   379  func (t *Term) MoveCursor(p state.Pos) error {
   380  	// Ignore it.
   381  	return nil
   382  }
   383  
   384  func (t *Term) SetTermProp(attr state.TermAttr, val interface{}) error {
   385  	// Ignore it.
   386  	return nil
   387  }
   388  
   389  func (t *Term) Output(data []byte) error {
   390  	// Ignore it.
   391  	return nil
   392  }
   393  
   394  func (t *Term) StringEvent(kind string, data []byte) error {
   395  	// Ignore them.
   396  	return nil
   397  }
   398  
   399  func NewTerm(ctx context.Context, d *DisplayEntry, height, width int) (*Term, error) {
   400  	term := &Term{
   401  		ent:    d,
   402  		output: make([][]rune, height),
   403  	}
   404  
   405  	for i := range term.output {
   406  		term.output[i] = make([]rune, width)
   407  	}
   408  
   409  	scr, err := screen.NewScreen(height, width, term)
   410  	if err != nil {
   411  		return nil, err
   412  	}
   413  
   414  	term.scr = scr
   415  
   416  	st, err := state.NewState(height, width, scr)
   417  	if err != nil {
   418  		return nil, err
   419  	}
   420  
   421  	r, w, err := os.Pipe()
   422  	if err != nil {
   423  		return nil, err
   424  	}
   425  
   426  	term.w = w
   427  
   428  	prs, err := parser.NewParser(r, st)
   429  	if err != nil {
   430  		return nil, err
   431  	}
   432  
   433  	term.ctx, term.cancel = context.WithCancel(ctx)
   434  
   435  	term.wg.Add(1)
   436  	go func() {
   437  		defer term.wg.Done()
   438  
   439  		err := prs.Drive(term.ctx)
   440  		if err != nil && err != context.Canceled {
   441  			term.parseErr = err
   442  		}
   443  	}()
   444  
   445  	return term, nil
   446  }
   447  
   448  func (t *Term) Write(b []byte) (int, error) {
   449  	return t.w.Write(b)
   450  }
   451  
   452  func (t *Term) Close() error {
   453  	t.cancel()
   454  	t.wg.Wait()
   455  	return t.parseErr
   456  }