1
0
mirror of https://github.com/makew0rld/amfora.git synced 2024-12-04 14:46:29 -05:00

🚧 Initial work

This commit is contained in:
makeworld 2021-02-27 18:17:49 -05:00
parent 4514f8b8c0
commit 8e7300726d
13 changed files with 196 additions and 99 deletions

View File

@ -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,
}
}

View File

@ -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()

View File

@ -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)

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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.

View File

@ -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

1
go.mod
View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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