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 }