2019-06-10 22:50:31 +03:00
|
|
|
package m
|
|
|
|
|
2019-06-11 19:29:30 +03:00
|
|
|
import (
|
|
|
|
"fmt"
|
2019-06-16 11:02:19 +03:00
|
|
|
"log"
|
2019-06-16 22:54:25 +03:00
|
|
|
"math"
|
2019-06-11 19:29:30 +03:00
|
|
|
"os"
|
2019-06-29 19:29:37 +03:00
|
|
|
"regexp"
|
2019-06-11 19:29:30 +03:00
|
|
|
|
|
|
|
"github.com/gdamore/tcell"
|
|
|
|
)
|
|
|
|
|
2019-06-10 22:50:31 +03:00
|
|
|
// Pager is the main on-screen pager
|
2019-06-11 16:31:26 +03:00
|
|
|
type _Pager struct {
|
2019-06-16 11:02:19 +03:00
|
|
|
reader Reader
|
2019-06-12 22:55:09 +03:00
|
|
|
screen tcell.Screen
|
2019-06-15 10:23:53 +03:00
|
|
|
quit bool
|
2019-06-12 22:55:09 +03:00
|
|
|
firstLineOneBased int
|
2019-06-28 23:41:29 +03:00
|
|
|
|
2019-06-29 19:29:37 +03:00
|
|
|
isSearching bool
|
|
|
|
searchString string
|
|
|
|
searchPattern *regexp.Regexp
|
2019-06-10 22:50:31 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewPager creates a new Pager
|
2019-06-16 11:02:19 +03:00
|
|
|
func NewPager(r Reader) *_Pager {
|
2019-06-11 16:31:26 +03:00
|
|
|
return &_Pager{
|
2019-06-12 22:55:09 +03:00
|
|
|
reader: r,
|
2019-06-15 10:23:53 +03:00
|
|
|
quit: false,
|
2019-06-12 22:55:09 +03:00
|
|
|
firstLineOneBased: 1,
|
2019-06-10 22:50:31 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-16 21:57:03 +03:00
|
|
|
func (p *_Pager) _AddLine(logger *log.Logger, lineNumber int, line string) {
|
2019-06-29 19:29:37 +03:00
|
|
|
tokens := TokensFromString(logger, line)
|
|
|
|
|
|
|
|
plainString := ""
|
|
|
|
for _, token := range tokens {
|
|
|
|
plainString += string(token.Rune)
|
|
|
|
}
|
|
|
|
matchRanges := GetMatchRanges(plainString, p.searchPattern)
|
|
|
|
|
|
|
|
for pos, token := range tokens {
|
|
|
|
style := token.Style
|
|
|
|
if matchRanges.InRange(pos) {
|
|
|
|
// FIXME: This doesn't work if the style is already reversed
|
|
|
|
style = style.Reverse(true)
|
|
|
|
}
|
|
|
|
|
|
|
|
p.screen.SetContent(pos, lineNumber, token.Rune, nil, style)
|
2019-06-12 08:07:13 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-28 23:41:29 +03:00
|
|
|
func (p *_Pager) _AddSearchFooter() {
|
|
|
|
_, height := p.screen.Size()
|
|
|
|
|
|
|
|
pos := 0
|
2019-06-29 12:24:03 +03:00
|
|
|
for _, token := range "Search: " + p.searchString {
|
2019-06-28 23:41:29 +03:00
|
|
|
p.screen.SetContent(pos, height-1, token, nil, tcell.StyleDefault)
|
|
|
|
pos++
|
|
|
|
}
|
2019-06-29 12:20:48 +03:00
|
|
|
|
2019-06-29 12:24:03 +03:00
|
|
|
// Add a cursor
|
|
|
|
p.screen.SetContent(pos, height-1, ' ', nil, tcell.StyleDefault.Reverse(true))
|
2019-06-28 23:41:29 +03:00
|
|
|
}
|
|
|
|
|
2019-06-16 21:57:03 +03:00
|
|
|
func (p *_Pager) _AddLines(logger *log.Logger) {
|
2019-06-18 22:58:17 +03:00
|
|
|
width, height := p.screen.Size()
|
2019-06-12 22:55:09 +03:00
|
|
|
wantedLineCount := height - 1
|
2019-06-11 19:52:38 +03:00
|
|
|
|
2019-06-12 22:55:09 +03:00
|
|
|
lines := p.reader.GetLines(p.firstLineOneBased, wantedLineCount)
|
2019-06-12 08:07:13 +03:00
|
|
|
|
2019-06-12 22:55:09 +03:00
|
|
|
// If we're asking for past-the-end lines, the Reader will clip for us,
|
|
|
|
// and we should adapt to that. Otherwise if you scroll 100 lines past
|
|
|
|
// the end, you'll then have to scroll 100 lines up again before the
|
|
|
|
// display starts scrolling visibly.
|
|
|
|
p.firstLineOneBased = lines.firstLineOneBased
|
|
|
|
|
|
|
|
for screenLineNumber, line := range lines.lines {
|
2019-06-16 21:57:03 +03:00
|
|
|
p._AddLine(logger, screenLineNumber, line)
|
2019-06-11 19:52:38 +03:00
|
|
|
}
|
2019-06-14 08:08:20 +03:00
|
|
|
|
2019-06-28 23:41:29 +03:00
|
|
|
if p.isSearching {
|
|
|
|
p._AddSearchFooter()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-06-18 22:58:17 +03:00
|
|
|
pos := 0
|
|
|
|
footerStyle := tcell.StyleDefault.Reverse(true)
|
2019-06-28 23:41:29 +03:00
|
|
|
for _, token := range lines.statusText + " Press ESC / q to exit, '/' to search" {
|
2019-06-18 22:58:17 +03:00
|
|
|
p.screen.SetContent(pos, height-1, token, nil, footerStyle)
|
|
|
|
pos++
|
2019-06-14 08:08:20 +03:00
|
|
|
}
|
|
|
|
|
2019-06-18 22:58:17 +03:00
|
|
|
for ; pos < width; pos++ {
|
|
|
|
p.screen.SetContent(pos, height-1, ' ', nil, footerStyle)
|
2019-06-14 08:08:20 +03:00
|
|
|
}
|
2019-06-12 08:07:13 +03:00
|
|
|
}
|
|
|
|
|
2019-06-16 21:57:03 +03:00
|
|
|
func (p *_Pager) _Redraw(logger *log.Logger) {
|
2019-06-11 22:09:57 +03:00
|
|
|
p.screen.Clear()
|
2019-06-11 19:52:38 +03:00
|
|
|
|
2019-06-16 21:57:03 +03:00
|
|
|
p._AddLines(logger)
|
2019-06-11 19:52:38 +03:00
|
|
|
|
2019-06-16 21:33:18 +03:00
|
|
|
p.screen.Show()
|
2019-06-11 19:52:38 +03:00
|
|
|
}
|
|
|
|
|
2019-06-15 09:10:56 +03:00
|
|
|
func (p *_Pager) Quit() {
|
2019-06-15 10:23:53 +03:00
|
|
|
p.quit = true
|
2019-06-11 22:21:12 +03:00
|
|
|
}
|
|
|
|
|
2019-06-29 19:29:37 +03:00
|
|
|
func (p *_Pager) UpdateSearchPattern() {
|
|
|
|
if len(p.searchString) == 0 {
|
|
|
|
p.searchPattern = nil
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
pattern, err := regexp.Compile(p.searchString)
|
|
|
|
if err == nil {
|
|
|
|
// Search string is a regexp
|
2019-06-30 10:53:24 +03:00
|
|
|
// FIXME: Make this case insensitive if input is all-lowercase
|
2019-06-29 19:29:37 +03:00
|
|
|
p.searchPattern = pattern
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
pattern, err = regexp.Compile(regexp.QuoteMeta(p.searchString))
|
|
|
|
if err == nil {
|
|
|
|
// Pattern matching the string exactly
|
2019-06-30 10:53:24 +03:00
|
|
|
// FIXME: Make this case insensitive if input is all-lowercase
|
2019-06-29 19:29:37 +03:00
|
|
|
p.searchPattern = pattern
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Unable to create a match-string-verbatim pattern
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
2019-06-28 23:41:29 +03:00
|
|
|
func (p *_Pager) _OnSearchKey(logger *log.Logger, key tcell.Key) {
|
|
|
|
switch key {
|
|
|
|
case tcell.KeyEscape, tcell.KeyEnter:
|
|
|
|
p.isSearching = false
|
|
|
|
|
2019-06-29 12:20:48 +03:00
|
|
|
case tcell.KeyBackspace, tcell.KeyDEL:
|
|
|
|
if len(p.searchString) == 0 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
p.searchString = p.searchString[:len(p.searchString)-1]
|
2019-06-29 19:29:37 +03:00
|
|
|
p.UpdateSearchPattern()
|
2019-06-29 12:20:48 +03:00
|
|
|
|
2019-06-28 23:41:29 +03:00
|
|
|
default:
|
2019-06-29 12:20:48 +03:00
|
|
|
logger.Printf("Unhandled search key event %v", key)
|
2019-06-28 23:41:29 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-16 11:02:19 +03:00
|
|
|
func (p *_Pager) _OnKey(logger *log.Logger, key tcell.Key) {
|
2019-06-28 23:41:29 +03:00
|
|
|
if p.isSearching {
|
|
|
|
p._OnSearchKey(logger, key)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-06-23 22:30:11 +03:00
|
|
|
// FIXME: Add support for pressing 'h' to get a list of keybindings
|
2019-06-11 22:28:21 +03:00
|
|
|
switch key {
|
2019-06-13 16:56:06 +03:00
|
|
|
case tcell.KeyEscape:
|
2019-06-15 09:10:56 +03:00
|
|
|
p.Quit()
|
2019-06-12 22:55:09 +03:00
|
|
|
|
|
|
|
case tcell.KeyUp:
|
|
|
|
// Clipping is done in _AddLines()
|
|
|
|
p.firstLineOneBased--
|
|
|
|
|
2019-06-13 16:56:06 +03:00
|
|
|
case tcell.KeyDown, tcell.KeyEnter:
|
2019-06-12 22:55:09 +03:00
|
|
|
// Clipping is done in _AddLines()
|
|
|
|
p.firstLineOneBased++
|
2019-06-13 07:14:41 +03:00
|
|
|
|
|
|
|
case tcell.KeyHome:
|
|
|
|
p.firstLineOneBased = 1
|
|
|
|
|
|
|
|
case tcell.KeyEnd:
|
2019-06-16 22:54:25 +03:00
|
|
|
p.firstLineOneBased = math.MaxInt32
|
2019-06-13 07:14:41 +03:00
|
|
|
|
2019-06-13 07:21:43 +03:00
|
|
|
case tcell.KeyPgDn:
|
|
|
|
_, height := p.screen.Size()
|
|
|
|
p.firstLineOneBased += (height - 1)
|
|
|
|
|
|
|
|
case tcell.KeyPgUp:
|
|
|
|
_, height := p.screen.Size()
|
|
|
|
p.firstLineOneBased -= (height - 1)
|
2019-06-16 11:02:19 +03:00
|
|
|
|
|
|
|
default:
|
2019-06-28 23:41:29 +03:00
|
|
|
logger.Printf("Unhandled key event %v", key)
|
2019-06-11 22:28:21 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-29 12:20:48 +03:00
|
|
|
func (p *_Pager) _OnSearchRune(logger *log.Logger, char rune) {
|
|
|
|
p.searchString = p.searchString + string(char)
|
2019-06-29 19:29:37 +03:00
|
|
|
p.UpdateSearchPattern()
|
2019-06-28 23:41:29 +03:00
|
|
|
}
|
|
|
|
|
2019-06-16 11:02:19 +03:00
|
|
|
func (p *_Pager) _OnRune(logger *log.Logger, char rune) {
|
2019-06-28 23:41:29 +03:00
|
|
|
if p.isSearching {
|
2019-06-29 12:20:48 +03:00
|
|
|
p._OnSearchRune(logger, char)
|
2019-06-28 23:41:29 +03:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-06-11 22:32:24 +03:00
|
|
|
switch char {
|
|
|
|
case 'q':
|
2019-06-15 09:10:56 +03:00
|
|
|
p.Quit()
|
2019-06-13 07:21:43 +03:00
|
|
|
|
2019-06-13 16:56:06 +03:00
|
|
|
case 'k', 'y':
|
|
|
|
// Clipping is done in _AddLines()
|
|
|
|
p.firstLineOneBased--
|
|
|
|
|
|
|
|
case 'j', 'e':
|
|
|
|
// Clipping is done in _AddLines()
|
|
|
|
p.firstLineOneBased++
|
|
|
|
|
2019-06-13 07:14:41 +03:00
|
|
|
case '<', 'g':
|
|
|
|
p.firstLineOneBased = 1
|
2019-06-13 07:21:43 +03:00
|
|
|
|
2019-06-13 07:14:41 +03:00
|
|
|
case '>', 'G':
|
2019-06-16 22:54:25 +03:00
|
|
|
p.firstLineOneBased = math.MaxInt32
|
2019-06-13 07:21:43 +03:00
|
|
|
|
2019-06-13 16:56:06 +03:00
|
|
|
case 'f', ' ':
|
2019-06-13 07:21:43 +03:00
|
|
|
_, height := p.screen.Size()
|
|
|
|
p.firstLineOneBased += (height - 1)
|
|
|
|
|
|
|
|
case 'b':
|
|
|
|
_, height := p.screen.Size()
|
|
|
|
p.firstLineOneBased -= (height - 1)
|
|
|
|
|
2019-06-28 23:41:29 +03:00
|
|
|
case '/':
|
|
|
|
p.isSearching = true
|
|
|
|
|
2019-06-16 11:02:19 +03:00
|
|
|
default:
|
|
|
|
logger.Printf("Unhandled rune keyress '%s'", string(char))
|
2019-06-11 22:32:24 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-10 22:50:31 +03:00
|
|
|
// StartPaging brings up the pager on screen
|
2019-06-16 11:02:19 +03:00
|
|
|
func (p *_Pager) StartPaging(logger *log.Logger, screen tcell.Screen) {
|
2019-06-16 22:26:04 +03:00
|
|
|
// We want to match the terminal theme, see screen.Init() source code
|
|
|
|
os.Setenv("TCELL_TRUECOLOR", "disable")
|
|
|
|
|
2019-06-15 09:10:56 +03:00
|
|
|
if e := screen.Init(); e != nil {
|
2019-06-11 19:29:30 +03:00
|
|
|
fmt.Fprintf(os.Stderr, "%v\n", e)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
2019-06-15 09:10:56 +03:00
|
|
|
p.screen = screen
|
|
|
|
screen.Show()
|
2019-06-16 21:57:03 +03:00
|
|
|
p._Redraw(logger)
|
2019-06-11 19:29:30 +03:00
|
|
|
|
|
|
|
// Main loop
|
2019-06-15 10:23:53 +03:00
|
|
|
for !p.quit {
|
|
|
|
ev := screen.PollEvent()
|
|
|
|
switch ev := ev.(type) {
|
|
|
|
case *tcell.EventKey:
|
|
|
|
if ev.Key() == tcell.KeyRune {
|
2019-06-16 11:02:19 +03:00
|
|
|
p._OnRune(logger, ev.Rune())
|
2019-06-15 10:23:53 +03:00
|
|
|
} else {
|
2019-06-16 11:02:19 +03:00
|
|
|
p._OnKey(logger, ev.Key())
|
2019-06-11 19:29:30 +03:00
|
|
|
}
|
2019-06-15 09:10:56 +03:00
|
|
|
|
2019-06-15 10:23:53 +03:00
|
|
|
case *tcell.EventResize:
|
|
|
|
// We'll be implicitly redrawn just by taking another lap in the loop
|
2019-06-16 11:02:19 +03:00
|
|
|
|
|
|
|
default:
|
|
|
|
logger.Printf("Unhandled event type: %v", ev)
|
2019-06-11 19:29:30 +03:00
|
|
|
}
|
|
|
|
|
2019-06-22 00:24:53 +03:00
|
|
|
// FIXME: If more events are ready, skip this redraw, that
|
|
|
|
// should speed up mouse wheel scrolling
|
|
|
|
|
2019-06-16 21:57:03 +03:00
|
|
|
p._Redraw(logger)
|
2019-06-15 10:23:53 +03:00
|
|
|
}
|
2019-06-10 22:50:31 +03:00
|
|
|
}
|