mirror of
https://github.com/makew0rld/amfora.git
synced 2024-12-04 14:46:29 -05:00
🔀 Refactor to use tab struct
Squashed commit of the following: commit72f36afc9e
Author: makeworld <colecmac@protonmail.com> Date: Tue Jul 7 16:15:45 2020 -0400 🚧 Scroll is applied correctly when navigating around commit4b8982723f
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 commitbe09ffcf91
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. commitef8ab3da39
Author: makeworld <colecmac@protonmail.com> Date: Mon Jul 6 12:10:50 2020 -0400 🚧 Fixed some bugs, major ones remain commitd3d47a344d
Author: makeworld <colecmac@protonmail.com> Date: Sat Jul 4 20:58:46 2020 -0400 🚧 Everything uses tab struct, no compile errors, untested commit44bf54c12f
Author: makeworld <colecmac@protonmail.com> Date: Sat Jul 4 13:24:49 2020 -0400 🚧 Initial work on tab struct
This commit is contained in:
parent
518c35453a
commit
543d15abfc
@ -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)
|
||||||
|
7
NOTES.md
7
NOTES.md
@ -1,10 +1,11 @@
|
|||||||
# Notes
|
# Notes
|
||||||
|
|
||||||
- Simplify into one struct
|
|
||||||
- All the maps and stuff could be replaced with a `Tab` struct
|
|
||||||
- And then just one single map of tab number to `Tab`
|
|
||||||
- URL for each tab should not be stored as a string - in the current code there's lots of reparsing the URL
|
- URL for each tab should not be stored as a string - in the current code there's lots of reparsing the URL
|
||||||
|
|
||||||
|
## Issues
|
||||||
|
- 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
|
||||||
- Filed [issue 23](https://gitlab.com/tslocum/cview/-/issues/23)
|
- Filed [issue 23](https://gitlab.com/tslocum/cview/-/issues/23)
|
||||||
|
2
cache/cache.go
vendored
2
cache/cache.go
vendored
@ -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()
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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,56 +226,53 @@ func Init() {
|
|||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tabs[curTab].mode == tabModeDone {
|
||||||
|
// All the keys and operations that can only work while NOT loading
|
||||||
|
|
||||||
// History arrow keys
|
// History arrow keys
|
||||||
if event.Modifiers() == tcell.ModAlt {
|
if event.Modifiers() == tcell.ModAlt {
|
||||||
if event.Key() == tcell.KeyLeft {
|
if event.Key() == tcell.KeyLeft {
|
||||||
histBack()
|
histBack(tabs[curTab])
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if event.Key() == tcell.KeyRight {
|
if event.Key() == tcell.KeyRight {
|
||||||
histForward()
|
histForward(tabs[curTab])
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch event.Key() {
|
switch event.Key() {
|
||||||
case tcell.KeyCtrlT:
|
case tcell.KeyCtrlT:
|
||||||
if selectedLink == "" {
|
if tabs[curTab].page.Mode == structs.ModeLinkSelect {
|
||||||
NewTab()
|
next, err := resolveRelLink(tabs[curTab], tabs[curTab].page.Url, tabs[curTab].page.Selected)
|
||||||
} else {
|
|
||||||
next, err := resolveRelLink(tabMap[curTab].Url, selectedLink)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error("URL Error", err.Error())
|
Error("URL Error", err.Error())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
NewTab()
|
NewTab()
|
||||||
URL(next)
|
URL(next)
|
||||||
|
} else {
|
||||||
|
NewTab()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
case tcell.KeyCtrlW:
|
|
||||||
CloseTab()
|
|
||||||
return nil
|
|
||||||
case tcell.KeyCtrlR:
|
case tcell.KeyCtrlR:
|
||||||
Reload()
|
Reload()
|
||||||
return nil
|
return nil
|
||||||
case tcell.KeyCtrlH:
|
case tcell.KeyCtrlH:
|
||||||
URL(viper.GetString("a-general.home"))
|
URL(viper.GetString("a-general.home"))
|
||||||
return nil
|
return nil
|
||||||
case tcell.KeyCtrlQ:
|
|
||||||
Stop()
|
|
||||||
return nil
|
|
||||||
case tcell.KeyCtrlB:
|
case tcell.KeyCtrlB:
|
||||||
Bookmarks()
|
Bookmarks(tabs[curTab])
|
||||||
addToHist("about:bookmarks")
|
tabs[curTab].addToHistory("about:bookmarks")
|
||||||
return nil
|
return nil
|
||||||
case tcell.KeyCtrlD:
|
case tcell.KeyCtrlD:
|
||||||
go addBookmark()
|
go addBookmark()
|
||||||
return nil
|
return nil
|
||||||
case tcell.KeyPgUp:
|
case tcell.KeyPgUp:
|
||||||
pageUp()
|
tabs[curTab].pageUp()
|
||||||
return nil
|
return nil
|
||||||
case tcell.KeyPgDn:
|
case tcell.KeyPgDn:
|
||||||
pageDown()
|
tabs[curTab].pageDown()
|
||||||
return nil
|
return nil
|
||||||
case tcell.KeyRune:
|
case tcell.KeyRune:
|
||||||
// Regular key was sent
|
// Regular key was sent
|
||||||
@ -285,29 +281,45 @@ func Init() {
|
|||||||
// Space starts typing, like Bombadillo
|
// Space starts typing, like Bombadillo
|
||||||
bottomBar.SetLabel("[::b]URL/Num./Search: [::-]")
|
bottomBar.SetLabel("[::b]URL/Num./Search: [::-]")
|
||||||
bottomBar.SetText("")
|
bottomBar.SetText("")
|
||||||
|
// Don't save bottom bar, so that whenever you switch tabs, it's not in that mode
|
||||||
App.SetFocus(bottomBar)
|
App.SetFocus(bottomBar)
|
||||||
return nil
|
return nil
|
||||||
case "q":
|
|
||||||
Stop()
|
|
||||||
return nil
|
|
||||||
case "R":
|
case "R":
|
||||||
Reload()
|
Reload()
|
||||||
return nil
|
return nil
|
||||||
case "b":
|
case "b":
|
||||||
histBack()
|
histBack(tabs[curTab])
|
||||||
return nil
|
return nil
|
||||||
case "f":
|
case "f":
|
||||||
histForward()
|
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:
|
||||||
|
CloseTab()
|
||||||
|
return nil
|
||||||
|
case tcell.KeyCtrlQ:
|
||||||
|
Stop()
|
||||||
|
return nil
|
||||||
|
case tcell.KeyRune:
|
||||||
|
// Regular key was sent
|
||||||
|
switch string(event.Rune()) {
|
||||||
|
case "q":
|
||||||
|
Stop()
|
||||||
return nil
|
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)
|
||||||
}
|
}
|
||||||
|
@ -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()
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
@ -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.
|
return false
|
||||||
func pageDown() {
|
|
||||||
row, col := tabViews[curTab].GetScrollOffset()
|
|
||||||
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
230
display/tab.go
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,13 @@ 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
|
||||||
@ -17,11 +24,14 @@ type Page struct {
|
|||||||
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])
|
||||||
}
|
}
|
||||||
|
@ -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")
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user