diff --git a/bombadillo.1 b/bombadillo.1 index 04d186c..bd7f0c8 100644 --- a/bombadillo.1 +++ b/bombadillo.1 @@ -88,6 +88,13 @@ k Scroll up a single line. .TP .B +n +Jump to next found text item. +.TP +.B +Jump to previous found text item. +.TP +.B q Quit \fBbombadillo\fP. .TP @@ -100,6 +107,10 @@ u Scroll up an amount corresponding to 75% of your terminal window height in the current document. .TP .B +/ +Search for text within current document. / followed by a text query will highlight and allow navigation of found text. / with an empty query will clear the current query. +.TP +.B Toggle the scroll focus between the bookmarks panel and the document panel. Only has an effect if the bookmarks panel is open. .TP diff --git a/client.go b/client.go index b0270ac..ed642a2 100644 --- a/client.go +++ b/client.go @@ -90,9 +90,8 @@ func (c *client) Draw() { var re *regexp.Regexp if c.Options["theme"] == "inverse" { screen.WriteString("\033[7m") - } else if c.Options["theme"] == "color" { - re = regexp.MustCompile(`\033\[(?:\d*;?)+[A-Za-z]`) } + re = regexp.MustCompile(`\033\[(?:\d*;?)+[A-Za-z]`) if c.BookMarks.IsOpen { bm := c.BookMarks.Render(c.Width, c.Height) bmWidth := len([]rune(bm[0])) @@ -137,11 +136,9 @@ func (c *client) Draw() { for i := 0; i < c.Height-3; i++ { if i < len(pageContent) { extra := 0 - if c.Options["theme"] == "color" { - escapes := re.FindAllString(pageContent[i], -1) - for _, esc := range escapes { - extra += len(esc) - } + escapes := re.FindAllString(pageContent[i], -1) + for _, esc := range escapes { + extra += len(esc) } screen.WriteString(fmt.Sprintf("%-*.*s", c.Width+extra, c.Width+extra, pageContent[i])) screen.WriteString("\n") @@ -235,6 +232,45 @@ func (c *client) TakeControlInput() { // Toggle bookmark browser focus on/off c.BookMarks.ToggleFocused() c.Draw() + case 'n': + // Next search item + c.ClearMessage() + err := c.NextSearchItem(1) + if err != nil { + c.SetMessage(err.Error(), false) + c.DrawMessage() + } + case 'N': + // Previous search item + c.ClearMessage() + err := c.NextSearchItem(-1) + if err != nil { + c.SetMessage(err.Error(), false) + c.DrawMessage() + } + case '/': + // Search for text + c.ClearMessage() + c.ClearMessageLine() + if c.Options["theme"] == "normal" || c.Options["theme"] == "color" { + fmt.Printf("\033[7m%*.*s\r", c.Width, c.Width, "") + } + entry, err := cui.GetLine("/") + c.ClearMessageLine() + if err != nil { + c.SetMessage(err.Error(), true) + c.DrawMessage() + break + } + err = c.find(entry) + if err != nil { + c.SetMessage(err.Error(), true) + c.DrawMessage() + } + err = c.NextSearchItem(0) + if err != nil { + c.Draw() + } case ':', ' ': // Process a command c.ClearMessage() @@ -242,7 +278,7 @@ func (c *client) TakeControlInput() { if c.Options["theme"] == "normal" || c.Options["theme"] == "color" { fmt.Printf("\033[7m%*.*s\r", c.Width, c.Width, "") } - entry, err := cui.GetLine() + entry, err := cui.GetLine(": ") c.ClearMessageLine() if err != nil { c.SetMessage(err.Error(), true) @@ -644,7 +680,7 @@ func (c *client) search(query, url, question string) { fmt.Printf("\033[7m%*.*s\r", c.Width, c.Width, "") } fmt.Print(question) - entry, err = cui.GetLine() + entry, err = cui.GetLine("? ") c.ClearMessageLine() if err != nil { c.SetMessage(err.Error(), true) @@ -1051,6 +1087,67 @@ func (c *client) handleWeb(u Url) { } } +func (c *client) find(s string) error { + c.PageState.History[c.PageState.Position].SearchTerm = s + c.PageState.History[c.PageState.Position].FindText() + if s == "" { + return nil + } + if len(c.PageState.History[c.PageState.Position].FoundLinkLines) == 0 { + return fmt.Errorf("No text matching %q was found", s) + } + return nil +} + +func (c *client) NextSearchItem(dir int) error { + page := c.PageState.History[c.PageState.Position] + if len(page.FoundLinkLines) == 0 { + return fmt.Errorf("The search is over before it has begun") + } + c.PageState.History[c.PageState.Position].SearchIndex += dir + page.SearchIndex += dir + if page.SearchIndex < 0 { + c.PageState.History[c.PageState.Position].SearchIndex = 0 + page.SearchIndex = 0 + } + + if page.SearchIndex >= len(page.FoundLinkLines) { + c.PageState.History[c.PageState.Position].SearchIndex = len(page.FoundLinkLines) - 1 + return fmt.Errorf("The search path goes no further") + } else if page.SearchIndex < 0 { + c.PageState.History[c.PageState.Position].SearchIndex = 0 + return fmt.Errorf("You are at the beginning of the search path") + } + + diff := page.FoundLinkLines[page.SearchIndex] - page.ScrollPosition + c.ScrollForSearch(diff) + c.Draw() + return nil +} + +func (c *client) ScrollForSearch(amount int) { + var percentRead int + page := c.PageState.History[c.PageState.Position] + bottom := len(page.WrappedContent) - c.Height + 3 // 3 for the three bars: top, msg, bottom + + newScrollPosition := page.ScrollPosition + amount + if newScrollPosition < 0 { + newScrollPosition = 0 + } else if newScrollPosition > bottom { + newScrollPosition = bottom + } + + c.PageState.History[c.PageState.Position].ScrollPosition = newScrollPosition + + if len(page.WrappedContent) < c.Height-3 { + percentRead = 100 + } else { + percentRead = int(float32(newScrollPosition+c.Height-3) / float32(len(page.WrappedContent)) * 100.0) + } + c.FootBar.SetPercentRead(percentRead) + c.Draw() +} + //------------------------------------------------\\ // + + + F U N C T I O N S + + + \\ //--------------------------------------------------\\ diff --git a/cui/cui.go b/cui/cui.go index b6ca90b..f294ea0 100644 --- a/cui/cui.go +++ b/cui/cui.go @@ -96,11 +96,11 @@ func Getch() rune { return char } -func GetLine() (string, error) { +func GetLine(prefix string) (string, error) { SetLineMode() reader := bufio.NewReader(os.Stdin) - fmt.Print(": ") + fmt.Print(prefix) text, err := reader.ReadString('\n') if err != nil { return "", err diff --git a/page.go b/page.go index 6ef719e..46c95ba 100644 --- a/page.go +++ b/page.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "strings" ) @@ -17,6 +18,9 @@ type Page struct { Links []string Location Url ScrollPosition int + FoundLinkLines []int + SearchTerm string + SearchIndex int } //------------------------------------------------\\ @@ -95,7 +99,7 @@ func (p *Page) WrapContent(width int, color bool) { } continue } else { - if counter < width { + if counter <= width { content.WriteRune(ch) counter++ } else { @@ -112,6 +116,47 @@ func (p *Page) WrapContent(width int, color bool) { } p.WrappedContent = strings.Split(content.String(), "\n") + p.HighlightFoundText() +} + +func (p *Page) HighlightFoundText() { + if p.SearchTerm == "" { + return + } + for i, ln := range p.WrappedContent { + found := strings.Index(ln, p.SearchTerm) + if found < 0 { + continue + } + format := "\033[7m%s\033[27m" + if bombadillo.Options["theme"] == "inverse" { + format = "\033[27m%s\033[7m" + } + ln = strings.ReplaceAll(ln, p.SearchTerm, fmt.Sprintf(format, p.SearchTerm)) + p.WrappedContent[i] = ln + } +} + +func (p *Page) FindText() { + p.FoundLinkLines = make([]int, 0, 10) + s := p.SearchTerm + p.SearchIndex = 0 + if s == "" { + return + } + format := "\033[7m%s\033[27m" + if bombadillo.Options["theme"] == "inverse" { + format = "\033[27m%s\033[7m" + } + for i, ln := range p.WrappedContent { + found := strings.Index(ln, s) + if found < 0 { + continue + } + ln = strings.ReplaceAll(ln, s, fmt.Sprintf(format, s)) + p.WrappedContent[i] = ln + p.FoundLinkLines = append(p.FoundLinkLines, i) + } } //------------------------------------------------\\ @@ -120,6 +165,6 @@ func (p *Page) WrapContent(width int, color bool) { // MakePage returns a Page struct with default values func MakePage(url Url, content string, links []string) Page { - p := Page{make([]string, 0), content, links, url, 0} + p := Page{make([]string, 0), content, links, url, 0, make([]int, 0), "", 0} return p }