From eef6fb190839909a5cc015f3a6e5a61eb8e1ddf4 Mon Sep 17 00:00:00 2001 From: TypicalAM Date: Sun, 23 Jul 2023 00:11:56 +0200 Subject: [PATCH 1/5] feat: add a faster experimental highlighting engine --- internal/model/tab/feed/feed.go | 34 +++++++++--- internal/model/tab/feed/selector.go | 83 +++++++++++++++++++---------- main.go | 18 ++++++- 3 files changed, 98 insertions(+), 37 deletions(-) diff --git a/internal/model/tab/feed/feed.go b/internal/model/tab/feed/feed.go index 4eec7cc..1465a5d 100644 --- a/internal/model/tab/feed/feed.go +++ b/internal/model/tab/feed/feed.go @@ -74,7 +74,8 @@ func (k Keymap) FullHelp() [][]key.Binding { type Model struct { list list.Model fetcher backend.Fetcher - tr *glamour.TermRenderer + colorTr *glamour.TermRenderer + noColorTr *glamour.TermRenderer colors *theme.Colors selector *selector title string @@ -263,9 +264,9 @@ func (m Model) loadTab(items []list.Item, articleContents []string) (tab.Tab, te m.list.DisableQuitKeybindings() m.viewport = viewport.New(m.style.viewportWidth, m.height) - m.articleContent = articleContents - termRenderer, err := glamour.NewTermRenderer( + + colorTr, err := glamour.NewTermRenderer( glamour.WithStyles(m.colors.MarkdownStyle), glamour.WithWordWrap(m.style.viewportWidth-2), ) @@ -276,8 +277,20 @@ func (m Model) loadTab(items []list.Item, articleContents []string) (tab.Tab, te return m, nil } + noColorTr, err := glamour.NewTermRenderer( + glamour.WithStyles(glamour.NoTTYStyleConfig), + glamour.WithWordWrap(m.style.viewportWidth-2), + ) + + if err != nil { + m.errShown = true + m.loaded = false + return m, nil + } + // Locked and loaded - m.tr = termRenderer + m.colorTr = colorTr + m.noColorTr = noColorTr m.loaded = true return m, nil } @@ -293,14 +306,21 @@ func (m Model) updateViewport() (tab.Tab, tea.Cmd) { return m, nil } - text, err := m.tr.Render(m.articleContent[m.list.Index()]) + rawText := m.articleContent[m.list.Index()] + styledText, err := m.colorTr.Render(rawText) + if err != nil { + m.viewport.SetContent(fmt.Sprintf("We have encountered an error styling the content: %s", err)) + return m, nil + } + + noColorText, err := m.noColorTr.Render(rawText) if err != nil { m.viewport.SetContent(fmt.Sprintf("We have encountered an error styling the content: %s", err)) return m, nil } - m.selector.newArticle(text) - m.viewport.SetContent(text) + m.selector.newArticle(&rawText, &noColorText) + m.viewport.SetContent(styledText) m.viewport.SetYOffset(0) return m, nil } diff --git a/internal/model/tab/feed/selector.go b/internal/model/tab/feed/selector.go index 62e150d..d546384 100644 --- a/internal/model/tab/feed/selector.go +++ b/internal/model/tab/feed/selector.go @@ -3,7 +3,6 @@ package feed import ( "errors" "os/exec" - "regexp" "runtime" "strings" @@ -12,13 +11,10 @@ import ( "mvdan.cc/xurls/v2" ) -// TODO: Move from this monstrosity to a custom written regex (this is gonna be fun) -var ansiRe = regexp.MustCompile("[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))") - // selector allows us to select links from a feed and open them in the browser type selector struct { linkStyle lipgloss.Style - article string + article *string urls []string indices [][]int selection int @@ -35,31 +31,61 @@ func newSelector(colors *theme.Colors) *selector { } // newArticle finds the URLs for this article -func (s *selector) newArticle(content string) { - rx := xurls.Relaxed() - - s.article = content +func (s *selector) newArticle(rawText, styledText *string) { + s.article = styledText s.selection = 0 s.active = false - rawIndices := rx.FindAllStringIndex(content, -1) + rx := xurls.Strict() + urlsToIndex := rx.FindAllString(*rawText, -1) + + // map the url to their possible linebreak indices + urlsMap := make(map[string][]int) + for _, url := range urlsToIndex { + if !strings.ContainsRune(url, '-') { + urlsMap[url] = append(urlsMap[url], len(url)-1) + continue + } + + for i := 0; i < len(url); i++ { + if url[i] == '-' { + urlsMap[url] = append(urlsMap[url], i) + } + } + + urlsMap[url] = append(urlsMap[url], len(url)-1) + } - // Fix the newline issues - s.indices = make([][]int, 0) s.urls = make([]string, 0) + s.indices = make([][]int, 0) + + for url, indices := range urlsMap { + s.urls = append(s.urls, url) + // Check if the entire url fits in one line + start := strings.Index(*styledText, url[:indices[len(indices)-1]]) + if start != -1 { + s.indices = append(s.indices, []int{start, start + len(url)}) + continue + } - // Link highlighting is stupid, so we have to do this - for i := 0; i < len(rawIndices); i++ { - str := s.article[rawIndices[i][0]:rawIndices[i][1]] - if str[len(str)-1] == '-' { - s.indices = append(s.indices, []int{rawIndices[i][0], rawIndices[i+1][1]}) - urlNoAnsi := ansiRe.ReplaceAllString(s.article[rawIndices[i][0]:rawIndices[i+1][1]], "") - urlStripped := strings.ReplaceAll(strings.ReplaceAll(urlNoAnsi, " ", ""), "\n", "") - s.urls = append(s.urls, urlStripped) - i++ - } else { - s.indices = append(s.indices, rawIndices[i]) - s.urls = append(s.urls, s.article[rawIndices[i][0]:rawIndices[i][1]]) + // Let's check on the - character on which the url is broken down + for i := len(indices) - 2; i >= 0; i-- { + start = strings.Index(*styledText, url[:indices[i]]) + if start == -1 { + continue + } + + // The line is broken down on index, let's search where it ends on the next line + end := 0 + for j := start + indices[i]; j < len(*styledText); j++ { + if (*styledText)[j] == url[indices[i]+1] { + end = j + len(url) - indices[i] - 1 + break + } + } + + s.indices = append(s.indices, []int{start, end + 1}) + break } } } @@ -75,12 +101,11 @@ func (s *selector) cycle() string { } start, end := s.indices[s.selection][0], s.indices[s.selection][1] - linkText := s.article[start:end] - b.WriteString(s.article[:start]) + b.WriteString((*s.article)[:start]) + linkText := (*s.article)[start:end] // This is tricky - if strings.ContainsRune(s.article[start:end], '\n') { - linkText = ansiRe.ReplaceAllString(linkText, "") + if strings.ContainsRune(linkText, '\n') { newLine := strings.IndexRune(linkText, '\n') lastSpace := strings.LastIndex(linkText, " ") @@ -91,7 +116,7 @@ func (s *selector) cycle() string { b.WriteString(s.linkStyle.Render(linkText)) } - b.WriteString(s.article[end:]) + b.WriteString((*s.article)[end:]) return b.String() } diff --git a/main.go b/main.go index 5cc7af4..8a67906 100644 --- a/main.go +++ b/main.go @@ -1,12 +1,28 @@ package main -import "github.com/TypicalAM/goread/cmd/goread" +import ( + "log" + "os" + "runtime/pprof" + + "github.com/TypicalAM/goread/cmd/goread" +) var ( version = "dev" ) func main() { + f, err := os.Create("test.prof") + if err != nil { + log.Fatal(err) + } + + if err = pprof.StartCPUProfile(f); err != nil { + log.Fatal(err) + } + + defer pprof.StopCPUProfile() goread.SetVersion(version) goread.Execute() } From e9418207961b463e470f38c8251b3a3cbb14c619 Mon Sep 17 00:00:00 2001 From: TypicalAM Date: Sun, 23 Jul 2023 01:14:13 +0200 Subject: [PATCH 2/5] fix: highlighting links put extra line in the vp --- internal/model/tab/feed/selector.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/model/tab/feed/selector.go b/internal/model/tab/feed/selector.go index d546384..fb2ef72 100644 --- a/internal/model/tab/feed/selector.go +++ b/internal/model/tab/feed/selector.go @@ -84,7 +84,7 @@ func (s *selector) newArticle(rawText, styledText *string) { } } - s.indices = append(s.indices, []int{start, end + 1}) + s.indices = append(s.indices, []int{start, end}) break } } From b8378896d82141b723efc0085bb1eb8401653057 Mon Sep 17 00:00:00 2001 From: TypicalAM Date: Mon, 24 Jul 2023 01:37:50 +0200 Subject: [PATCH 3/5] fix: buggy newline issue when highlighting --- internal/model/browser/browser.go | 4 +-- internal/model/popup/popup.go | 44 +++++++++++++------------------ internal/model/simplelist/list.go | 3 +-- 3 files changed, 22 insertions(+), 29 deletions(-) diff --git a/internal/model/browser/browser.go b/internal/model/browser/browser.go index dfdf8a7..8309eef 100644 --- a/internal/model/browser/browser.go +++ b/internal/model/browser/browser.go @@ -169,7 +169,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m.createNewTab(msg) case backend.NewItemMsg: - bg := lipgloss.NewStyle().Width(m.width).Height((m.height)).Render(m.View()) + bg := m.View() width := m.width / 2 height := 17 @@ -194,7 +194,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m.downloadItem(msg) case backend.MakeChoiceMsg: - bg := lipgloss.NewStyle().Width(m.width).Height((m.height)).Render(m.View()) + bg := m.View() width := m.width / 2 m.popup = popup.NewChoice(m.style.colors, bg, width, msg.Question, msg.Default) diff --git a/internal/model/popup/popup.go b/internal/model/popup/popup.go index c5c27ab..a4d999c 100644 --- a/internal/model/popup/popup.go +++ b/internal/model/popup/popup.go @@ -4,7 +4,6 @@ import ( "strings" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" "github.com/muesli/ansi" ) @@ -15,13 +14,12 @@ type Popup interface { // Default is a default popup window. type Default struct { - prefix string - suffix string - ogSection []string - section []string + textAbove string + textBelow string + rowPrefix []string + rowSuffix []string width int height int - startCol int } // New creates a new default popup window. @@ -33,21 +31,24 @@ func New(bgRaw string, width, height int) Default { startRow := (bgHeight - height) / 2 startCol := (bgWidth - width) / 2 - ogSection := make([]string, height) - section := make([]string, height) + rowPrefix := make([]string, height) + rowSuffix := make([]string, height) + + for i, text := range bg[startRow : startRow+height] { + rowPrefix[i] = text[:findPrintableIndex(text, startCol)] + rowSuffix[i] = text[findPrintableIndex(text, startCol+width):] + } prefix := strings.Join(bg[:startRow], "\n") suffix := strings.Join(bg[startRow+height:], "\n") - copy(ogSection, bg[startRow:startRow+height]) return Default{ - ogSection: ogSection, - section: section, + rowPrefix: rowPrefix, + rowSuffix: rowSuffix, width: width, height: height, - prefix: prefix, - suffix: suffix, - startCol: startCol, + textAbove: prefix, + textBelow: suffix, } } @@ -57,19 +58,12 @@ func (p Default) Overlay(text string) string { lines := strings.Split(text, "\n") // Overlay the background with the styled text. - // TODO: Use a string builder - for i, text := range p.ogSection { - p.section[i] = text[:findPrintableIndex(text, p.startCol)] + - lines[i] + - text[findPrintableIndex(text, p.startCol+p.width):] + output := make([]string, len(lines)) + for i := 0; i < len(lines); i++ { + output[i] = p.rowPrefix[i] + lines[i] + p.rowSuffix[i] } - return lipgloss.JoinVertical( - lipgloss.Top, - p.prefix, - strings.Join(p.section, "\n"), - p.suffix, - ) + return p.textAbove + "\n" + strings.Join(output, "\n") + "\n" + p.textBelow } // Width returns the width of the popup window. diff --git a/internal/model/simplelist/list.go b/internal/model/simplelist/list.go index 4a5e00c..77065c5 100644 --- a/internal/model/simplelist/list.go +++ b/internal/model/simplelist/list.go @@ -7,7 +7,6 @@ import ( "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/muesli/reflow/ansi" ) // Item is an item in the list @@ -142,7 +141,7 @@ func (m Model) View() string { if m.showDesc { if item, ok := m.items[i].(list.DefaultItem); ok { - if ansi.PrintableRuneWidth(item.Description()) != 0 { + if len(item.Description()) != 0 { sections = append(sections, m.style.styleDescription(item.Description())) } else { sections = append(sections, "") From 397df6a23f2427386769bcfdf798772fbac6390f Mon Sep 17 00:00:00 2001 From: TypicalAM Date: Mon, 24 Jul 2023 01:40:17 +0200 Subject: [PATCH 4/5] chore: remove the profiling --- main.go | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/main.go b/main.go index 8a67906..0f2e7a8 100644 --- a/main.go +++ b/main.go @@ -1,10 +1,6 @@ package main import ( - "log" - "os" - "runtime/pprof" - "github.com/TypicalAM/goread/cmd/goread" ) @@ -13,16 +9,6 @@ var ( ) func main() { - f, err := os.Create("test.prof") - if err != nil { - log.Fatal(err) - } - - if err = pprof.StartCPUProfile(f); err != nil { - log.Fatal(err) - } - - defer pprof.StopCPUProfile() goread.SetVersion(version) goread.Execute() } From 9a7d7b7b8a5706f92518920b1460e200459e2675 Mon Sep 17 00:00:00 2001 From: TypicalAM Date: Mon, 24 Jul 2023 04:52:02 +0200 Subject: [PATCH 5/5] fix: left pad popup prefix strings when width is not correct --- internal/model/popup/popup.go | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/internal/model/popup/popup.go b/internal/model/popup/popup.go index a4d999c..f06ca21 100644 --- a/internal/model/popup/popup.go +++ b/internal/model/popup/popup.go @@ -35,8 +35,21 @@ func New(bgRaw string, width, height int) Default { rowSuffix := make([]string, height) for i, text := range bg[startRow : startRow+height] { - rowPrefix[i] = text[:findPrintableIndex(text, startCol)] - rowSuffix[i] = text[findPrintableIndex(text, startCol+width):] + popupStart := findPrintIndex(text, startCol) + popupEnd := findPrintIndex(text, startCol+width) + + if popupStart != -1 { + rowPrefix[i] = text[:popupStart] + } else { + rowPrintable := ansi.PrintableRuneWidth(text) + rowPrefix[i] = text + strings.Repeat(" ", startCol-rowPrintable) + } + + if popupEnd != -1 { + rowSuffix[i] = text[popupEnd:] + } else { + rowSuffix[i] = "" + } } prefix := strings.Join(bg[:startRow], "\n") @@ -76,8 +89,8 @@ func (p Default) Height() int { return p.height } -// findPrintableIndex finds the index of the last printable rune at the given index. -func findPrintableIndex(str string, index int) int { +// findPrintIndex finds the print index, that is what string index corresponds to the given printable rune index. +func findPrintIndex(str string, index int) int { for i := len(str) - 1; i >= 0; i-- { if ansi.PrintableRuneWidth(str[:i]) == index { return i