github.com/hugelgupf/u-root@v0.0.0-20191023214958-4807c632154c/pkg/less/less.go (about) 1 // Copyright 2018 the u-root Authors. All rights reserved 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package less 6 7 import ( 8 "fmt" 9 "io" 10 "log" 11 "os" 12 "regexp" 13 "sync" 14 15 "github.com/nsf/termbox-go" 16 "github.com/u-root/u-root/pkg/lineio" 17 "github.com/u-root/u-root/pkg/sortedmap" 18 ) 19 20 type size struct { 21 x int 22 y int 23 } 24 25 type Event int 26 27 const ( 28 // EventQuit requests an application exit. 29 EventQuit Event = iota 30 31 // EventRefresh requests a display refresh. 32 EventRefresh 33 ) 34 35 type Mode int 36 37 const ( 38 // ModeNormal is the standard mode, allowing file navigation. 39 ModeNormal Mode = iota 40 41 // ModeSearchEntry is search entry mode. Key presses are added 42 // to the search string. 43 ModeSearchEntry 44 ) 45 46 type Less struct { 47 // src is the source file being displayed. 48 src *lineio.LineReader 49 50 // tabStop is the number of spaces per tab. 51 tabStop int 52 53 // events is used to notify the main goroutine of events. 54 events chan Event 55 56 // mu locks the fields below. 57 mu sync.Mutex 58 59 // size is the size of the file display. 60 // There is a statusbar beneath the display. 61 size size 62 63 // line is the line number of the first line of the display. 64 line int64 65 66 // mode is the viewer mode. 67 mode Mode 68 69 // regexp is the search regexp specified by the user. 70 // Must only be modified by the event goroutine. 71 regexp string 72 73 // searchResults are the results for the current search. 74 // They should be highlighted. 75 searchResults *searchResults 76 } 77 78 // lastLine returns the last line on the display. It may be beyond the end 79 // of the file, if the file is short enough. 80 // mu must be held on call. 81 func (l *Less) lastLine() int64 { 82 return l.line + int64(l.size.y) - 1 83 } 84 85 // Scroll describes a scroll action. 86 type Scroll int 87 88 const ( 89 // ScrollTop goes to the first line. 90 ScrollTop Scroll = iota 91 // ScrollBottom goes to the last line. 92 ScrollBottom 93 // ScrollUp goes up one line. 94 ScrollUp 95 // ScrollDown goes down one line. 96 ScrollDown 97 // ScrollUpPage goes up one page full. 98 ScrollUpPage 99 // ScrollDownPage goes down one page full. 100 ScrollDownPage 101 // ScrollUpHalfPage goes up one half page full. 102 ScrollUpHalfPage 103 // ScrollDownHalfPage goes down one half page full. 104 ScrollDownHalfPage 105 ) 106 107 // scrollLine tries to scroll the display to the given line, 108 // but will not scroll beyond the first or last lines in the file. 109 // l.mu must be held when calling scrollLine. 110 func (l *Less) scrollLine(dest int64) { 111 var delta int64 112 if dest > l.line { 113 delta = 1 114 } else { 115 delta = -1 116 } 117 118 for l.line != dest && l.line+delta > 0 && l.src.LineExists(l.lastLine()+delta) { 119 l.line += delta 120 } 121 } 122 123 // scroll moves the display based on the passed scroll action, without 124 // going past the beginning or end of the file. 125 func (l *Less) scroll(s Scroll) { 126 l.mu.Lock() 127 defer l.mu.Unlock() 128 129 var dest int64 130 switch s { 131 case ScrollTop: 132 dest = 1 133 case ScrollBottom: 134 // Just try to go to int64 max. 135 dest = 0x7fffffffffffffff 136 case ScrollUp: 137 dest = l.line - 1 138 case ScrollDown: 139 dest = l.line + 1 140 case ScrollUpPage: 141 dest = l.line - int64(l.size.y) 142 case ScrollDownPage: 143 dest = l.line + int64(l.size.y) 144 case ScrollUpHalfPage: 145 dest = l.line - int64(l.size.y)/2 146 case ScrollDownHalfPage: 147 dest = l.line + int64(l.size.y)/2 148 } 149 150 l.scrollLine(dest) 151 } 152 153 func (l *Less) handleEvent(e termbox.Event) { 154 l.mu.Lock() 155 mode := l.mode 156 l.mu.Unlock() 157 158 if e.Type != termbox.EventKey { 159 return 160 } 161 162 c := e.Ch 163 k := e.Key 164 // Key is only valid is Ch is 0 165 if c != 0 { 166 k = 0 167 } 168 169 switch mode { 170 case ModeNormal: 171 switch { 172 case c == 'q': 173 l.events <- EventQuit 174 case c == 'j': 175 l.scroll(ScrollDown) 176 l.events <- EventRefresh 177 case c == 'k': 178 l.scroll(ScrollUp) 179 l.events <- EventRefresh 180 case c == 'g': 181 l.scroll(ScrollTop) 182 l.events <- EventRefresh 183 case c == 'G': 184 l.scroll(ScrollBottom) 185 l.events <- EventRefresh 186 case k == termbox.KeyPgup: 187 l.scroll(ScrollUpPage) 188 l.events <- EventRefresh 189 case k == termbox.KeyPgdn: 190 l.scroll(ScrollDownPage) 191 l.events <- EventRefresh 192 case k == termbox.KeyCtrlU: 193 l.scroll(ScrollUpHalfPage) 194 l.events <- EventRefresh 195 case k == termbox.KeyCtrlD: 196 l.scroll(ScrollDownHalfPage) 197 l.events <- EventRefresh 198 case c == '/': 199 l.mu.Lock() 200 l.mode = ModeSearchEntry 201 l.mu.Unlock() 202 l.events <- EventRefresh 203 case c == 'n': 204 l.mu.Lock() 205 if r, ok := l.searchResults.Next(l.line); ok { 206 l.scrollLine(r.line) 207 l.events <- EventRefresh 208 } 209 l.mu.Unlock() 210 case c == 'N': 211 l.mu.Lock() 212 if r, ok := l.searchResults.Prev(l.line); ok { 213 l.scrollLine(r.line) 214 l.events <- EventRefresh 215 } 216 l.mu.Unlock() 217 } 218 case ModeSearchEntry: 219 switch { 220 case k == termbox.KeyEnter: 221 r := l.search(l.regexp) 222 l.mu.Lock() 223 l.mode = ModeNormal 224 l.regexp = "" 225 l.searchResults = r 226 // Jump to nearest result 227 if r, ok := l.searchResults.Next(l.line); ok { 228 l.scrollLine(r.line) 229 } 230 l.mu.Unlock() 231 l.events <- EventRefresh 232 default: 233 l.mu.Lock() 234 l.regexp += string(c) 235 l.mu.Unlock() 236 l.events <- EventRefresh 237 } 238 } 239 } 240 241 func (l *Less) listenEvents() { 242 for { 243 e := termbox.PollEvent() 244 l.handleEvent(e) 245 } 246 } 247 248 // searchResult describes search matches on a single line. 249 type searchResult struct { 250 line int64 251 matches [][]int 252 err error 253 } 254 255 // matchesChar returns true if the search result contains a match for 256 // character index c. 257 func (s searchResult) matchesChar(c int) bool { 258 for _, match := range s.matches { 259 if len(match) < 2 { 260 continue 261 } 262 263 if c >= match[0] && c < match[1] { 264 return true 265 } 266 } 267 return false 268 } 269 270 type searchResults struct { 271 // mu locks the fields below. 272 mu sync.Mutex 273 274 // lines maps search results for a specific line to an index in results. 275 lines sortedmap.Map 276 277 // results contains the actual search results, in no particular order. 278 results []searchResult 279 } 280 281 func NewSearchResults() *searchResults { 282 return &searchResults{ 283 lines: sortedmap.NewMap(), 284 } 285 } 286 287 // Add adds a result. 288 func (s *searchResults) Add(r searchResult) { 289 s.mu.Lock() 290 defer s.mu.Unlock() 291 292 i := int64(len(s.results)) 293 s.results = append(s.results, r) 294 s.lines.Insert(r.line, i) 295 } 296 297 // Get finds the result for a specific line, returning ok if found 298 func (s *searchResults) Get(line int64) (searchResult, bool) { 299 s.mu.Lock() 300 defer s.mu.Unlock() 301 302 if i, ok := s.lines.Get(line); ok { 303 return s.results[i], true 304 } 305 306 return searchResult{}, false 307 } 308 309 // Next returns the search result for the nearest line after line, 310 // noninclusive, if one exists. 311 func (s *searchResults) Next(line int64) (searchResult, bool) { 312 s.mu.Lock() 313 defer s.mu.Unlock() 314 315 _, i, err := s.lines.NearestGreater(line) 316 if err != nil { 317 // Probably ErrNoSuchKey, aka none found. 318 return searchResult{}, false 319 } 320 321 return s.results[i], true 322 } 323 324 // Prev returns the search result for the nearest line before line, 325 // noninclusive, if one exists. 326 func (s *searchResults) Prev(line int64) (searchResult, bool) { 327 s.mu.Lock() 328 defer s.mu.Unlock() 329 330 // Search for line - 1, since it may be equal. 331 _, i, err := s.lines.NearestLessEqual(line - 1) 332 if err != nil { 333 // Probably ErrNoSuchKey, aka none found. 334 return searchResult{}, false 335 } 336 337 return s.results[i], true 338 } 339 340 func (l *Less) search(s string) *searchResults { 341 reg, err := regexp.Compile(s) 342 if err != nil { 343 // TODO(prattmic): display a better error 344 log.Printf("regexp failed to compile: %v", err) 345 return NewSearchResults() 346 } 347 348 resultChan := make(chan searchResult, 100) 349 350 searchLine := func(line int64) { 351 r, err := l.src.SearchLine(reg, line) 352 if err != nil { 353 r = nil 354 } 355 356 resultChan <- searchResult{ 357 line: line, 358 matches: r, 359 err: err, 360 } 361 } 362 363 nextLine := int64(1) 364 // Spawn initial search goroutines 365 for ; nextLine <= 5; nextLine++ { 366 go searchLine(nextLine) 367 } 368 369 results := NewSearchResults() 370 371 var count int64 372 373 waitResult := func() searchResult { 374 ret := <-resultChan 375 count++ 376 377 // Only store results with matches. 378 if len(ret.matches) > 0 { 379 results.Add(ret) 380 } 381 382 return ret 383 } 384 385 // Collect results, start searching next lines until we start 386 // hitting EOF. 387 for { 388 r := waitResult() 389 390 // We started hitting errors on a previous line, 391 // there is no reason to search later lines. 392 if r.err != nil { 393 break 394 } 395 396 go searchLine(nextLine) 397 nextLine++ 398 } 399 400 // Collect the remaining results. 401 for count < nextLine-1 { 402 waitResult() 403 } 404 405 return results 406 } 407 408 // statusBar renders the status bar. 409 // mu must be held on call. 410 func (l *Less) statusBar() { 411 // The statusbar is just below the display. 412 413 // Clear the statusbar 414 for i := 0; i < l.size.x; i++ { 415 termbox.SetCell(i, l.size.y, ' ', 0, 0) 416 } 417 418 switch l.mode { 419 case ModeNormal: 420 // Just a colon and a cursor 421 termbox.SetCell(0, l.size.y, ':', 0, 0) 422 termbox.SetCursor(1, l.size.y) 423 case ModeSearchEntry: 424 // / and search string 425 termbox.SetCell(0, l.size.y, '/', 0, 0) 426 for i, c := range l.regexp { 427 termbox.SetCell(1+i, l.size.y, c, 0, 0) 428 } 429 termbox.SetCursor(1+len(l.regexp), l.size.y) 430 } 431 } 432 433 // alignUp aligns n up to the next multiple of divisor. 434 func alignUp(n, divisor int) int { 435 return n + (divisor - (n % divisor)) 436 } 437 438 func (l *Less) refreshScreen() error { 439 l.mu.Lock() 440 defer l.mu.Unlock() 441 442 for y := 0; y < l.size.y; y++ { 443 buf := make([]byte, l.size.x) 444 line := l.line + int64(y) 445 446 _, err := l.src.ReadLine(buf, line) 447 // EOF just means the line was shorter than the display. 448 if err != nil && err != io.EOF { 449 return err 450 } 451 452 highlight, ok := l.searchResults.Get(line) 453 454 var displayColumn int 455 for i, c := range buf { 456 if displayColumn >= l.size.x { 457 break 458 } 459 460 fg := termbox.ColorDefault 461 bg := termbox.ColorDefault 462 463 // Highlight matches 464 if ok && highlight.matchesChar(i) { 465 fg = termbox.ColorBlack 466 bg = termbox.ColorWhite 467 } 468 469 if c == '\t' { 470 // Tabs align the display up to the next 471 // multiple of tabstop. 472 next := alignUp(displayColumn, l.tabStop) 473 474 // Clear the tab spaces 475 for j := displayColumn; j < next; j++ { 476 termbox.SetCell(j, y, ' ', 0, 0) 477 } 478 479 displayColumn = next 480 } else { 481 termbox.SetCell(displayColumn, y, rune(c), fg, bg) 482 displayColumn++ 483 } 484 } 485 } 486 487 l.statusBar() 488 489 termbox.Flush() 490 491 return nil 492 } 493 494 func (l *Less) Run() { 495 // Start populating the LineReader cache, to speed things up later. 496 go l.src.Populate() 497 498 go l.listenEvents() 499 500 err := l.refreshScreen() 501 if err != nil { 502 fmt.Fprintf(os.Stderr, "Failed to refresh screen: %v\n", err) 503 return 504 } 505 506 for { 507 e := <-l.events 508 509 switch e { 510 case EventQuit: 511 return 512 case EventRefresh: 513 err = l.refreshScreen() 514 if err != nil { 515 fmt.Fprintf(os.Stderr, "Failed to refresh screen: %v\n", err) 516 return 517 } 518 } 519 } 520 } 521 522 func NewLess(r io.ReaderAt, ts int) Less { 523 x, y := termbox.Size() 524 525 return Less{ 526 src: lineio.NewLineReader(r), 527 tabStop: ts, 528 // Save one line for statusbar. 529 size: size{x: x, y: y - 1}, 530 line: 1, 531 events: make(chan Event, 1), 532 mode: ModeNormal, 533 searchResults: NewSearchResults(), 534 } 535 }