1
0
mirror of https://github.com/makew0rld/amfora.git synced 2024-06-25 19:55:22 +00:00

🚧 Switch to using tab pointers instead of ints

Almost finished overall work.
This commit is contained in:
makeworld 2020-07-06 20:30:54 -04:00
parent ef8ab3da39
commit be09ffcf91
7 changed files with 150 additions and 122 deletions

View File

@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- Many potential network and display race conditions eliminated - Many potential network and display race conditions eliminated
- Whether a tab is loading stays indicated when you switch away from it and go back - 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

View File

@ -1,12 +1,9 @@
# Notes # Notes
- 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
- Switch to UUIDs for each tab maybe? So that if `handleURL` completes after the tab is closed (and reopened) it doesn't go anywhere
- New UUID every time a new page is loaded?
## Bugs ## Bugs
- Can't go back or do other things while page is loading - need a way to stop `handleURL` - Can't go back or do other things while page is loading - need a way to stop `handleURL`
- `handleURL` will reference a tab that doesn't exist and cause a panic if the tab is closed
## Upstream Bugs ## Upstream Bugs
- Wrapping messes up on brackets - Wrapping messes up on brackets

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(tab int) { 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,8 +113,8 @@ func Bookmarks(tab int) {
Width: termW, Width: termW,
Mediatype: structs.TextGemini, Mediatype: structs.TextGemini,
} }
setPage(curTab, &page) setPage(t, &page)
tabs[tab].applyBottomBar() 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.

View File

