🔀 Refactor to use tab struct

Squashed commit of the following:

commit 72f36afc9ea51b1d4fc6e24895bbc32b8b8cd872
Author: makeworld <colecmac@protonmail.com>
Date:   Tue Jul 7 16:15:45 2020 -0400

    🚧 Scroll is applied correctly when navigating around

commit 4b8982723f60294977f5e9d213fa3c8a51362356
Author: makeworld <colecmac@protonmail.com>
Date:   Tue Jul 7 15:34:45 2020 -0400

    🚧 Fix bottomBar code

    Make sure it always resets to a selected link if one was selected before

commit be09ffcf913662983a0362431d8e4765d06b4187
Author: makeworld <colecmac@protonmail.com>
Date:   Mon Jul 6 20:30:54 2020 -0400

    🚧 Switch to using tab pointers instead of ints

    Almost finished overall work.

commit ef8ab3da39eb2681f439337354b6b8f9fed74f15
Author: makeworld <colecmac@protonmail.com>
Date:   Mon Jul 6 12:10:50 2020 -0400

    🚧 Fixed some bugs, major ones remain

commit d3d47a344d54a9aac66acddd6ea9ca893ecc30df
Author: makeworld <colecmac@protonmail.com>
Date:   Sat Jul 4 20:58:46 2020 -0400

    🚧 Everything uses tab struct, no compile errors, untested

commit 44bf54c12f379524fe95073ca4eb1f3fb2c7195a
Author: makeworld <colecmac@protonmail.com>
Date:   Sat Jul 4 13:24:49 2020 -0400

    🚧 Initial work on tab struct
This commit is contained in:
makeworld 2020-07-07 21:13:45 -04:00
parent 518c35453a
commit 543d15abfc
10 changed files with 559 additions and 385 deletions

View File

