github.com/oweisse/u-root@v0.0.0-20181109060735-d005ad25fef1/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 remaing 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  }