@ -65,7 +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 App = cview.NewApplication(). var App = cview.NewApplication().
EnableMouse(false). EnableMouse(false).
@ -76,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) {
tabs[tab].reformatMut.Lock() // Only one reformat job per tab t.reformatMut.Lock() // Only one reformat job per tab
defer tabs[tab].reformatMut.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, tabs[tab].page) reformatPageAndSetView(t, t.page)
}(curTab) }(tabs[curTab])
}) })
func Init() { func Init() {
@ -180,7 +180,7 @@ func Init() {
} }
if i <= len(tabs[tab].page.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(tab, tabs[tab].page.Url, tabs[tab].page.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
@ -199,7 +199,7 @@ func Init() {
// 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,
@ -230,11 +230,11 @@ func Init() {
// 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
} }
} }
@ -242,7 +242,7 @@ func Init() {
switch event.Key() { switch event.Key() {
case tcell.KeyCtrlT: case tcell.KeyCtrlT:
if tabs[curTab].page.Mode == structs.ModeLinkSelect { if tabs[curTab].page.Mode == structs.ModeLinkSelect {
next, err := resolveRelLink(curTab, tabs[curTab].page.Url, tabs[curTab].page.Selected) next, err := resolveRelLink(tabs[curTab], tabs[curTab].page.Url, tabs[curTab].page.Selected)
if err != nil { if err != nil {
Error("URL Error", err.Error()) Error("URL Error", err.Error())
return nil return nil
@ -260,7 +260,7 @@ func Init() {
URL(viper.GetString("a-general.home")) URL(viper.GetString("a-general.home"))
return nil return nil
case tcell.KeyCtrlB: case tcell.KeyCtrlB:
Bookmarks(curTab) Bookmarks(tabs[curTab])
tabs[curTab].addToHistory("about:bookmarks") tabs[curTab].addToHistory("about:bookmarks")
return nil return nil
case tcell.KeyCtrlD: case tcell.KeyCtrlD:
@ -286,10 +286,10 @@ func Init() {
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 return nil
case "u": case "u":
tabs[curTab].pageUp() tabs[curTab].pageUp()
@ -383,7 +383,8 @@ func NewTab() {
curTab = NumTabs() curTab = NumTabs()
tabs = append(tabs, makeNewTab()) tabs = append(tabs, makeNewTab())
setPage(curTab, newTabPage) temp := newTabPage // Copy
setPage(tabs[curTab], &temp)
// 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.
@ -479,7 +480,7 @@ func SwitchTab(tab int) {
curTab = tab % NumTabs() curTab = tab % NumTabs()
// Display tab // Display tab
reformatPageAndSetView(curTab, tabs[curTab].page) 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].applySelected() tabs[curTab].applySelected()
@ -497,13 +498,13 @@ func Reload() {
} }
cache.Remove(tabs[curTab].page.Url) cache.Remove(tabs[curTab].page.Url)
go func(tab int) { go func(t *tab) {
handleURL(tab, tabs[tab].page.Url) // goURL is not used bc history shouldn't be added to handleURL(t, t.page.Url) // goURL is not used bc history shouldn't be added to
if tab == curTab { if t == tabs[curTab] {
// Display the bottomBar state that handleURL set // Display the bottomBar state that handleURL set
tabs[tab].applyBottomBar() t.applyBottomBar()
} }
}(curTab) }(tabs[curTab])
} }
// URL loads and handles the provided URL for the current tab. // URL loads and handles the provided URL for the current tab.
@ -512,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(curTab) Bookmarks(tabs[curTab])
tabs[curTab].addToHistory("about:bookmarks") tabs[curTab].addToHistory("about:bookmarks")
return return
} }
if u == "about:newtab" { if u == "about:newtab" {
setPage(curTab, newTabPage) temp := newTabPage // Copy
setPage(tabs[curTab], &temp)
return return
} }
if strings.HasPrefix(u, "about:") { if strings.HasPrefix(u, "about:") {
@ -525,7 +527,7 @@ func URL(u string) {
return return
} }
go goURL(curTab, u) go goURL(tabs[curTab], u)
} }
func NumTabs() int { func NumTabs() int {

View File

@ -1,35 +1,35 @@
package display package display
func histForward() { func histForward(t *tab) {
if tabs[curTab].history.pos >= len(tabs[curTab].history.urls)-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
} }
tabs[curTab].history.pos++ t.history.pos++
go func(tab int) { go func(tt *tab) {
handleURL(tab, tabs[tab].history.urls[tabs[tab].history.pos]) // Load that position in history handleURL(tt, tt.history.urls[tt.history.pos]) // Load that position in history
tabs[tab].applyScroll() tt.applyScroll()
tabs[tab].applySelected() tt.applySelected()
if tab == curTab { if tt == tabs[curTab] {
// Display the bottomBar state that handleURL set // Display the bottomBar state that handleURL set
tabs[tab].applyBottomBar() tt.applyBottomBar()
} }
}(curTab) }(t)
} }
func histBack() { func histBack(t *tab) {
if tabs[curTab].history.pos <= 0 { if t.history.pos <= 0 {
// First tab in history // First tab in history
return return
} }
tabs[curTab].history.pos-- t.history.pos--
go func(tab int) { go func(tt *tab) {
handleURL(tab, tabs[tab].history.urls[tabs[tab].history.pos]) // Load that position in history handleURL(tt, tt.history.urls[tt.history.pos]) // Load that position in history
tabs[tab].applyScroll() tt.applyScroll()
tabs[tab].applySelected() tt.applySelected()
if tab == curTab { if tt == tabs[curTab] {
// Display the bottomBar state that handleURL set // Display the bottomBar state that handleURL set
tabs[tab].applyBottomBar() tt.applyBottomBar()
} }
}(curTab) }(t)
} }

View File

@ -18,6 +18,16 @@ 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.
// isValidTab indicates whether the passed tab is still being used, even if it's not currently displayed.
func isValidTab(t *tab) bool {
for i := range tabs {
if tabs[i] == t {
return true
}
}
return false
}
func leftMargin() int { func leftMargin() int {
return int(float64(termW) * viper.GetFloat64("a-general.left_margin")) return int(float64(termW) * viper.GetFloat64("a-general.left_margin"))
} }
@ -51,8 +61,8 @@ func queryEscape(path string) string {
// 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(tab int, prev, next string) (string, error) { func resolveRelLink(t *tab, prev, next string) (string, error) {
if !tabs[tab].hasContent() { if !t.hasContent() {
return next, nil return next, nil
} }
@ -67,12 +77,12 @@ func resolveRelLink(tab int, 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.
// It will handle setting the bottomBar. // It will handle setting the bottomBar.
func followLink(tab int, prev, next string) { func followLink(t *tab, prev, next string) {
// Copied from URL() // Copied from URL()
if next == "about:bookmarks" { if next == "about:bookmarks" {
Bookmarks(tab) Bookmarks(t)
tabs[tab].addToHistory("about:bookmarks") t.addToHistory("about:bookmarks")
return return
} }
if strings.HasPrefix(next, "about:") { if strings.HasPrefix(next, "about:") {
@ -80,14 +90,14 @@ func followLink(tab int, prev, next string) {
return return
} }
if tabs[tab].hasContent() { if t.hasContent() {
tabs[tab].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(tab, 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 goURL(tab, nextURL) go goURL(t, nextURL)
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.
@ -97,7 +107,7 @@ func followLink(tab int, prev, next string) {
Error("URL Error", "Link URL could not be parsed") Error("URL Error", "Link URL could not be parsed")
return return
} }
go goURL(tab, next) go goURL(t, next)
} }
// 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.
@ -126,33 +136,38 @@ 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) {
tabs[tab].saveScroll() t.saveScroll()
reformatPage(p) reformatPage(p)
tabs[tab].view.SetText(p.Content) t.view.SetText(p.Content)
tabs[tab].applyScroll() // Go back to where you were, roughly t.applyScroll() // Go back to where you were, roughly
} }
// setPage displays a Page on the passed tab number. // setPage displays a Page on the passed tab number.
// The bottomBar is not actually changed in this func // The bottomBar is not actually changed in this func
func setPage(tab int, p *structs.Page) { func setPage(t *tab, p *structs.Page) {
tabs[tab].saveScroll() // Save the scroll of the previous 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
tabs[tab].page = p t.page = p
tabs[tab].view.SetText(p.Content) t.view.SetText(p.Content)
tabs[tab].view.Highlight("") // Turn off highlights t.view.Highlight("") // Turn off highlights, other funcs may restore if necessary
tabs[tab].view.ScrollToBeginning() t.view.ScrollToBeginning()
// Setup display // Setup display
App.SetFocus(tabs[tab].view) App.SetFocus(t.view)
// Save bottom bar for the tab - TODO: other funcs will apply/display it // Save bottom bar for the tab - TODO: other funcs will apply/display it
tabs[tab].barLabel = "" t.barLabel = ""
tabs[tab].barText = p.Url t.barText = p.Url
} }
// goURL is like handleURL, but takes care of history and the bottomBar. // goURL is like handleURL, but takes care of history and the bottomBar.
@ -160,14 +175,14 @@ func setPage(tab int, p *structs.Page) {
// It has no return values to be processed. // It has no return values to be processed.
// //
// It should be called in a goroutine. // It should be called in a goroutine.
func goURL(tab int, u string) { func goURL(t *tab, u string) {
final, displayed := handleURL(tab, u) final, displayed := handleURL(t, u)
if displayed { if displayed {
tabs[tab].addToHistory(final) t.addToHistory(final)
} }
if tab == curTab { if t == tabs[curTab] {
// Display the bottomBar state that handleURL set // Display the bottomBar state that handleURL set
tabs[tab].applyBottomBar() t.applyBottomBar()
} }
} }
@ -183,15 +198,32 @@ func goURL(tab int, u string) {
// //
// The bottomBar is not actually changed in this func, except during loading. // 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. // The func that calls this one should apply the bottomBar values if necessary.
func handleURL(tab int, u string) (string, bool) { func handleURL(t *tab, u string) (string, bool) {
defer App.Draw() // Just in case defer App.Draw() // Just in case
App.SetFocus(tabs[tab].view) // 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
}
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(tab) Bookmarks(t)
return "about:bookmarks", true return ret("about:bookmarks", true)
} }
u = normalizeURL(u) u = normalizeURL(u)
@ -199,8 +231,7 @@ func handleURL(tab int, 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())
tabs[tab].barText = tabs[tab].page.Url return ret("", false)
return "", false
} }
if strings.HasPrefix(u, "http") { if strings.HasPrefix(u, "http") {
@ -222,33 +253,36 @@ func handleURL(tab int, u string) (string, bool) {
Error("HTTP Error", "Error executing custom browser command: "+err.Error()) Error("HTTP Error", "Error executing custom browser command: "+err.Error())
} }
} }
tabs[tab].barText = tabs[tab].page.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)
tabs[tab].barText = tabs[tab].page.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(tab, page) setPage(t, page)
return u, true return ret(u, true)
} }
// Otherwise download it // Otherwise download it
bottomBar.SetText("Loading...") bottomBar.SetText("Loading...")
tabs[tab].barText = "Loading..." // Save it too, in case the tab switches during loading t.barText = "Loading..." // Save it too, in case the tab switches during loading
tabs[tab].mode = tabModeLoading t.mode = tabModeLoading
defer func(t int) { defer func(tt *tab) {
tabs[t].mode = tabModeDone tt.mode = tabModeDone
}(tab) }(t)
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
@ -256,37 +290,31 @@ func handleURL(tab int, 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)
tabs[tab].barText = tabs[tab].page.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)
tabs[tab].barText = tabs[tab].page.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)
tabs[tab].barText = tabs[tab].page.Url
return "", false
} }
go cache.Add(page) go cache.Add(page)
setPage(tab, 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(tabs[curTab].page.Url)
tabs[tab].barText = tabs[curTab].page.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:
@ -298,31 +326,31 @@ func handleURL(tab int, u string) (string, bool) {
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(tab, 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(tab, redir) return handleURL(t, redir)
} }
return "", false return ret("", false)
case 40: case 40:
Error("Temporary Failure", cview.Escape(res.Meta)) 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?")
@ -345,7 +373,7 @@ func handleURL(tab int, 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

View File

@ -79,7 +79,7 @@ func makeNewTab() *tab {
linkN, _ := strconv.Atoi(currentSelection[0]) linkN, _ := strconv.Atoi(currentSelection[0])
tabs[tab].page.Selected = tabs[tab].page.Links[linkN] tabs[tab].page.Selected = tabs[tab].page.Links[linkN]
tabs[tab].page.SelectedID = currentSelection[0] tabs[tab].page.SelectedID = currentSelection[0]
followLink(tab, tabs[tab].page.Url, tabs[tab].page.Links[linkN]) followLink(tabs[tab], tabs[tab].page.Url, tabs[tab].page.Links[linkN])
return return
} else { } else {
// They've started link highlighting // They've started link highlighting