github.com/attic-labs/noms@v0.0.0-20210827224422-e5fa29d95e8b/samples/go/decent/lib/termui.go (about)

     1  // See: https://github.com/attic-labs/noms/issues/3808
     2  // +build ignore
     3  
     4  // Copyright 2017 Attic Labs, Inc. All rights reserved.
     5  // Licensed under the Apache License, version 2.0:
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  
     8  package lib
     9  
    10  import (
    11  	"fmt"
    12  	"regexp"
    13  	"runtime"
    14  	"strings"
    15  
    16  	"github.com/attic-labs/noms/go/d"
    17  	"github.com/attic-labs/noms/go/datas"
    18  	"github.com/attic-labs/noms/go/types"
    19  	"github.com/attic-labs/noms/go/util/math"
    20  	"github.com/attic-labs/noms/samples/go/decent/dbg"
    21  	"github.com/jroimartin/gocui"
    22  )
    23  
    24  const (
    25  	allViews     = ""
    26  	usersView    = "users"
    27  	messageView  = "messages"
    28  	inputView    = "input"
    29  	linestofetch = 50
    30  
    31  	searchPrefix = "/s"
    32  	quitPrefix   = "/q"
    33  )
    34  
    35  type TermUI struct {
    36  	Gui      *gocui.Gui
    37  	InSearch bool
    38  	lines    []string
    39  	dp       *dataPager
    40  }
    41  
    42  var (
    43  	viewNames   = []string{usersView, messageView, inputView}
    44  	firstLayout = true
    45  )
    46  
    47  func CreateTermUI(events chan ChatEvent) *TermUI {
    48  	g, err := gocui.NewGui(gocui.Output256)
    49  	d.PanicIfError(err)
    50  
    51  	g.Highlight = true
    52  	g.SelFgColor = gocui.ColorGreen
    53  	g.Cursor = true
    54  
    55  	relayout := func(g *gocui.Gui) error {
    56  		return layout(g)
    57  	}
    58  	g.SetManagerFunc(relayout)
    59  
    60  	termUI := new(TermUI)
    61  	termUI.Gui = g
    62  
    63  	d.PanicIfError(g.SetKeybinding(allViews, gocui.KeyF1, gocui.ModNone, debugInfo(termUI)))
    64  	d.PanicIfError(g.SetKeybinding(allViews, gocui.KeyCtrlC, gocui.ModNone, quit))
    65  	d.PanicIfError(g.SetKeybinding(allViews, gocui.KeyCtrlC, gocui.ModAlt, quitWithStack))
    66  	d.PanicIfError(g.SetKeybinding(allViews, gocui.KeyTab, gocui.ModNone, nextView))
    67  	d.PanicIfError(g.SetKeybinding(messageView, gocui.KeyArrowUp, gocui.ModNone, arrowUp(termUI)))
    68  	d.PanicIfError(g.SetKeybinding(messageView, gocui.KeyArrowDown, gocui.ModNone, arrowDown(termUI)))
    69  	d.PanicIfError(g.SetKeybinding(inputView, gocui.KeyEnter, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) (err error) {
    70  		defer func() {
    71  			v.Clear()
    72  			v.SetCursor(0, 0)
    73  			msgView, err := g.View(messageView)
    74  			d.PanicIfError(err)
    75  			msgView.Title = "messages"
    76  			msgView.Autoscroll = true
    77  		}()
    78  		buf := strings.TrimSpace(v.Buffer())
    79  		if strings.HasPrefix(buf, searchPrefix) {
    80  			events <- ChatEvent{EventType: SearchEvent, Event: strings.TrimSpace(buf[len(searchPrefix):])}
    81  			return
    82  		}
    83  		if strings.HasPrefix(buf, quitPrefix) {
    84  			err = gocui.ErrQuit
    85  			return
    86  		}
    87  		events <- ChatEvent{EventType: InputEvent, Event: buf}
    88  		return
    89  	}))
    90  
    91  	return termUI
    92  }
    93  
    94  func (t *TermUI) Close() {
    95  	dbg.Debug("Closing gui")
    96  	t.Gui.Close()
    97  }
    98  
    99  func (t *TermUI) UpdateMessagesFromSync(ds datas.Dataset) {
   100  	if t.InSearch || !t.textScrolledToEnd() {
   101  		t.Gui.Execute(func(g *gocui.Gui) (err error) {
   102  			updateViewTitle(g, messageView, "messages (NEW!)")
   103  			return
   104  		})
   105  	} else {
   106  		t.UpdateMessagesAsync(ds, nil, nil)
   107  	}
   108  }
   109  
   110  func (t *TermUI) Layout() error {
   111  	return layout(t.Gui)
   112  }
   113  
   114  func layout(g *gocui.Gui) error {
   115  	maxX, maxY := g.Size()
   116  	if v, err := g.SetView(usersView, 0, 0, 25, maxY-1); err != nil {
   117  		if err != gocui.ErrUnknownView {
   118  			return err
   119  		}
   120  		v.Title = usersView
   121  		v.Wrap = false
   122  		v.Editable = false
   123  	}
   124  	if v, err := g.SetView(messageView, 25, 0, maxX-1, maxY-2-1); err != nil {
   125  		if err != gocui.ErrUnknownView {
   126  			return err
   127  		}
   128  		v.Title = messageView
   129  		v.Editable = false
   130  		v.Wrap = true
   131  		v.Autoscroll = true
   132  		return nil
   133  	}
   134  	if v, err := g.SetView(inputView, 25, maxY-2-1, maxX-1, maxY-1); err != nil {
   135  		if err != gocui.ErrUnknownView {
   136  			return err
   137  		}
   138  		v.Wrap = true
   139  		v.Editable = true
   140  		v.Autoscroll = true
   141  	}
   142  	if firstLayout {
   143  		firstLayout = false
   144  		g.SetCurrentView(inputView)
   145  		dbg.Debug("started up")
   146  	}
   147  	return nil
   148  }
   149  
   150  func (t *TermUI) UpdateMessages(ds datas.Dataset, filterIds *types.Map, terms []string) error {
   151  	defer dbg.BoxF("updateMessages")()
   152  
   153  	t.ResetAuthors(ds)
   154  	v, err := t.Gui.View(messageView)
   155  	d.PanicIfError(err)
   156  	v.Clear()
   157  	t.lines = []string{}
   158  	v.SetOrigin(0, 0)
   159  	_, winHeight := v.Size()
   160  
   161  	if t.dp != nil {
   162  		t.dp.Close()
   163  	}
   164  
   165  	doneChan := make(chan struct{})
   166  	msgMap, msgKeyChan, err := ListMessages(ds, filterIds, doneChan)
   167  	d.PanicIfError(err)
   168  	t.dp = NewDataPager(ds, msgKeyChan, doneChan, msgMap, terms)
   169  	t.lines, _ = t.dp.Prepend(t.lines, math.MaxInt(linestofetch, winHeight+10))
   170  
   171  	for _, s := range t.lines {
   172  		fmt.Fprintf(v, "%s\n", s)
   173  	}
   174  	return nil
   175  }
   176  
   177  func (t *TermUI) ResetAuthors(ds datas.Dataset) {
   178  	v, err := t.Gui.View(usersView)
   179  	d.PanicIfError(err)
   180  	v.Clear()
   181  	for _, u := range GetAuthors(ds) {
   182  		fmt.Fprintln(v, u)
   183  	}
   184  }
   185  
   186  func (t *TermUI) UpdateMessagesAsync(ds datas.Dataset, sids *types.Map, terms []string) {
   187  	t.Gui.Execute(func(_ *gocui.Gui) error {
   188  		err := t.UpdateMessages(ds, sids, terms)
   189  		d.PanicIfError(err)
   190  		return nil
   191  	})
   192  }
   193  
   194  func (t *TermUI) scrollView(v *gocui.View, dy int) {
   195  	// Get the size and position of the view.
   196  	lineCnt := len(t.lines)
   197  	_, windowHeight := v.Size()
   198  	ox, oy := v.Origin()
   199  	cx, cy := v.Cursor()
   200  
   201  	// maxCy will either be the height of the screen - 1, or in the case that
   202  	// the there aren't enough lines to fill the screen, it will be the
   203  	// lineCnt - origin
   204  	newCy := cy + dy
   205  	maxCy := math.MinInt(lineCnt-oy, windowHeight-1)
   206  
   207  	// If the newCy doesn't require scrolling, then just move the cursor.
   208  	if newCy >= 0 && newCy < maxCy {
   209  		v.MoveCursor(cx, dy, false)
   210  		return
   211  	}
   212  
   213  	// If the cursor is already at the bottom of the screen and there are no
   214  	// lines left to scroll up, then we're at the bottom.
   215  	if newCy >= maxCy && oy >= lineCnt-windowHeight {
   216  		// Set autoscroll to normal again.
   217  		v.Autoscroll = true
   218  	} else {
   219  		// The cursor is already at the bottom or top of the screen so scroll
   220  		// the text
   221  		v.Autoscroll = false
   222  		v.SetOrigin(ox, oy+dy)
   223  	}
   224  }
   225  
   226  func quit(_ *gocui.Gui, _ *gocui.View) error {
   227  	dbg.Debug("QUITTING #####")
   228  	return gocui.ErrQuit
   229  }
   230  
   231  func quitWithStack(_ *gocui.Gui, _ *gocui.View) error {
   232  	dbg.Debug("QUITTING WITH STACK")
   233  	stacktrace := make([]byte, 1024*1024)
   234  	length := runtime.Stack(stacktrace, true)
   235  	dbg.Debug(string(stacktrace[:length]))
   236  	return gocui.ErrQuit
   237  }
   238  
   239  func arrowUp(t *TermUI) func(*gocui.Gui, *gocui.View) error {
   240  	return func(_ *gocui.Gui, v *gocui.View) error {
   241  		lineCnt := len(t.lines)
   242  		ox, oy := v.Origin()
   243  		if oy == 0 {
   244  			var ok bool
   245  			t.lines, ok = t.dp.Prepend(t.lines, linestofetch)
   246  			if ok {
   247  				v.Clear()
   248  				for _, s := range t.lines {
   249  					fmt.Fprintf(v, "%s\n", s)
   250  				}
   251  				c1 := len(t.lines)
   252  				v.SetOrigin(ox, c1-lineCnt)
   253  			}
   254  		}
   255  		t.scrollView(v, -1)
   256  		return nil
   257  	}
   258  }
   259  
   260  func arrowDown(t *TermUI) func(*gocui.Gui, *gocui.View) error {
   261  	return func(_ *gocui.Gui, v *gocui.View) error {
   262  		t.scrollView(v, 1)
   263  		return nil
   264  	}
   265  }
   266  
   267  func debugInfo(t *TermUI) func(*gocui.Gui, *gocui.View) error {
   268  	return func(g *gocui.Gui, _ *gocui.View) error {
   269  		msgView, _ := g.View(messageView)
   270  		w, h := msgView.Size()
   271  		dbg.Debug("info, window size:(%d, %d), lineCnt: %d", w, h, len(t.lines))
   272  		cx, cy := msgView.Cursor()
   273  		ox, oy := msgView.Origin()
   274  		dbg.Debug("info, origin: (%d,%d), cursor: (%d,%d)", ox, oy, cx, cy)
   275  		dbg.Debug("info, view buffer:\n%s", highlightTerms(viewBuffer(msgView), t.dp.terms))
   276  		return nil
   277  	}
   278  }
   279  
   280  func viewBuffer(v *gocui.View) string {
   281  	buf := strings.TrimSpace(v.ViewBuffer())
   282  	if len(buf) > 0 && buf[len(buf)-1] != byte('\n') {
   283  		buf = buf + "\n"
   284  	}
   285  	return buf
   286  }
   287  
   288  func nextView(g *gocui.Gui, v *gocui.View) (err error) {
   289  	nextName := nextViewName(v.Name())
   290  	if _, err = g.SetCurrentView(nextName); err != nil {
   291  		return
   292  	}
   293  	_, err = g.SetViewOnTop(nextName)
   294  	return
   295  }
   296  
   297  func nextViewName(currentView string) string {
   298  	for i, viewname := range viewNames {
   299  		if currentView == viewname {
   300  			return viewNames[(i+1)%len(viewNames)]
   301  		}
   302  	}
   303  	return viewNames[0]
   304  }
   305  
   306  func (t *TermUI) textScrolledToEnd() bool {
   307  	v, err := t.Gui.View(messageView)
   308  	if err != nil {
   309  		// doubt this will ever happen, if it does just assume we're at bottom
   310  		return true
   311  	}
   312  	_, oy := v.Origin()
   313  	_, h := v.Size()
   314  	lc := len(t.lines)
   315  	dbg.Debug("textScrolledToEnd, oy: %d, h: %d, lc: %d, lc-oy: %d, res: %t", oy, h, lc, lc-oy, lc-oy <= h)
   316  	return lc-oy <= h
   317  }
   318  
   319  func updateViewTitle(g *gocui.Gui, viewname, title string) (err error) {
   320  	v, err := g.View(viewname)
   321  	if err != nil {
   322  		return
   323  	}
   324  	v.Title = title
   325  	return
   326  }
   327  
   328  var bgColors, fgColors = genColors()
   329  
   330  func genColors() ([]string, []string) {
   331  	bg, fg := []string{}, []string{}
   332  	for i := 1; i <= 9; i++ {
   333  		// skip dark blue & white
   334  		if i != 4 && i != 7 {
   335  			bg = append(bg, fmt.Sprintf("\x1b[48;5;%dm\x1b[30m%%s\x1b[0m", i))
   336  			fg = append(fg, fmt.Sprintf("\x1b[38;5;%dm%%s\x1b[0m", i))
   337  		}
   338  	}
   339  	return bg, fg
   340  }
   341  
   342  func colorTerm(color int, s string, background bool) string {
   343  	c := fgColors[color]
   344  	if background {
   345  		c = bgColors[color]
   346  	}
   347  	return fmt.Sprintf(c, s)
   348  }
   349  
   350  func highlightTerms(s string, terms []string) string {
   351  	for i, t := range terms {
   352  		color := i % len(fgColors)
   353  		re := regexp.MustCompile(fmt.Sprintf("(?i)%s", regexp.QuoteMeta(t)))
   354  		s = re.ReplaceAllStringFunc(s, func(s string) string {
   355  			return colorTerm(color, s, false)
   356  		})
   357  	}
   358  	return s
   359  }