@ -9,10 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Link and heading lines are wrapped just like regular text lines - Link and heading lines are wrapped just like regular text lines
- Wrapped list items are indented to stay behind the bullet (#35) - Wrapped list items are indented to stay behind the bullet (#35)
- Certificate expiry date is stored when the cert IDs match (#39) - Certificate expiry date is stored when the cert IDs match (#39)
- What link was selected is remembered as you browse through history
### Changed ### Changed
- Pages are rewrapped dynamically, whenever the terminal size changes (#33) - Pages are rewrapped dynamically, whenever the terminal size changes (#33)
### Fixed
- Many potential network and display race conditions eliminated
- Whether a tab is loading stays indicated when you switch away from it and go back
- Plain text documents are displayed faithfully (there were some edge conditions)
## [1.2.0] - 2020-07-02 ## [1.2.0] - 2020-07-02
### Added ### Added
- Alt-Left and Alt-Right for history navigation (#23) - Alt-Left and Alt-Right for history navigation (#23)

View File

@ -1,9 +1,10 @@
# Notes # Notes
- Simplify into one struct - URL for each tab should not be stored as a string - in the current code there's lots of reparsing the URL
- All the maps and stuff could be replaced with a `Tab` struct
- And then just one single map of tab number to `Tab` ## Issues
- URL for each tab should not be stored as a string - in the current code there's lots of reparsing the URL - Can't go back or do other things while page is loading - need a way to stop `handleURL`
- Change renderer to start style tags on each new line of wrapped link, to prevent left margin from being highlighted
## Upstream Bugs ## Upstream Bugs
- Wrapping messes up on brackets - Wrapping messes up on brackets

2
cache/cache.go vendored
View File

@ -111,7 +111,7 @@ func NumPages() int {
} }
// Get returns the page struct, and a bool indicating if the page was in the cache or not. // Get returns the page struct, and a bool indicating if the page was in the cache or not.
// An empty page struct is returned if the page isn't in the cache // An empty page struct is returned if the page isn't in the cache.
func Get(url string) (*structs.Page, bool) { func Get(url string) (*structs.Page, bool) {
lock.RLock() lock.RLock()
defer lock.RUnlock() defer lock.RUnlock()

View File

@ -96,7 +96,7 @@ func openBkmkModal(name string, exists bool) (string, int) {
} }
// Bookmarks displays the bookmarks page on the current tab. // Bookmarks displays the bookmarks page on the current tab.
func Bookmarks() { func Bookmarks(t *tab) {
// Gather bookmarks // Gather bookmarks
rawContent := "# Bookmarks\r\n\r\n" rawContent := "# Bookmarks\r\n\r\n"
m, keys := bookmarks.All() m, keys := bookmarks.All()
@ -113,27 +113,28 @@ func Bookmarks() {
Width: termW, Width: termW,
Mediatype: structs.TextGemini, Mediatype: structs.TextGemini,
} }
setPage(&page) setPage(t, &page)
t.applyBottomBar()
} }
// addBookmark goes through the process of adding a bookmark for the current page. // addBookmark goes through the process of adding a bookmark for the current page.
// It is the high-level way of doing it. It should be called in a goroutine. // It is the high-level way of doing it. It should be called in a goroutine.
// It can also be called to edit an existing bookmark. // It can also be called to edit an existing bookmark.
func addBookmark() { func addBookmark() {
if !strings.HasPrefix(tabMap[curTab].Url, "gemini://") { if !strings.HasPrefix(tabs[curTab].page.Url, "gemini://") {
// Can't make bookmarks for other kinds of URLs // Can't make bookmarks for other kinds of URLs
return return
} }
name, exists := bookmarks.Get(tabMap[curTab].Url) name, exists := bookmarks.Get(tabs[curTab].page.Url)
// Open a bookmark modal with the current name of the bookmark, if it exists // Open a bookmark modal with the current name of the bookmark, if it exists
newName, action := openBkmkModal(name, exists) newName, action := openBkmkModal(name, exists)
switch action { switch action {
case 1: case 1:
// Add/change the bookmark // Add/change the bookmark
bookmarks.Set(tabMap[curTab].Url, newName) bookmarks.Set(tabs[curTab].page.Url, newName)
case -1: case -1:
bookmarks.Remove(tabMap[curTab].Url) bookmarks.Remove(tabs[curTab].page.Url)
} }
// Other case is action = 0, meaning "Cancel", so nothing needs to happen // Other case is action = 0, meaning "Cancel", so nothing needs to happen
} }

View File

@ -6,7 +6,6 @@ import (
"path" "path"
"strconv" "strconv"
"strings" "strings"
"sync"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/makeworld-the-better-one/amfora/cache" "github.com/makeworld-the-better-one/amfora/cache"
@ -16,19 +15,13 @@ import (
"gitlab.com/tslocum/cview" "gitlab.com/tslocum/cview"
) )
var curTab = -1 // What number tab is currently visible, -1 means there are no tabs at all var tabs []*tab // Slice of all the current browser tabs
var tabMap = make(map[int]*structs.Page) // Map of tab number to page var curTab = -1 // What tab is currently visible - index for the tabs slice (-1 means there are no tabs)
// Holds the actual tab primitives
var tabViews = make(map[int]*cview.TextView)
// Terminal dimensions // Terminal dimensions
var termW int var termW int
var termH int var termH int
// The link currently selected when in link selection mode
// Set to "" when not in that mode
var selectedLink string
// The user input and URL display bar at the bottom // The user input and URL display bar at the bottom
var bottomBar = cview.NewInputField(). var bottomBar = cview.NewInputField().
SetFieldBackgroundColor(tcell.ColorWhite). SetFieldBackgroundColor(tcell.ColorWhite).
@ -72,9 +65,7 @@ var layout = cview.NewFlex().
var renderedNewTabContent string var renderedNewTabContent string
var newTabLinks []string var newTabLinks []string
var newTabPage *structs.Page var newTabPage structs.Page
var reformatMuts = make(map[int]*sync.Mutex) // Mutex for each tab
var App = cview.NewApplication(). var App = cview.NewApplication().
EnableMouse(false). EnableMouse(false).
@ -85,12 +76,12 @@ var App = cview.NewApplication().
termH = height termH = height
// Make sure the current tab content is reformatted when the terminal size changes // Make sure the current tab content is reformatted when the terminal size changes
go func(tab int) { go func(t *tab) {
reformatMuts[tab].Lock() // Only one reformat job per tab t.reformatMut.Lock() // Only one reformat job per tab
defer reformatMuts[tab].Unlock() defer t.reformatMut.Unlock()
// Use the current tab, but don't affect other tabs if the user switches tabs // Use the current tab, but don't affect other tabs if the user switches tabs
reformatPageAndSetView(tab, tabMap[tab]) reformatPageAndSetView(t, t.page)
}(curTab) }(tabs[curTab])
}) })
func Init() { func Init() {
@ -107,7 +98,15 @@ func Init() {
} }
bottomBar.SetBackgroundColor(tcell.ColorWhite) bottomBar.SetBackgroundColor(tcell.ColorWhite)
bottomBar.SetDoneFunc(func(key tcell.Key) { bottomBar.SetDoneFunc(func(key tcell.Key) {
defer bottomBar.SetLabel("") tab := curTab
// Reset func to set the bottomBar back to what it was before
// Use for errors.
reset := func() {
bottomBar.SetLabel("")
tabs[tab].applyAll()
App.SetFocus(tabs[tab].view)
}
switch key { switch key {
case tcell.KeyEnter: case tcell.KeyEnter:
@ -118,21 +117,19 @@ func Init() {
if strings.TrimSpace(query) == "" { if strings.TrimSpace(query) == "" {
// Ignore // Ignore
bottomBar.SetText(tabMap[curTab].Url) reset()
App.SetFocus(tabViews[curTab])
return return
} }
if query == ".." && tabHasContent() { if query == ".." && tabs[tab].hasContent() {
// Go up a directory // Go up a directory
parsed, err := url.Parse(tabMap[curTab].Url) parsed, err := url.Parse(tabs[tab].page.Url)
if err != nil { if err != nil {
// This shouldn't occur // This shouldn't occur
return return
} }
if parsed.Path == "/" { if parsed.Path == "/" {
// Can't go up further // Can't go up further
bottomBar.SetText(tabMap[curTab].Url) reset()
App.SetFocus(tabViews[curTab])
return return
} }
@ -152,17 +149,19 @@ func Init() {
// They're trying to open a link number in a new tab // They're trying to open a link number in a new tab
i, err = strconv.Atoi(query[4:]) i, err = strconv.Atoi(query[4:])
if err != nil { if err != nil {
reset()
return return
} }
if i <= len(tabMap[curTab].Links) && i > 0 { if i <= len(tabs[tab].page.Links) && i > 0 {
// Open new tab and load link // Open new tab and load link
oldTab := curTab oldTab := tab
NewTab() NewTab()
// Resolve and follow link manually // Resolve and follow link manually
prevParsed, _ := url.Parse(tabMap[oldTab].Url) prevParsed, _ := url.Parse(tabs[oldTab].page.Url)
nextParsed, err := url.Parse(tabMap[oldTab].Links[i-1]) nextParsed, err := url.Parse(tabs[oldTab].page.Links[i-1])
if err != nil { if err != nil {
Error("URL Error", "link URL could not be parsed") Error("URL Error", "link URL could not be parsed")
reset()
return return
} }
URL(prevParsed.ResolveReference(nextParsed).String()) URL(prevParsed.ResolveReference(nextParsed).String())
@ -172,7 +171,7 @@ func Init() {
// It's a full URL or search term // It's a full URL or search term
// Detect if it's a search or URL // Detect if it's a search or URL
if strings.Contains(query, " ") || (!strings.Contains(query, "//") && !strings.Contains(query, ".") && !strings.HasPrefix(query, "about:")) { if strings.Contains(query, " ") || (!strings.Contains(query, "//") && !strings.Contains(query, ".") && !strings.HasPrefix(query, "about:")) {
u := viper.GetString("a-general.search") + "?" + pathEscape(query) u := viper.GetString("a-general.search") + "?" + queryEscape(query)
cache.Remove(u) // Don't use the cached version of the search cache.Remove(u) // Don't use the cached version of the search
URL(u) URL(u)
} else { } else {
@ -183,26 +182,26 @@ func Init() {
return return
} }
} }
if i <= len(tabMap[curTab].Links) && i > 0 { if i <= len(tabs[tab].page.Links) && i > 0 {
// It's a valid link number // It's a valid link number
followLink(tabMap[curTab].Url, tabMap[curTab].Links[i-1]) followLink(tabs[tab], tabs[tab].page.Url, tabs[tab].page.Links[i-1])
return return
} }
// Invalid link number, don't do anything // Invalid link number, don't do anything
bottomBar.SetText(tabMap[curTab].Url) reset()
App.SetFocus(tabViews[curTab]) return
case tcell.KeyEscape: case tcell.KeyEscape:
// Set back to what it was // Set back to what it was
bottomBar.SetText(tabMap[curTab].Url) reset()
App.SetFocus(tabViews[curTab]) return
} }
// Other potential keys are Tab and Backtab, they are ignored // Other potential keys are Tab and Backtab, they are ignored
}) })
// Render the default new tab content ONCE and store it for later // Render the default new tab content ONCE and store it for later
renderedNewTabContent, newTabLinks = renderer.RenderGemini(newTabContent, textWidth(), leftMargin()) renderedNewTabContent, newTabLinks = renderer.RenderGemini(newTabContent, textWidth(), leftMargin())
newTabPage = &structs.Page{ newTabPage = structs.Page{
Raw: newTabContent, Raw: newTabContent,
Content: renderedNewTabContent, Content: renderedNewTabContent,
Links: newTabLinks, Links: newTabLinks,
@ -227,87 +226,100 @@ func Init() {
return event return event
} }
// History arrow keys if tabs[curTab].mode == tabModeDone {
if event.Modifiers() == tcell.ModAlt { // All the keys and operations that can only work while NOT loading
if event.Key() == tcell.KeyLeft {
histBack()
return nil
}
if event.Key() == tcell.KeyRight {
histForward()
return nil
}
}
switch event.Key() { // History arrow keys
case tcell.KeyCtrlT: if event.Modifiers() == tcell.ModAlt {
if selectedLink == "" { if event.Key() == tcell.KeyLeft {
NewTab() histBack(tabs[curTab])
} else { return nil
next, err := resolveRelLink(tabMap[curTab].Url, selectedLink) }
if err != nil { if event.Key() == tcell.KeyRight {
Error("URL Error", err.Error()) histForward(tabs[curTab])
return nil return nil
} }
NewTab()
URL(next)
} }
return nil
switch event.Key() {
case tcell.KeyCtrlT:
if tabs[curTab].page.Mode == structs.ModeLinkSelect {
next, err := resolveRelLink(tabs[curTab], tabs[curTab].page.Url, tabs[curTab].page.Selected)
if err != nil {
Error("URL Error", err.Error())
return nil
}
NewTab()
URL(next)
} else {
NewTab()
}
return nil
case tcell.KeyCtrlR:
Reload()
return nil
case tcell.KeyCtrlH:
URL(viper.GetString("a-general.home"))
return nil
case tcell.KeyCtrlB:
Bookmarks(tabs[curTab])
tabs[curTab].addToHistory("about:bookmarks")
return nil
case tcell.KeyCtrlD:
go addBookmark()
return nil
case tcell.KeyPgUp:
tabs[curTab].pageUp()
return nil
case tcell.KeyPgDn:
tabs[curTab].pageDown()
return nil
case tcell.KeyRune:
// Regular key was sent
switch string(event.Rune()) {
case " ":
// Space starts typing, like Bombadillo
bottomBar.SetLabel("[::b]URL/Num./Search: [::-]")
bottomBar.SetText("")
// Don't save bottom bar, so that whenever you switch tabs, it's not in that mode
App.SetFocus(bottomBar)
return nil
case "R":
Reload()
return nil
case "b":
histBack(tabs[curTab])
return nil
case "f":
histForward(tabs[curTab])
return nil
case "u":
tabs[curTab].pageUp()
return nil
case "d":
tabs[curTab].pageDown()
return nil
}
}
}
// All the keys and operations that can work while a tab IS loading
switch event.Key() {
case tcell.KeyCtrlW: case tcell.KeyCtrlW:
CloseTab() CloseTab()
return nil return nil
case tcell.KeyCtrlR:
Reload()
return nil
case tcell.KeyCtrlH:
URL(viper.GetString("a-general.home"))
return nil
case tcell.KeyCtrlQ: case tcell.KeyCtrlQ:
Stop() Stop()
return nil return nil
case tcell.KeyCtrlB:
Bookmarks()
addToHist("about:bookmarks")
return nil
case tcell.KeyCtrlD:
go addBookmark()
return nil
case tcell.KeyPgUp:
pageUp()
return nil
case tcell.KeyPgDn:
pageDown()
return nil
case tcell.KeyRune: case tcell.KeyRune:
// Regular key was sent // Regular key was sent
switch string(event.Rune()) { switch string(event.Rune()) {
case " ":
// Space starts typing, like Bombadillo
bottomBar.SetLabel("[::b]URL/Num./Search: [::-]")
bottomBar.SetText("")
App.SetFocus(bottomBar)
return nil
case "q": case "q":
Stop() Stop()
return nil return nil
case "R":
Reload()
return nil
case "b":
histBack()
return nil
case "f":
histForward()
return nil
case "?": case "?":
Help() Help()
return nil return nil
case "u":
pageUp()
return nil
case "d":
pageDown()
return nil
// Shift+NUMBER keys, for switching to a specific tab // Shift+NUMBER keys, for switching to a specific tab
case "!": case "!":
@ -342,6 +354,8 @@ func Init() {
return nil return nil
} }
} }
// Let another element handle the event, it's not a special global key
return event return event
}) })
} }
@ -355,87 +369,31 @@ func Stop() {
// NewTab opens a new tab and switches to it, displaying the // NewTab opens a new tab and switches to it, displaying the
// the default empty content because there's no URL. // the default empty content because there's no URL.
func NewTab() { func NewTab() {
// Create TextView in tabViews and change curTab // Create TextView and change curTab
// Set the textView options, and the changed func to App.Draw() // Set the TextView options, and the changed func to App.Draw()
// SetDoneFunc to do link highlighting // SetDoneFunc to do link highlighting
// Add view to pages and switch to it // Add view to pages and switch to it
// But first, turn off link selecting mode in the current tab // Process current tab before making a new one
if curTab > -1 { if curTab > -1 {
tabViews[curTab].Highlight("") // Turn off link selecting mode in the current tab
tabs[curTab].view.Highlight("")
// Save bottomBar state
tabs[curTab].saveBottomBar()
} }
selectedLink = ""
curTab = NumTabs() curTab = NumTabs()
reformatPage(newTabPage)
tabMap[curTab] = newTabPage
reformatMuts[curTab] = &sync.Mutex{}
tabViews[curTab] = cview.NewTextView().
SetDynamicColors(true).
SetRegions(true).
SetScrollable(true).
SetWrap(false).
SetText(tabMap[curTab].Content).
ScrollToBeginning().
SetChangedFunc(func() {
App.Draw()
}).
SetDoneFunc(func(key tcell.Key) {
// Altered from: https://gitlab.com/tslocum/cview/-/blob/master/demos/textview/main.go
// Handles being able to select and "click" links with the enter and tab keys
if key == tcell.KeyEsc { tabs = append(tabs, makeNewTab())
// Stop highlighting temp := newTabPage // Copy
tabViews[curTab].Highlight("") setPage(tabs[curTab], &temp)
bottomBar.SetLabel("")
bottomBar.SetText(tabMap[curTab].Url)
selectedLink = ""
}
currentSelection := tabViews[curTab].GetHighlights()
numSelections := len(tabMap[curTab].Links)
if key == tcell.KeyEnter {
if len(currentSelection) > 0 && len(tabMap[curTab].Links) > 0 {
// A link was selected, "click" it and load the page it's for
bottomBar.SetLabel("")
selectedLink = ""
linkN, _ := strconv.Atoi(currentSelection[0])
followLink(tabMap[curTab].Url, tabMap[curTab].Links[linkN])
return
} else {
tabViews[curTab].Highlight("0").ScrollToHighlight()
// Display link URL in bottomBar
bottomBar.SetLabel("[::b]Link: [::-]")
bottomBar.SetText(tabMap[curTab].Links[0])
selectedLink = tabMap[curTab].Links[0]
}
} else if len(currentSelection) > 0 {
// There's still a selection, but a different key was pressed, not Enter
index, _ := strconv.Atoi(currentSelection[0])
if key == tcell.KeyTab {
index = (index + 1) % numSelections
} else if key == tcell.KeyBacktab {
index = (index - 1 + numSelections) % numSelections
} else {
return
}
tabViews[curTab].Highlight(strconv.Itoa(index)).ScrollToHighlight()
// Display link URL in bottomBar
bottomBar.SetLabel("[::b]Link: [::-]")
bottomBar.SetText(tabMap[curTab].Links[index])
selectedLink = tabMap[curTab].Links[index]
}
})
tabHist[curTab] = []string{}
// Can't go backwards, but this isn't the first page either. // Can't go backwards, but this isn't the first page either.
// The first page will be the next one the user goes to. // The first page will be the next one the user goes to.
tabHistPos[curTab] = -1 tabs[curTab].history.pos = -1
tabPages.AddAndSwitchToPage(strconv.Itoa(curTab), tabViews[curTab], true) tabPages.AddAndSwitchToPage(strconv.Itoa(curTab), tabs[curTab].view, true)
App.SetFocus(tabViews[curTab]) App.SetFocus(tabs[curTab].view)
// Add tab number to the actual place where tabs are show on the screen // Add tab number to the actual place where tabs are show on the screen
// Tab regions are 0-indexed but text displayed on the screen starts at 1 // Tab regions are 0-indexed but text displayed on the screen starts at 1
@ -448,6 +406,7 @@ func NewTab() {
bottomBar.SetLabel("") bottomBar.SetLabel("")
bottomBar.SetText("") bottomBar.SetText("")
tabs[curTab].saveBottomBar()
// Draw just in case // Draw just in case
App.Draw() App.Draw()
@ -470,13 +429,8 @@ func CloseTab() {
return return
} }
delete(tabMap, curTab) tabs = tabs[:len(tabs)-1]
tabPages.RemovePage(strconv.Itoa(curTab)) tabPages.RemovePage(strconv.Itoa(curTab))
delete(tabViews, curTab)
delete(reformatMuts, curTab)
delete(tabHist, curTab)
delete(tabHistPos, curTab)
if curTab <= 0 { if curTab <= 0 {
curTab = NumTabs() - 1 curTab = NumTabs() - 1
@ -498,8 +452,10 @@ func CloseTab() {
} }
tabRow.Highlight(strconv.Itoa(curTab)).ScrollToHighlight() tabRow.Highlight(strconv.Itoa(curTab)).ScrollToHighlight()
bottomBar.SetLabel("") // Restore previous tab's state
bottomBar.SetText(tabMap[curTab].Url) tabs[curTab].applyAll()
App.SetFocus(tabs[curTab].view)
// Just in case // Just in case
App.Draw() App.Draw()
@ -516,25 +472,39 @@ func SwitchTab(tab int) {
tab = NumTabs() - 1 tab = NumTabs() - 1
} }
// Save current tab attributes
if curTab > -1 {
// Save bottomBar state
tabs[curTab].saveBottomBar()
}
curTab = tab % NumTabs() curTab = tab % NumTabs()
reformatPageAndSetView(curTab, tabMap[curTab])
// Display tab
reformatPageAndSetView(tabs[curTab], tabs[curTab].page)
tabPages.SwitchToPage(strconv.Itoa(curTab)) tabPages.SwitchToPage(strconv.Itoa(curTab))
tabRow.Highlight(strconv.Itoa(curTab)).ScrollToHighlight() tabRow.Highlight(strconv.Itoa(curTab)).ScrollToHighlight()
tabs[curTab].applyAll()
bottomBar.SetLabel("") App.SetFocus(tabs[curTab].view)
bottomBar.SetText(tabMap[curTab].Url)
// Just in case // Just in case
App.Draw() App.Draw()
} }
func Reload() { func Reload() {
if !tabHasContent() { if !tabs[curTab].hasContent() {
return return
} }
cache.Remove(tabMap[curTab].Url) go cache.Remove(tabs[curTab].page.Url)
go handleURL(tabMap[curTab].Url) go func(t *tab) {
handleURL(t, t.page.Url) // goURL is not used bc history shouldn't be added to
if t == tabs[curTab] {
// Display the bottomBar state that handleURL set
t.applyBottomBar()
}
}(tabs[curTab])
} }
// URL loads and handles the provided URL for the current tab. // URL loads and handles the provided URL for the current tab.
@ -543,12 +513,13 @@ func URL(u string) {
// Some code is copied in followLink() // Some code is copied in followLink()
if u == "about:bookmarks" { if u == "about:bookmarks" {
Bookmarks() Bookmarks(tabs[curTab])
addToHist("about:bookmarks") tabs[curTab].addToHistory("about:bookmarks")
return return
} }
if u == "about:newtab" { if u == "about:newtab" {
setPage(newTabPage) temp := newTabPage // Copy
setPage(tabs[curTab], &temp)
return return
} }
if strings.HasPrefix(u, "about:") { if strings.HasPrefix(u, "about:") {
@ -556,14 +527,9 @@ func URL(u string) {
return return
} }
go func() { go goURL(tabs[curTab], u)
final, displayed := handleURL(u)
if displayed {
addToHist(final)
}
}()
} }
func NumTabs() int { func NumTabs() int {
return len(tabViews) return len(tabs)
} }

View File

@ -1,44 +1,25 @@
package display package display
// Tab number mapped to list of URLs ordered from first to most recent. // applyHist is a history.go internal function, to load a URL in the history.
var tabHist = make(map[int][]string) func applyHist(t *tab) {
handleURL(t, t.history.urls[t.history.pos]) // Load that position in history
// Tab number mapped to where in its history you are. t.applyAll()
// The value is a valid index of the string slice above.
var tabHistPos = make(map[int]int)
// addToHist adds the given URL to history.
// It assumes the URL is currently being loaded and displayed on the page.
func addToHist(u string) {
if tabHistPos[curTab] < len(tabHist[curTab])-1 {
// We're somewhere in the middle of the history instead, with URLs ahead and behind.
// The URLs ahead need to be removed so this new URL is the most recent item in the history
tabHist[curTab] = tabHist[curTab][:tabHistPos[curTab]+1]
}
tabHist[curTab] = append(tabHist[curTab], u)
tabHistPos[curTab]++
} }
func histForward() { func histForward(t *tab) {
if tabHistPos[curTab] >= len(tabHist[curTab])-1 { if t.history.pos >= len(t.history.urls)-1 {
// Already on the most recent URL in the history // Already on the most recent URL in the history
return return
} }
tabHistPos[curTab]++ t.history.pos++
go func() { go applyHist(t)
handleURL(tabHist[curTab][tabHistPos[curTab]])
applyScroll()
}()
} }
func histBack() { func histBack(t *tab) {
if tabHistPos[curTab] <= 0 { if t.history.pos <= 0 {
// First tab in history // First tab in history
return return
} }
tabHistPos[curTab]-- t.history.pos--
go func() { go applyHist(t)
handleURL(tabHist[curTab][tabHistPos[curTab]])
applyScroll()
}()
} }

View File

@ -18,16 +18,15 @@ import (
// This file contains the functions that aren't part of the public API. // This file contains the functions that aren't part of the public API.
// pageUp scrolls up 75% of the height of the terminal, like Bombadillo. // isValidTab indicates whether the passed tab is still being used, even if it's not currently displayed.
func pageUp() { func isValidTab(t *tab) bool {
row, col := tabViews[curTab].GetScrollOffset() tempTabs := tabs
tabViews[curTab].ScrollTo(row-(termH/4)*3, col) for i := range tempTabs {
} if tempTabs[i] == t {
return true
// pageDown scrolls down 75% of the height of the terminal, like Bombadillo. }
func pageDown() { }
row, col := tabViews[curTab].GetScrollOffset() return false
tabViews[curTab].ScrollTo(row+(termH/4)*3, col)
} }
func leftMargin() int { func leftMargin() int {
@ -54,55 +53,17 @@ func textWidth() int {
return viper.GetInt("a-general.max_width") return viper.GetInt("a-general.max_width")
} }
// pathEscape is the same as url.PathEscape, but it also replaces the +. // queryEscape is the same as url.PathEscape, but it also replaces the +.
func pathEscape(path string) string { // This is because Gemini requires percent-escaping for queries.
func queryEscape(path string) string {
return strings.ReplaceAll(url.PathEscape(path), "+", "%2B") return strings.ReplaceAll(url.PathEscape(path), "+", "%2B")
} }
// tabHasContent returns true when the current tab has a page being displayed.
// The most likely situation where false would be returned is when the default
// new tab content is being displayed.
func tabHasContent() bool {
if curTab < 0 {
return false
}
if len(tabViews) < curTab {
// There isn't a TextView for the current tab number
return false
}
if tabMap[curTab].Url == "" {
// Likely the default content page
return false
}
if strings.HasPrefix(tabMap[curTab].Url, "about:") {
return false
}
_, ok := tabMap[curTab]
return ok // If there's a page, return true
}
// saveScroll saves where in the page the user was.
// It should be used whenever moving from one page to another.
func saveScroll() {
// It will also be saved in the cache because the cache uses the same pointer
row, col := tabViews[curTab].GetScrollOffset()
tabMap[curTab].Row = row
tabMap[curTab].Column = col
}
// applyScroll applies the saved scroll values to the current page and tab.
// It should only be used when going backward and forward, not when
// loading a new page (that might have scroll vals cached anyway).
func applyScroll() {
tabViews[curTab].ScrollTo(tabMap[curTab].Row, tabMap[curTab].Column)
}
// resolveRelLink returns an absolute link for the given absolute link and relative one. // resolveRelLink returns an absolute link for the given absolute link and relative one.
// It also returns an error if it could not resolve the links, which should be displayed // It also returns an error if it could not resolve the links, which should be displayed
// to the user. // to the user.
func resolveRelLink(prev, next string) (string, error) { func resolveRelLink(t *tab, prev, next string) (string, error) {
if !tabHasContent() { if !t.hasContent() {
return next, nil return next, nil
} }
@ -116,12 +77,13 @@ func resolveRelLink(prev, next string) (string, error) {
// followLink should be used when the user "clicks" a link on a page. // followLink should be used when the user "clicks" a link on a page.
// Not when a URL is opened on a new tab for the first time. // Not when a URL is opened on a new tab for the first time.
func followLink(prev, next string) { // It will handle setting the bottomBar.
func followLink(t *tab, prev, next string) {
// Copied from URL() // Copied from URL()
if next == "about:bookmarks" { if next == "about:bookmarks" {
Bookmarks() Bookmarks(t)
addToHist("about:bookmarks") t.addToHistory("about:bookmarks")
return return
} }
if strings.HasPrefix(next, "about:") { if strings.HasPrefix(next, "about:") {
@ -129,19 +91,14 @@ func followLink(prev, next string) {
return return
} }
if tabHasContent() { if t.hasContent() {
saveScroll() // Likely called later on, it's here just in case t.saveScroll() // Likely called later on, it's here just in case
nextURL, err := resolveRelLink(prev, next) nextURL, err := resolveRelLink(t, prev, next)
if err != nil { if err != nil {
Error("URL Error", err.Error()) Error("URL Error", err.Error())
return return
} }
go func() { go goURL(t, nextURL)
final, displayed := handleURL(nextURL)
if displayed {
addToHist(final)
}
}()
return return
} }
// No content on current tab, so the "prev" URL is not valid. // No content on current tab, so the "prev" URL is not valid.
@ -151,12 +108,7 @@ func followLink(prev, next string) {
Error("URL Error", "Link URL could not be parsed") Error("URL Error", "Link URL could not be parsed")
return return
} }
go func() { go goURL(t, next)
final, displayed := handleURL(next)
if displayed {
addToHist(final)
}
}()
} }
// reformatPage will take the raw page content and reformat it according to the current terminal dimensions. // reformatPage will take the raw page content and reformat it according to the current terminal dimensions.
@ -185,30 +137,54 @@ func reformatPage(p *structs.Page) {
// reformatPageAndSetView is for reformatting a page that is already being displayed. // 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. // setPage should be used when a page is being loaded for the first time.
func reformatPageAndSetView(tab int, p *structs.Page) { func reformatPageAndSetView(t *tab, p *structs.Page) {
saveScroll() t.saveScroll()
reformatPage(p) reformatPage(p)
tabViews[tab].SetText(p.Content) t.view.SetText(p.Content)
applyScroll() // Go back to where you were, roughly t.applyScroll() // Go back to where you were, roughly
} }
// setPage displays a Page on the current tab. // setPage displays a Page on the passed tab number.
func setPage(p *structs.Page) { // The bottomBar is not actually changed in this func
saveScroll() // Save the scroll of the previous page func setPage(t *tab, p *structs.Page) {
if !isValidTab(t) {
// Don't waste time reformatting an invalid tab
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 // Make sure the page content is fitted to the terminal every time it's displayed
reformatPage(p) reformatPage(p)
// Change page on screen // Change page on screen
tabMap[curTab] = p t.page = p
tabViews[curTab].SetText(p.Content) t.view.SetText(p.Content)
tabViews[curTab].Highlight("") // Turn off highlights t.view.Highlight("") // Turn off highlights, other funcs may restore if necessary
tabViews[curTab].ScrollToBeginning() t.view.ScrollToBeginning()
// Setup display // Setup display
App.SetFocus(tabViews[curTab]) App.SetFocus(t.view)
bottomBar.SetLabel("")
bottomBar.SetText(p.Url) // Save bottom bar for the tab - TODO: other funcs will apply/display it
t.barLabel = ""
t.barText = p.Url
}
// goURL is like handleURL, but takes care of history and the bottomBar.
// It should be preferred over handleURL in most cases.
// It has no return values to be processed.
//
// It should be called in a goroutine.
func goURL(t *tab, u string) {
final, displayed := handleURL(t, u)
if displayed {
t.addToHistory(final)
}
if t == tabs[curTab] {
// Display the bottomBar state that handleURL set
t.applyBottomBar()
}
} }
// handleURL displays whatever action is needed for the provided URL, // handleURL displays whatever action is needed for the provided URL,
@ -220,15 +196,36 @@ func setPage(p *structs.Page) {
// If there is some error, it will return "". // If there is some error, it will return "".
// The second returned item is a bool indicating if page content was displayed. // The second returned item is a bool indicating if page content was displayed.
// It returns false for Errors, other protocols, etc. // It returns false for Errors, other protocols, etc.
func handleURL(u string) (string, bool) { //
// The bottomBar is not actually changed in this func, except during loading.
// The func that calls this one should apply the bottomBar values if necessary.
func handleURL(t *tab, u string) (string, bool) {
defer App.Draw() // Just in case defer App.Draw() // Just in case
App.SetFocus(tabViews[curTab]) // Save for resetting on error
oldLable := t.barLabel
oldText := t.barText
// Custom return function
ret := func(s string, b bool) (string, bool) {
if !b {
// Reset bottomBar if page wasn't loaded
t.barLabel = oldLable
t.barText = oldText
}
t.mode = tabModeDone
return s, b
}
t.barLabel = ""
bottomBar.SetLabel("")
App.SetFocus(t.view)
// To allow linking to the bookmarks page, and history browsing // To allow linking to the bookmarks page, and history browsing
if u == "about:bookmarks" { if u == "about:bookmarks" {
Bookmarks() Bookmarks(t)
return "about:bookmarks", true return ret("about:bookmarks", true)
} }
u = normalizeURL(u) u = normalizeURL(u)
@ -236,8 +233,7 @@ func handleURL(u string) (string, bool) {
parsed, err := url.Parse(u) parsed, err := url.Parse(u)
if err != nil { if err != nil {
Error("URL Error", err.Error()) Error("URL Error", err.Error())
bottomBar.SetText(tabMap[curTab].Url) return ret("", false)
return "", false
} }
if strings.HasPrefix(u, "http") { if strings.HasPrefix(u, "http") {
@ -259,27 +255,33 @@ func handleURL(u string) (string, bool) {
Error("HTTP Error", "Error executing custom browser command: "+err.Error()) Error("HTTP Error", "Error executing custom browser command: "+err.Error())
} }
} }
bottomBar.SetText(tabMap[curTab].Url) return ret("", false)
return "", false
} }
if !strings.HasPrefix(u, "gemini") { if !strings.HasPrefix(u, "gemini") {
Error("Protocol Error", "Only gemini and HTTP are supported. URL was "+u) Error("Protocol Error", "Only gemini and HTTP are supported. URL was "+u)
bottomBar.SetText(tabMap[curTab].Url) return ret("", false)
return "", false
} }
// Gemini URL // Gemini URL
// Load page from cache if possible // Load page from cache if possible
page, ok := cache.Get(u) page, ok := cache.Get(u)
if ok { if ok {
setPage(page) setPage(t, page)
return u, true return ret(u, true)
} }
// Otherwise download it // Otherwise download it
bottomBar.SetText("Loading...") bottomBar.SetText("Loading...")
t.barText = "Loading..." // Save it too, in case the tab switches during loading
t.mode = tabModeLoading
App.Draw() App.Draw()
res, err := client.Fetch(u) res, err := client.Fetch(u)
// Loading may have taken a while, make sure tab is still valid
if !isValidTab(t) {
return ret("", false)
}
if err == client.ErrTofu { if err == client.ErrTofu {
if Tofu(parsed.Host) { if Tofu(parsed.Host) {
// They want to continue anyway // They want to continue anyway
@ -287,36 +289,31 @@ func handleURL(u string) (string, bool) {
// Response can be used further down, no need to reload // Response can be used further down, no need to reload
} else { } else {
// They don't want to continue // They don't want to continue
// Set the bar back to original URL return ret("", false)
bottomBar.SetText(tabMap[curTab].Url)
return "", false
} }
} else if err != nil { } else if err != nil {
Error("URL Fetch Error", err.Error()) Error("URL Fetch Error", err.Error())
// Set the bar back to original URL return ret("", false)
bottomBar.SetText(tabMap[curTab].Url)
return "", false
} }
if renderer.CanDisplay(res) { if renderer.CanDisplay(res) {
page, err := renderer.MakePage(u, res, textWidth(), leftMargin()) page, err := renderer.MakePage(u, res, textWidth(), leftMargin())
// Rendering may have taken a while, make sure tab is still valid
if !isValidTab(t) {
return ret("", false)
}
page.Width = termW page.Width = termW
if err != nil { if err != nil {
Error("Page Error", "Issuing creating page: "+err.Error()) Error("Page Error", "Issuing creating page: "+err.Error())
// Set the bar back to original URL return ret("", false)
bottomBar.SetText(tabMap[curTab].Url)
return "", false
} }
cache.Add(page) go cache.Add(page)
setPage(page) setPage(t, page)
return u, true return ret(u, true)
} }
// Not displayable // Not displayable
// Could be a non 20 (or 21) status code, or a different kind of document // Could be a non 20 (or 21) status code, or a different kind of document
// Set the bar back to original URL
bottomBar.SetText(tabMap[curTab].Url)
App.Draw()
// Handle each status code // Handle each status code
switch gemini.SimplifyStatus(res.Status) { switch gemini.SimplifyStatus(res.Status) {
case 10: case 10:
@ -324,36 +321,35 @@ func handleURL(u string) (string, bool) {
if ok { if ok {
// Make another request with the query string added // Make another request with the query string added
// + chars are replaced because PathEscape doesn't do that // + chars are replaced because PathEscape doesn't do that
parsed.RawQuery = pathEscape(userInput) parsed.RawQuery = queryEscape(userInput)
if len(parsed.String()) > 1024 { if len(parsed.String()) > 1024 {
// 1024 is the max size for URLs in the spec // 1024 is the max size for URLs in the spec
Error("Input Error", "URL for that input would be too long.") Error("Input Error", "URL for that input would be too long.")
return "", false return ret("", false)
} }
return handleURL(parsed.String()) return ret(handleURL(t, parsed.String()))
} }
return "", false return ret("", false)
case 30: case 30:
parsedMeta, err := url.Parse(res.Meta) parsedMeta, err := url.Parse(res.Meta)
if err != nil { if err != nil {
Error("Redirect Error", "Invalid URL: "+err.Error()) Error("Redirect Error", "Invalid URL: "+err.Error())
return "", false return ret("", false)
} }
redir := parsed.ResolveReference(parsedMeta).String() redir := parsed.ResolveReference(parsedMeta).String()
if YesNo("Follow redirect?\n" + redir) { if YesNo("Follow redirect?\n" + redir) {
return handleURL(redir) return handleURL(t, redir)
} }
return "", false return ret("", false)
case 40: case 40:
Error("Temporary Failure", cview.Escape(res.Meta)) // Escaped just in case, to not allow malicious meta strings Error("Temporary Failure", cview.Escape(res.Meta))
return "", false return ret("", false)
case 50: case 50:
Error("Permanent Failure", cview.Escape(res.Meta)) Error("Permanent Failure", cview.Escape(res.Meta))
return "", false return ret("", false)
case 60: case 60:
Info("The server requested a certificate. Cert handling is coming to Amfora soon!") Info("The server requested a certificate. Cert handling is coming to Amfora soon!")
return "", false return ret("", false)
} }
// Status code 20, but not a document that can be displayed // Status code 20, but not a document that can be displayed
yes := YesNo("This type of file can't be displayed. Downloading will be implemented soon. Would like to open the file in a HTTPS proxy for now?") yes := YesNo("This type of file can't be displayed. Downloading will be implemented soon. Would like to open the file in a HTTPS proxy for now?")
@ -376,7 +372,7 @@ func handleURL(u string) (string, bool) {
} }
App.Draw() App.Draw()
} }
return "", false return ret("", false)
} }
// normalizeURL attempts to make URLs that are different strings // normalizeURL attempts to make URLs that are different strings

230
display/tab.go Normal file
View File

@ -0,0 +1,230 @@
package display
import (
"strconv"
"strings"
"sync"
"github.com/gdamore/tcell"
"github.com/makeworld-the-better-one/amfora/structs"
"gitlab.com/tslocum/cview"
)
type tabMode int
const (
tabModeDone tabMode = iota
tabModeLoading
)
type tabHistory struct {
urls []string
pos int // Position: where in the list of URLs we are
}
// tab hold the information needed for each browser tab.
type tab struct {
page *structs.Page
view *cview.TextView
history *tabHistory
mode tabMode
reformatMut *sync.Mutex // Mutex for reformatting, so there's only one reformat job at once
barLabel string // The bottomBar label for the tab
barText string // The bottomBar text for the tab
}
// makeNewTab initializes an tab struct with no content.
func makeNewTab() *tab {
t := tab{
page: &structs.Page{Mode: structs.ModeOff},
view: cview.NewTextView().
SetDynamicColors(true).
SetRegions(true).
SetScrollable(true).
SetWrap(false).
SetChangedFunc(func() {
App.Draw()
}),
history: &tabHistory{},
reformatMut: &sync.Mutex{},
mode: tabModeDone,
}
t.view.SetDoneFunc(func(key tcell.Key) {
// Altered from: https://gitlab.com/tslocum/cview/-/blob/master/demos/textview/main.go
// Handles being able to select and "click" links with the enter and tab keys
tab := curTab // Don't let it change in the middle of the code
if key == tcell.KeyEsc && tabs[tab].mode == tabModeDone {
// Stop highlighting
bottomBar.SetLabel("")
bottomBar.SetText(tabs[tab].page.Url)
tabs[tab].clearSelected()
tabs[tab].saveBottomBar()
return
}
if len(tabs[tab].page.Links) <= 0 {
// No links on page
return
}
currentSelection := tabs[tab].view.GetHighlights()
numSelections := len(tabs[tab].page.Links)
if key == tcell.KeyEnter {
if len(currentSelection) > 0 {
// A link was selected, "click" it and load the page it's for
bottomBar.SetLabel("")
linkN, _ := strconv.Atoi(currentSelection[0])
tabs[tab].page.Selected = tabs[tab].page.Links[linkN]
tabs[tab].page.SelectedID = currentSelection[0]
followLink(tabs[tab], tabs[tab].page.Url, tabs[tab].page.Links[linkN])
return
} else {
// They've started link highlighting
tabs[tab].page.Mode = structs.ModeLinkSelect
tabs[tab].view.Highlight("0").ScrollToHighlight()
// Display link URL in bottomBar
bottomBar.SetLabel("[::b]Link: [::-]")
bottomBar.SetText(tabs[tab].page.Links[0])
tabs[tab].saveBottomBar()
tabs[tab].page.Selected = tabs[tab].page.Links[0]
tabs[tab].page.SelectedID = "0"
}
} else if len(currentSelection) > 0 {
// There's still a selection, but a different key was pressed, not Enter
index, _ := strconv.Atoi(currentSelection[0])
if key == tcell.KeyTab {
index = (index + 1) % numSelections
} else if key == tcell.KeyBacktab {
index = (index - 1 + numSelections) % numSelections
} else {
return
}
tabs[tab].view.Highlight(strconv.Itoa(index)).ScrollToHighlight()
// Display link URL in bottomBar
bottomBar.SetLabel("[::b]Link: [::-]")
bottomBar.SetText(tabs[tab].page.Links[index])
tabs[tab].saveBottomBar()
tabs[tab].page.Selected = tabs[tab].page.Links[index]
tabs[tab].page.SelectedID = strconv.Itoa(index)
}
})
return &t
}
// addToHistory adds the given URL to history.
// It assumes the URL is currently being loaded and displayed on the page.
func (t *tab) addToHistory(u string) {
if t.history.pos < len(t.history.urls)-1 {
// We're somewhere in the middle of the history instead, with URLs ahead and behind.
// The URLs ahead need to be removed so this new URL is the most recent item in the history
t.history.urls = t.history.urls[:t.history.pos+1]
}
t.history.urls = append(t.history.urls, u)
t.history.pos++
}
// pageUp scrolls up 75% of the height of the terminal, like Bombadillo.
func (t *tab) pageUp() {
row, col := t.view.GetScrollOffset()
t.view.ScrollTo(row-(termH/4)*3, col)
}
// pageDown scrolls down 75% of the height of the terminal, like Bombadillo.
func (t *tab) pageDown() {
row, col := t.view.GetScrollOffset()
t.view.ScrollTo(row+(termH/4)*3, col)
}
// hasContent returns true when the tab has a page that could be displayed.
// The most likely situation where false would be returned is when the default
// new tab content is being displayed.
func (t *tab) hasContent() bool {
if t.page == nil || t.view == nil {
return false
}
if t.page.Url == "" {
return false
}
if strings.HasPrefix(t.page.Url, "about:") {
return false
}
if t.page.Content == "" {
return false
}
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
}
// 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)
}
// saveBottomBar saves the current bottomBar values in the tab.
func (t *tab) saveBottomBar() {
t.barLabel = bottomBar.GetLabel()
t.barText = bottomBar.GetText()
}
// applyBottomBar sets the bottomBar using the stored tab values
func (t *tab) applyBottomBar() {
bottomBar.SetLabel(t.barLabel)
bottomBar.SetText(t.barText)
}
// clearSelected turns off any selection that was going on.
// It does not affect the bottomBar.
func (t *tab) clearSelected() {
t.page.Mode = structs.ModeOff
t.page.Selected = ""
t.page.SelectedID = ""
t.view.Highlight("")
}
// applySelected selects whatever is stored as the selected element in the struct,
// and sets the mode accordingly.
// It is safe to call if nothing was selected previously.
//
// applyBottomBar should be called after, as this func might set some bottomBar values.
func (t *tab) applySelected() {
if t.page.Mode == structs.ModeOff {
// Just in case
t.page.Selected = ""
t.page.SelectedID = ""
t.view.Highlight("")
return
} else if t.page.Mode == structs.ModeLinkSelect {
t.view.Highlight(t.page.SelectedID)
if t.mode == tabModeDone {
// Page is not loading so bottomBar can change
t.barLabel = "[::b]Link: [::-]"
t.barText = t.page.Selected
}
}
}
// applyAll uses applyScroll and applySelected to put a tab's TextView back the way it was.
// It also uses applyBottomBar if this is the current tab.
func (t *tab) applyAll() {
t.applySelected()
t.applyScroll()
if t == tabs[curTab] {
t.applyBottomBar()
}
}

View File

@ -7,21 +7,31 @@ const (
TextPlain Mediatype = "text/plain" TextPlain Mediatype = "text/plain"
) )
type PageMode int
const (
ModeOff PageMode = iota // Regular mode
ModeLinkSelect // When the enter key is pressed, allow for tab-based link navigation
)
// Page is for storing UTF-8 text/gemini pages, as well as text/plain pages. // Page is for storing UTF-8 text/gemini pages, as well as text/plain pages.
type Page struct { type Page struct {
Url string Url string
Mediatype Mediatype Mediatype Mediatype
Raw string // The raw response, as received over the network Raw string // The raw response, as received over the network
Content string // The processed content, NOT raw. Uses cview colour tags. All link/link texts must have region tags. It will also have a left margin. Content string // The processed content, NOT raw. Uses cview colour tags. All link/link texts must have region tags. It will also have a left margin.
Links []string // URLs, for each region in the content. Links []string // URLs, for each region in the content.
Row int // Scroll position Row int // Scroll position
Column int // ditto Column int // ditto
Width int // The width of the terminal at the time when the Content was set. This is to know when reformatting should happen. Width int // The width of the terminal at the time when the Content was set. This is 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
} }
// Size returns an approx. size of a Page in bytes. // Size returns an approx. size of a Page in bytes.
func (p *Page) Size() int { func (p *Page) Size() int {
b := len(p.Raw) + len(p.Content) + len(p.Url) b := len(p.Raw) + len(p.Content) + len(p.Url) + len(p.Selected) + len(p.SelectedID)
for i := range p.Links { for i := range p.Links {
b += len(p.Links[i]) b += len(p.Links[i])
} }

View File

@ -1,17 +0,0 @@
package structs
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSize(t *testing.T) {
p := Page{
Url: "12345",
Raw: "12345",
Content: "12345",
Links: []string{"1", "2", "3", "4", "5"},
}
assert.Equal(t, 20, p.Size(), "sizes should be equal")
}