From 8e7300726dd36053eb7b409eb8dd659c499c1020 Mon Sep 17 00:00:00 2001 From: makeworld Date: Sat, 27 Feb 2021 18:17:49 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20Initial=20work?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- display/about.go | 15 +++---- display/bookmarks.go | 15 +++---- display/display.go | 46 ++++++++++++--------- display/file.go | 43 +++++++++++--------- display/handlers.go | 2 +- display/private.go | 21 +++++----- display/subscriptions.go | 30 +++++++------- display/tab.go | 88 ++++++++++++++++++++++++++++++++++++---- display/util.go | 8 ++-- go.mod | 1 + renderer/page.go | 5 ++- renderer/renderer.go | 14 +++++-- structs/structs.go | 7 ++-- 13 files changed, 196 insertions(+), 99 deletions(-) diff --git a/display/about.go b/display/about.go index 4637751..f22c480 100644 --- a/display/about.go +++ b/display/about.go @@ -34,13 +34,14 @@ func aboutInit(version, commit, builtBy string) { } func createAboutPage(url string, content string) structs.Page { - renderContent, links := renderer.RenderGemini(content, textWidth(), false) + renderContent, links, maxPreCols := renderer.RenderGemini(content, textWidth(), false) return structs.Page{ - Raw: content, - Content: renderContent, - Links: links, - URL: url, - Width: -1, // Force reformatting on first display - Mediatype: structs.TextGemini, + Raw: content, + Content: renderContent, + MaxPreCols: maxPreCols, + Links: links, + URL: url, + TermWidth: -1, // Force reformatting on first display + Mediatype: structs.TextGemini, } } diff --git a/display/bookmarks.go b/display/bookmarks.go index 0fab79b..6415f74 100644 --- a/display/bookmarks.go +++ b/display/bookmarks.go @@ -132,14 +132,15 @@ func Bookmarks(t *tab) { bkmkPageRaw += fmt.Sprintf("=> %s %s\r\n", keys[i], m[keys[i]]) } // Render and display - content, links := renderer.RenderGemini(bkmkPageRaw, textWidth(), false) + content, links, maxPreCols := renderer.RenderGemini(bkmkPageRaw, textWidth(), false) page := structs.Page{ - Raw: bkmkPageRaw, - Content: content, - Links: links, - URL: "about:bookmarks", - Width: termW, - Mediatype: structs.TextGemini, + Raw: bkmkPageRaw, + Content: content, + MaxPreCols: maxPreCols, + Links: links, + URL: "about:bookmarks", + TermWidth: termW, + Mediatype: structs.TextGemini, } setPage(t, &page) t.applyBottomBar() diff --git a/display/display.go b/display/display.go index 0f3d7e4..0a658a7 100644 --- a/display/display.go +++ b/display/display.go @@ -72,7 +72,11 @@ func Init(version, commit, builtBy string) { reformatMu.Lock() // Only allow one reformat job at a time for i := range tabs { // Overwrite all tabs with a new, differently sized, left margin - browser.AddTab(strconv.Itoa(i), makeTabLabel(strconv.Itoa(i+1)), makeContentLayout(tabs[i].view)) + browser.AddTab( + strconv.Itoa(i), + makeTabLabel(strconv.Itoa(i+1)), + makeContentLayout(tabs[i].view, leftMargin()), + ) if tabs[i] == t { // Reformat page ASAP, in the middle of loop reformatPageAndSetView(t, t.page) @@ -129,8 +133,6 @@ func Init(version, commit, builtBy string) { bottomBar.SetDoneFunc(func(key tcell.Key) { tab := curTab - tabs[tab].saveScroll() - // Reset func to set the bottomBar back to what it was before // Use for errors. reset := func() { @@ -247,14 +249,15 @@ func Init(version, commit, builtBy string) { // Render the default new tab content ONCE and store it for later // This code is repeated in Reload() newTabContent := getNewTabContent() - renderedNewTabContent, newTabLinks := renderer.RenderGemini(newTabContent, textWidth(), false) + renderedNewTabContent, newTabLinks, maxPreCols := renderer.RenderGemini(newTabContent, textWidth(), false) newTabPage = structs.Page{ - Raw: newTabContent, - Content: renderedNewTabContent, - Links: newTabLinks, - URL: "about:newtab", - Width: -1, // Force reformatting on first display - Mediatype: structs.TextGemini, + Raw: newTabContent, + Content: renderedNewTabContent, + MaxPreCols: maxPreCols, + Links: newTabLinks, + URL: "about:newtab", + TermWidth: -1, // Force reformatting on first display + Mediatype: structs.TextGemini, } modalInit() @@ -432,7 +435,6 @@ func NewTab() { tabs[curTab].view.Highlight("") // Save bottomBar state tabs[curTab].saveBottomBar() - tabs[curTab].saveScroll() } curTab = NumTabs() @@ -443,7 +445,11 @@ func NewTab() { tabs[curTab].addToHistory("about:newtab") tabs[curTab].history.pos = 0 // Manually set as first page - browser.AddTab(strconv.Itoa(curTab), makeTabLabel(strconv.Itoa(curTab+1)), makeContentLayout(tabs[curTab].view)) + browser.AddTab( + strconv.Itoa(curTab), + makeTabLabel(strconv.Itoa(curTab+1)), + makeContentLayout(tabs[curTab].view, leftMargin()), + ) browser.SetCurrentTab(strconv.Itoa(curTab)) App.SetFocus(tabs[curTab].view) @@ -506,7 +512,6 @@ func SwitchTab(tab int) { if curTab > -1 { // Save bottomBar state tabs[curTab].saveBottomBar() - tabs[curTab].saveScroll() } curTab = tab % NumTabs() @@ -527,14 +532,15 @@ func Reload() { // Re-render new tab, similar to Init() newTabContent := getNewTabContent() tmpTermW := termW - renderedNewTabContent, newTabLinks := renderer.RenderGemini(newTabContent, textWidth(), false) + renderedNewTabContent, newTabLinks, maxPreCols := renderer.RenderGemini(newTabContent, textWidth(), false) newTabPage = structs.Page{ - Raw: newTabContent, - Content: renderedNewTabContent, - Links: newTabLinks, - URL: "about:newtab", - Width: tmpTermW, - Mediatype: structs.TextGemini, + Raw: newTabContent, + Content: renderedNewTabContent, + MaxPreCols: maxPreCols, + Links: newTabLinks, + URL: "about:newtab", + TermWidth: tmpTermW, + Mediatype: structs.TextGemini, } temp := newTabPage // Copy setPage(tabs[curTab], &temp) diff --git a/display/file.go b/display/file.go index a5ba3da..53e74bf 100644 --- a/display/file.go +++ b/display/file.go @@ -59,23 +59,25 @@ func handleFile(u string) (*structs.Page, bool) { } if mimetype == "text/gemini" { - rendered, links := renderer.RenderGemini(string(content), textWidth(), false) + rendered, links, maxPreCols := renderer.RenderGemini(string(content), textWidth(), false) page = &structs.Page{ - Mediatype: structs.TextGemini, - URL: u, - Raw: string(content), - Content: rendered, - Links: links, - Width: termW, + Mediatype: structs.TextGemini, + URL: u, + Raw: string(content), + Content: rendered, + MaxPreCols: maxPreCols, + Links: links, + TermWidth: termW, } } else { page = &structs.Page{ - Mediatype: structs.TextPlain, - URL: u, - Raw: string(content), - Content: renderer.RenderPlainText(string(content)), - Links: []string{}, - Width: termW, + Mediatype: structs.TextPlain, + URL: u, + Raw: string(content), + Content: renderer.RenderPlainText(string(content)), + MaxPreCols: -1, + Links: []string{}, + TermWidth: termW, } } } @@ -107,14 +109,15 @@ func createDirectoryListing(u string) (*structs.Page, bool) { content += fmt.Sprintf("=> %s%s %s%s\n", f.Name(), separator, f.Name(), separator) } - rendered, links := renderer.RenderGemini(content, textWidth(), false) + rendered, links, maxPreCols := renderer.RenderGemini(content, textWidth(), false) page = &structs.Page{ - Mediatype: structs.TextGemini, - URL: u, - Raw: content, - Content: rendered, - Links: links, - Width: termW, + Mediatype: structs.TextGemini, + URL: u, + Raw: content, + Content: rendered, + MaxPreCols: maxPreCols, + Links: links, + TermWidth: termW, } return page, true } diff --git a/display/handlers.go b/display/handlers.go index 0f45419..8f63370 100644 --- a/display/handlers.go +++ b/display/handlers.go @@ -337,7 +337,7 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) { return ret("", false) } - page.Width = termW + page.TermWidth = termW if !client.HasClientCert(parsed.Host) { // Don't cache pages with client certs diff --git a/display/private.go b/display/private.go index de2e505..5f66a75 100644 --- a/display/private.go +++ b/display/private.go @@ -24,7 +24,6 @@ func followLink(t *tab, prev, next string) { } if t.hasContent() { - t.saveScroll() // Likely called later on, it's here just in case nextURL, err := resolveRelLink(t, prev, next) if err != nil { Error("URL Error", err.Error()) @@ -48,7 +47,7 @@ func followLink(t *tab, prev, next string) { // It will not waste resources if the passed page is already fitted to the current terminal width, and can be // called safely even when the page might be already formatted properly. func reformatPage(p *structs.Page) { - if p.Width == termW { + if p.TermWidth == termW { // No changes to make return } @@ -65,7 +64,7 @@ func reformatPage(p *structs.Page) { strings.HasPrefix(p.URL, "file") { proxied = false } - rendered, _ = renderer.RenderGemini(p.Raw, textWidth(), proxied) + rendered, _, _ = renderer.RenderGemini(p.Raw, textWidth(), proxied) case structs.TextPlain: rendered = renderer.RenderPlainText(p.Raw) case structs.TextAnsi: @@ -75,17 +74,16 @@ func reformatPage(p *structs.Page) { return } p.Content = rendered - p.Width = termW + p.TermWidth = termW } // reformatPageAndSetView is for reformatting a page that is already being displayed. // setPage should be used when a page is being loaded for the first time. func reformatPageAndSetView(t *tab, p *structs.Page) { - if p.Width == termW { + if p.TermWidth == termW { // No changes to make return } - t.saveScroll() reformatPage(p) t.view.SetText(p.Content) t.applyScroll() // Go back to where you were, roughly @@ -101,8 +99,6 @@ func setPage(t *tab, p *structs.Page) { return } - t.saveScroll() // Save the scroll of the previous page - // Make sure the page content is fitted to the terminal every time it's displayed reformatPage(p) @@ -112,10 +108,13 @@ func setPage(t *tab, p *structs.Page) { t.view.SetText(p.Content) t.view.Highlight("") // Turn off highlights, other funcs may restore if necessary t.view.ScrollToBeginning() - - // Set tab number in case a favicon from before overwrote it + // Reset page left margin tabNum := tabNumber(t) - browser.SetTabLabel(strconv.Itoa(tabNum), makeTabLabel(strconv.Itoa(tabNum+1))) + browser.AddTab( + strconv.Itoa(tabNum), + makeTabLabel(strconv.Itoa(tabNum+1)), + makeContentLayout(t.view, leftMargin()), + ) App.Draw() // Setup display diff --git a/display/subscriptions.go b/display/subscriptions.go index d65c19e..50f00c4 100644 --- a/display/subscriptions.go +++ b/display/subscriptions.go @@ -149,14 +149,15 @@ func Subscriptions(t *tab, u string) string { } } - content, links := renderer.RenderGemini(rawPage, textWidth(), false) + content, links, maxPreCols := renderer.RenderGemini(rawPage, textWidth(), false) page := structs.Page{ - Raw: rawPage, - Content: content, - Links: links, - URL: u, - Width: termW, - Mediatype: structs.TextGemini, + Raw: rawPage, + Content: content, + MaxPreCols: maxPreCols, + Links: links, + URL: u, + TermWidth: termW, + Mediatype: structs.TextGemini, } go cache.AddPage(&page) setPage(t, &page) @@ -191,14 +192,15 @@ func ManageSubscriptions(t *tab, u string) { ) } - content, links := renderer.RenderGemini(rawPage, textWidth(), false) + content, links, maxPreCols := renderer.RenderGemini(rawPage, textWidth(), false) page := structs.Page{ - Raw: rawPage, - Content: content, - Links: links, - URL: "about:manage-subscriptions", - Width: termW, - Mediatype: structs.TextGemini, + Raw: rawPage, + Content: content, + MaxPreCols: maxPreCols, + Links: links, + URL: "about:manage-subscriptions", + TermWidth: termW, + Mediatype: structs.TextGemini, } go cache.AddPage(&page) setPage(t, &page) diff --git a/display/tab.go b/display/tab.go index 9052bd9..0bc6abf 100644 --- a/display/tab.go +++ b/display/tab.go @@ -121,6 +121,58 @@ func makeNewTab() *tab { tabs[tab].page.SelectedID = strconv.Itoa(index) } }) + t.view.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + // Capture left/right scrolling and change the left margin size accordingly + // See #197 + // Up/down scrolling is saved in this func to keep them in sync, but the keys + // are passed and no extra behaviour happens. + + key := event.Key() + mod := event.Modifiers() + ru := event.Rune() + + oldCol := t.page.Column + + if (key == tcell.KeyRight && mod == tcell.ModNone) || + (key == tcell.KeyRune && mod == tcell.ModNone && ru == 'l') { + // Scrolling to the right + // TODO check if already scrolled to the end + t.page.Column++ + } else if (key == tcell.KeyLeft && mod == tcell.ModNone) || + (key == tcell.KeyRune && mod == tcell.ModNone && ru == 'h') { + // Scrolling to the left + if t.page.Column == 0 { + // Can't scroll to the left anymore + return nil + } + t.page.Column-- + } else if (key == tcell.KeyUp && mod == tcell.ModNone) || + (key == tcell.KeyRune && mod == tcell.ModNone && ru == 'k') { + // Scrolling up + if t.page.Row > 0 { + t.page.Row-- + } + return event + } else if (key == tcell.KeyDown && mod == tcell.ModNone) || + (key == tcell.KeyRune && mod == tcell.ModNone && ru == 'j') { + // Scrolling down + // TODO need to check for max vertical scroll before doing this + return event + } else { + // Some other key, stop processing it + return event + } + + if t.page.MaxPreCols <= termW && t.page.MaxPreCols > -1 { + // No scrolling is actually necessary + t.page.Column = oldCol // Reset + return nil // Ignore keys + } + + t.applyHorizontalScroll() + App.Draw() + return nil + }) return &t } @@ -167,19 +219,39 @@ func (t *tab) hasContent() bool { return true } -// saveScroll saves where in the page the user was. -// It should be used whenever moving from one page to another. -func (t *tab) saveScroll() { - // It will also be saved in the cache because the cache uses the same pointer - row, col := t.view.GetScrollOffset() - t.page.Row = row - t.page.Column = col +// applyHorizontalScroll handles horizontal scroll logic including left margin resizing, +// see #197 for details. Use applyScroll instead. +// +// In certain cases it will still use and apply the saved Row. +func (t *tab) applyHorizontalScroll() { + i := tabNumber(t) + if i == -1 { + // Tab is not actually being used and should not be (re)added to the browser + return + } + if t.page.Column >= leftMargin() { + // Scrolled to the right far enough that no left margin is needed + browser.AddTab( + strconv.Itoa(i), + makeTabLabel(strconv.Itoa(i+1)), + makeContentLayout(t.view, 0), + ) + t.view.ScrollTo(t.page.Row, t.page.Column-leftMargin()) + } else { + // Left margin is still needed, but is not necessarily at the right size by default + browser.AddTab( + strconv.Itoa(i), + makeTabLabel(strconv.Itoa(i+1)), + makeContentLayout(t.view, leftMargin()-t.page.Column), + ) + } } // applyScroll applies the saved scroll values to the page and tab. // It should only be used when going backward and forward. func (t *tab) applyScroll() { - t.view.ScrollTo(t.page.Row, t.page.Column) + t.view.ScrollTo(t.page.Row, 0) + t.applyHorizontalScroll() } // saveBottomBar saves the current bottomBar values in the tab. diff --git a/display/util.go b/display/util.go index 2594ff7..788fd43 100644 --- a/display/util.go +++ b/display/util.go @@ -14,13 +14,15 @@ import ( // This file contains funcs that are small, self-contained utilities. // makeContentLayout returns a flex that contains the given TextView -// along with the current correct left margin, as well as a single empty +// along with the provided left margin, as well as a single empty // line at the top, for a top margin. -func makeContentLayout(tv *cview.TextView) *cview.Flex { +func makeContentLayout(tv *cview.TextView, leftMargin int) *cview.Flex { // Create horizontal flex with the left margin as an empty space horiz := cview.NewFlex() horiz.SetDirection(cview.FlexColumn) - horiz.AddItem(nil, leftMargin(), 0, false) + if leftMargin > 0 { + horiz.AddItem(nil, leftMargin, 0, false) + } horiz.AddItem(tv, 0, 1, true) // Create a vertical flex with the other one and a top margin diff --git a/go.mod b/go.mod index 3737232..4d1f970 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/gdamore/tcell/v2 v2.1.1-0.20210125004847-19e17097d8fe github.com/google/go-cmp v0.5.0 // indirect github.com/makeworld-the-better-one/go-gemini v0.11.0 + github.com/mattn/go-runewidth v0.0.10 github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/mapstructure v1.3.1 // indirect github.com/mmcdole/gofeed v1.1.0 diff --git a/renderer/page.go b/renderer/page.go index 940a322..20ca083 100644 --- a/renderer/page.go +++ b/renderer/page.go @@ -118,13 +118,14 @@ func MakePage(url string, res *gemini.Response, width int, proxied bool) (*struc } if mediatype == "text/gemini" { - rendered, links := RenderGemini(utfText, width, proxied) + rendered, links, maxPreCols := RenderGemini(utfText, width, proxied) return &structs.Page{ Mediatype: structs.TextGemini, RawMediatype: mediatype, URL: url, Raw: utfText, Content: rendered, + MaxPreCols: maxPreCols, Links: links, MadeAt: time.Now(), }, nil @@ -137,6 +138,7 @@ func MakePage(url string, res *gemini.Response, width int, proxied bool) (*struc URL: url, Raw: utfText, Content: RenderANSI(utfText), + MaxPreCols: -1, Links: []string{}, MadeAt: time.Now(), }, nil @@ -149,6 +151,7 @@ func MakePage(url string, res *gemini.Response, width int, proxied bool) (*struc URL: url, Raw: utfText, Content: RenderPlainText(utfText), + MaxPreCols: -1, Links: []string{}, MadeAt: time.Now(), }, nil diff --git a/renderer/renderer.go b/renderer/renderer.go index 9552835..2e31b6d 100644 --- a/renderer/renderer.go +++ b/renderer/renderer.go @@ -12,6 +12,7 @@ import ( "strings" "github.com/makeworld-the-better-one/amfora/config" + "github.com/mattn/go-runewidth" "github.com/spf13/viper" "gitlab.com/tslocum/cview" ) @@ -267,19 +268,19 @@ func convertRegularGemini(s string, numLinks, width int, proxied bool) (string, } // RenderGemini converts text/gemini into a cview displayable format. -// It also returns a slice of link URLs. +// It also returns a slice of link URLs, and the Page.MaxPreCols value. // // width is the number of columns to wrap to. // leftMargin is the number of blank spaces to prepend to each line. // // proxied is whether the request is through the gemini:// scheme. // If it's not a gemini:// page, set this to true. -func RenderGemini(s string, width int, proxied bool) (string, []string) { +func RenderGemini(s string, width int, proxied bool) (string, []string, int) { s = cview.Escape(s) lines := strings.Split(s, "\n") - links := make([]string, 0) + maxPreCols := 0 // Process and wrap non preformatted lines rendered := "" // Final result @@ -288,6 +289,11 @@ func RenderGemini(s string, width int, proxied bool) (string, []string) { // processPre is for rendering preformatted blocks processPre := func() { + lineWidth := runewidth.StringWidth(buf) + if lineWidth > maxPreCols { + maxPreCols = lineWidth + } + // Support ANSI color codes in preformatted blocks - see #59 if viper.GetBool("a-general.color") && viper.GetBool("a-general.ansi") { buf = cview.TranslateANSI(buf) @@ -348,5 +354,5 @@ func RenderGemini(s string, width int, proxied bool) (string, []string) { processRegular() } - return rendered, links + return rendered, links, maxPreCols } diff --git a/structs/structs.go b/structs/structs.go index 2bcca8f..d5f19e6 100644 --- a/structs/structs.go +++ b/structs/structs.go @@ -25,10 +25,11 @@ type Page struct { RawMediatype string // The actual mediatype sent by the server Raw string // The raw response, as received over the network Content string // The processed content, NOT raw. Uses cview color tags. It will also have a left margin. + MaxPreCols int // The amount of the terminal columns the longest preformatted line in Raw takes up, used for #197. -1 means infinite length lines, AKA always allow scrolling. Links []string // URLs, for each region in the content. - Row int // Scroll position - Column int // ditto - Width int // The terminal width when the Content was set, to know when reformatting should happen. + Row int // Vertical scroll position + Column int // Horizontal scroll position - does not map exactly to a cview.TextView because it includes left margin size changes, see #197 + TermWidth int // The terminal width when the Content was set, to know when reformatting should happen. Selected string // The current text or link selected SelectedID string // The cview region ID for the selected text/link Mode PageMode