1
0
mirror of https://github.com/makew0rld/amfora.git synced 2025-02-02 15:07:34 -05:00

🚧 Initial work

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

View File

@ -34,13 +34,14 @@ func aboutInit(version, commit, builtBy string) {
} }
func createAboutPage(url string, content string) structs.Page { func createAboutPage(url string, content string) structs.Page {
renderContent, links := renderer.RenderGemini(content, textWidth(), false) renderContent, links, maxPreCols := renderer.RenderGemini(content, textWidth(), false)
return structs.Page{ return structs.Page{
Raw: content, Raw: content,
Content: renderContent, Content: renderContent,
MaxPreCols: maxPreCols,
Links: links, Links: links,
URL: url, URL: url,
Width: -1, // Force reformatting on first display TermWidth: -1, // Force reformatting on first display
Mediatype: structs.TextGemini, Mediatype: structs.TextGemini,
} }
} }

View File

@ -132,13 +132,14 @@ func Bookmarks(t *tab) {
bkmkPageRaw += fmt.Sprintf("=> %s %s\r\n", keys[i], m[keys[i]]) bkmkPageRaw += fmt.Sprintf("=> %s %s\r\n", keys[i], m[keys[i]])
} }
// Render and display // Render and display
content, links := renderer.RenderGemini(bkmkPageRaw, textWidth(), false) content, links, maxPreCols := renderer.RenderGemini(bkmkPageRaw, textWidth(), false)
page := structs.Page{ page := structs.Page{
Raw: bkmkPageRaw, Raw: bkmkPageRaw,
Content: content, Content: content,
MaxPreCols: maxPreCols,
Links: links, Links: links,
URL: "about:bookmarks", URL: "about:bookmarks",
Width: termW, TermWidth: termW,
Mediatype: structs.TextGemini, Mediatype: structs.TextGemini,
} }
setPage(t, &page) setPage(t, &page)

View File

@ -72,7 +72,11 @@ func Init(version, commit, builtBy string) {
reformatMu.Lock() // Only allow one reformat job at a time reformatMu.Lock() // Only allow one reformat job at a time
for i := range tabs { for i := range tabs {
// Overwrite all tabs with a new, differently sized, left margin // Overwrite all tabs with a new, differently sized, left margin
browser.AddTab(strconv.Itoa(i), makeTabLabel(strconv.Itoa(i+1)), makeContentLayout(tabs[i].view)) browser.AddTab(
strconv.Itoa(i),
makeTabLabel(strconv.Itoa(i+1)),
makeContentLayout(tabs[i].view, leftMargin()),
)
if tabs[i] == t { if tabs[i] == t {
// Reformat page ASAP, in the middle of loop // Reformat page ASAP, in the middle of loop
reformatPageAndSetView(t, t.page) reformatPageAndSetView(t, t.page)
@ -129,8 +133,6 @@ func Init(version, commit, builtBy string) {
bottomBar.SetDoneFunc(func(key tcell.Key) { bottomBar.SetDoneFunc(func(key tcell.Key) {
tab := curTab tab := curTab
tabs[tab].saveScroll()
// Reset func to set the bottomBar back to what it was before // Reset func to set the bottomBar back to what it was before
// Use for errors. // Use for errors.
reset := func() { reset := func() {
@ -247,13 +249,14 @@ func Init(version, commit, builtBy string) {
// Render the default new tab content ONCE and store it for later // Render the default new tab content ONCE and store it for later
// This code is repeated in Reload() // This code is repeated in Reload()
newTabContent := getNewTabContent() newTabContent := getNewTabContent()
renderedNewTabContent, newTabLinks := renderer.RenderGemini(newTabContent, textWidth(), false) renderedNewTabContent, newTabLinks, maxPreCols := renderer.RenderGemini(newTabContent, textWidth(), false)
newTabPage = structs.Page{ newTabPage = structs.Page{
Raw: newTabContent, Raw: newTabContent,
Content: renderedNewTabContent, Content: renderedNewTabContent,
MaxPreCols: maxPreCols,
Links: newTabLinks, Links: newTabLinks,
URL: "about:newtab", URL: "about:newtab",
Width: -1, // Force reformatting on first display TermWidth: -1, // Force reformatting on first display
Mediatype: structs.TextGemini, Mediatype: structs.TextGemini,
} }
@ -432,7 +435,6 @@ func NewTab() {
tabs[curTab].view.Highlight("") tabs[curTab].view.Highlight("")
// Save bottomBar state // Save bottomBar state
tabs[curTab].saveBottomBar() tabs[curTab].saveBottomBar()
tabs[curTab].saveScroll()
} }
curTab = NumTabs() curTab = NumTabs()
@ -443,7 +445,11 @@ func NewTab() {
tabs[curTab].addToHistory("about:newtab") tabs[curTab].addToHistory("about:newtab")
tabs[curTab].history.pos = 0 // Manually set as first page tabs[curTab].history.pos = 0 // Manually set as first page
browser.AddTab(strconv.Itoa(curTab), makeTabLabel(strconv.Itoa(curTab+1)), makeContentLayout(tabs[curTab].view)) browser.AddTab(
strconv.Itoa(curTab),
makeTabLabel(strconv.Itoa(curTab+1)),
makeContentLayout(tabs[curTab].view, leftMargin()),
)
browser.SetCurrentTab(strconv.Itoa(curTab)) browser.SetCurrentTab(strconv.Itoa(curTab))
App.SetFocus(tabs[curTab].view) App.SetFocus(tabs[curTab].view)
@ -506,7 +512,6 @@ func SwitchTab(tab int) {
if curTab > -1 { if curTab > -1 {
// Save bottomBar state // Save bottomBar state
tabs[curTab].saveBottomBar() tabs[curTab].saveBottomBar()
tabs[curTab].saveScroll()
} }
curTab = tab % NumTabs() curTab = tab % NumTabs()
@ -527,13 +532,14 @@ func Reload() {
// Re-render new tab, similar to Init() // Re-render new tab, similar to Init()
newTabContent := getNewTabContent() newTabContent := getNewTabContent()
tmpTermW := termW tmpTermW := termW
renderedNewTabContent, newTabLinks := renderer.RenderGemini(newTabContent, textWidth(), false) renderedNewTabContent, newTabLinks, maxPreCols := renderer.RenderGemini(newTabContent, textWidth(), false)
newTabPage = structs.Page{ newTabPage = structs.Page{
Raw: newTabContent, Raw: newTabContent,
Content: renderedNewTabContent, Content: renderedNewTabContent,
MaxPreCols: maxPreCols,
Links: newTabLinks, Links: newTabLinks,
URL: "about:newtab", URL: "about:newtab",
Width: tmpTermW, TermWidth: tmpTermW,
Mediatype: structs.TextGemini, Mediatype: structs.TextGemini,
} }
temp := newTabPage // Copy temp := newTabPage // Copy

View File

@ -59,14 +59,15 @@ func handleFile(u string) (*structs.Page, bool) {
} }
if mimetype == "text/gemini" { if mimetype == "text/gemini" {
rendered, links := renderer.RenderGemini(string(content), textWidth(), false) rendered, links, maxPreCols := renderer.RenderGemini(string(content), textWidth(), false)
page = &structs.Page{ page = &structs.Page{
Mediatype: structs.TextGemini, Mediatype: structs.TextGemini,
URL: u, URL: u,
Raw: string(content), Raw: string(content),
Content: rendered, Content: rendered,
MaxPreCols: maxPreCols,
Links: links, Links: links,
Width: termW, TermWidth: termW,
} }
} else { } else {
page = &structs.Page{ page = &structs.Page{
@ -74,8 +75,9 @@ func handleFile(u string) (*structs.Page, bool) {
URL: u, URL: u,
Raw: string(content), Raw: string(content),
Content: renderer.RenderPlainText(string(content)), Content: renderer.RenderPlainText(string(content)),
MaxPreCols: -1,
Links: []string{}, Links: []string{},
Width: termW, TermWidth: termW,
} }
} }
} }
@ -107,14 +109,15 @@ func createDirectoryListing(u string) (*structs.Page, bool) {
content += fmt.Sprintf("=> %s%s %s%s\n", f.Name(), separator, f.Name(), separator) content += fmt.Sprintf("=> %s%s %s%s\n", f.Name(), separator, f.Name(), separator)
} }
rendered, links := renderer.RenderGemini(content, textWidth(), false) rendered, links, maxPreCols := renderer.RenderGemini(content, textWidth(), false)
page = &structs.Page{ page = &structs.Page{
Mediatype: structs.TextGemini, Mediatype: structs.TextGemini,
URL: u, URL: u,
Raw: content, Raw: content,
Content: rendered, Content: rendered,
MaxPreCols: maxPreCols,
Links: links, Links: links,
Width: termW, TermWidth: termW,
} }
return page, true return page, true
} }

View File

@ -337,7 +337,7 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
return ret("", false) return ret("", false)
} }
page.Width = termW page.TermWidth = termW
if !client.HasClientCert(parsed.Host) { if !client.HasClientCert(parsed.Host) {
// Don't cache pages with client certs // Don't cache pages with client certs

View File

@ -24,7 +24,6 @@ func followLink(t *tab, prev, next string) {
} }
if t.hasContent() { if t.hasContent() {
t.saveScroll() // Likely called later on, it's here just in case
nextURL, err := resolveRelLink(t, prev, next) nextURL, err := resolveRelLink(t, prev, next)
if err != nil { if err != nil {
Error("URL Error", err.Error()) Error("URL Error", err.Error())
@ -48,7 +47,7 @@ func followLink(t *tab, prev, next string) {
// It will not waste resources if the passed page is already fitted to the current terminal width, and can be // It will not waste resources if the passed page is already fitted to the current terminal width, and can be
// called safely even when the page might be already formatted properly. // called safely even when the page might be already formatted properly.
func reformatPage(p *structs.Page) { func reformatPage(p *structs.Page) {
if p.Width == termW { if p.TermWidth == termW {
// No changes to make // No changes to make
return return
} }
@ -65,7 +64,7 @@ func reformatPage(p *structs.Page) {
strings.HasPrefix(p.URL, "file") { strings.HasPrefix(p.URL, "file") {
proxied = false proxied = false
} }
rendered, _ = renderer.RenderGemini(p.Raw, textWidth(), proxied) rendered, _, _ = renderer.RenderGemini(p.Raw, textWidth(), proxied)
case structs.TextPlain: case structs.TextPlain:
rendered = renderer.RenderPlainText(p.Raw) rendered = renderer.RenderPlainText(p.Raw)
case structs.TextAnsi: case structs.TextAnsi:
@ -75,17 +74,16 @@ func reformatPage(p *structs.Page) {
return return
} }
p.Content = rendered p.Content = rendered
p.Width = termW p.TermWidth = termW
} }
// 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(t *tab, p *structs.Page) { func reformatPageAndSetView(t *tab, p *structs.Page) {
if p.Width == termW { if p.TermWidth == termW {
// No changes to make // No changes to make
return return
} }
t.saveScroll()
reformatPage(p) reformatPage(p)
t.view.SetText(p.Content) t.view.SetText(p.Content)
t.applyScroll() // Go back to where you were, roughly t.applyScroll() // Go back to where you were, roughly
@ -101,8 +99,6 @@ func setPage(t *tab, p *structs.Page) {
return 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)
@ -112,10 +108,13 @@ func setPage(t *tab, p *structs.Page) {
t.view.SetText(p.Content) t.view.SetText(p.Content)
t.view.Highlight("") // Turn off highlights, other funcs may restore if necessary t.view.Highlight("") // Turn off highlights, other funcs may restore if necessary
t.view.ScrollToBeginning() t.view.ScrollToBeginning()
// Reset page left margin
// Set tab number in case a favicon from before overwrote it
tabNum := tabNumber(t) tabNum := tabNumber(t)
browser.SetTabLabel(strconv.Itoa(tabNum), makeTabLabel(strconv.Itoa(tabNum+1))) browser.AddTab(
strconv.Itoa(tabNum),
makeTabLabel(strconv.Itoa(tabNum+1)),
makeContentLayout(t.view, leftMargin()),
)
App.Draw() App.Draw()
// Setup display // Setup display

View File

@ -149,13 +149,14 @@ func Subscriptions(t *tab, u string) string {
} }
} }
content, links := renderer.RenderGemini(rawPage, textWidth(), false) content, links, maxPreCols := renderer.RenderGemini(rawPage, textWidth(), false)
page := structs.Page{ page := structs.Page{
Raw: rawPage, Raw: rawPage,
Content: content, Content: content,
MaxPreCols: maxPreCols,
Links: links, Links: links,
URL: u, URL: u,
Width: termW, TermWidth: termW,
Mediatype: structs.TextGemini, Mediatype: structs.TextGemini,
} }
go cache.AddPage(&page) go cache.AddPage(&page)
@ -191,13 +192,14 @@ func ManageSubscriptions(t *tab, u string) {
) )
} }
content, links := renderer.RenderGemini(rawPage, textWidth(), false) content, links, maxPreCols := renderer.RenderGemini(rawPage, textWidth(), false)
page := structs.Page{ page := structs.Page{
Raw: rawPage, Raw: rawPage,
Content: content, Content: content,
MaxPreCols: maxPreCols,
Links: links, Links: links,
URL: "about:manage-subscriptions", URL: "about:manage-subscriptions",
Width: termW, TermWidth: termW,
Mediatype: structs.TextGemini, Mediatype: structs.TextGemini,
} }
go cache.AddPage(&page) go cache.AddPage(&page)

View File

@ -121,6 +121,58 @@ func makeNewTab() *tab {
tabs[tab].page.SelectedID = strconv.Itoa(index) tabs[tab].page.SelectedID = strconv.Itoa(index)
} }
}) })
t.view.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
// Capture left/right scrolling and change the left margin size accordingly
// See #197
// Up/down scrolling is saved in this func to keep them in sync, but the keys
// are passed and no extra behaviour happens.
key := event.Key()
mod := event.Modifiers()
ru := event.Rune()
oldCol := t.page.Column
if (key == tcell.KeyRight && mod == tcell.ModNone) ||
(key == tcell.KeyRune && mod == tcell.ModNone && ru == 'l') {
// Scrolling to the right
// TODO check if already scrolled to the end
t.page.Column++
} else if (key == tcell.KeyLeft && mod == tcell.ModNone) ||
(key == tcell.KeyRune && mod == tcell.ModNone && ru == 'h') {
// Scrolling to the left
if t.page.Column == 0 {
// Can't scroll to the left anymore
return nil
}
t.page.Column--
} else if (key == tcell.KeyUp && mod == tcell.ModNone) ||
(key == tcell.KeyRune && mod == tcell.ModNone && ru == 'k') {
// Scrolling up
if t.page.Row > 0 {
t.page.Row--
}
return event
} else if (key == tcell.KeyDown && mod == tcell.ModNone) ||
(key == tcell.KeyRune && mod == tcell.ModNone && ru == 'j') {
// Scrolling down
// TODO need to check for max vertical scroll before doing this
return event
} else {
// Some other key, stop processing it
return event
}
if t.page.MaxPreCols <= termW && t.page.MaxPreCols > -1 {
// No scrolling is actually necessary
t.page.Column = oldCol // Reset
return nil // Ignore keys
}
t.applyHorizontalScroll()
App.Draw()
return nil
})
return &t return &t
} }
@ -167,19 +219,39 @@ func (t *tab) hasContent() bool {
return true return true
} }
// saveScroll saves where in the page the user was. // applyHorizontalScroll handles horizontal scroll logic including left margin resizing,
// It should be used whenever moving from one page to another. // see #197 for details. Use applyScroll instead.
func (t *tab) saveScroll() { //
// It will also be saved in the cache because the cache uses the same pointer // In certain cases it will still use and apply the saved Row.
row, col := t.view.GetScrollOffset() func (t *tab) applyHorizontalScroll() {
t.page.Row = row i := tabNumber(t)
t.page.Column = col if i == -1 {
// Tab is not actually being used and should not be (re)added to the browser
return
}
if t.page.Column >= leftMargin() {
// Scrolled to the right far enough that no left margin is needed
browser.AddTab(
strconv.Itoa(i),
makeTabLabel(strconv.Itoa(i+1)),
makeContentLayout(t.view, 0),
)
t.view.ScrollTo(t.page.Row, t.page.Column-leftMargin())
} else {
// Left margin is still needed, but is not necessarily at the right size by default
browser.AddTab(
strconv.Itoa(i),
makeTabLabel(strconv.Itoa(i+1)),
makeContentLayout(t.view, leftMargin()-t.page.Column),
)
}
} }
// applyScroll applies the saved scroll values to the page and tab. // applyScroll applies the saved scroll values to the page and tab.
// It should only be used when going backward and forward. // It should only be used when going backward and forward.
func (t *tab) applyScroll() { func (t *tab) applyScroll() {
t.view.ScrollTo(t.page.Row, t.page.Column) t.view.ScrollTo(t.page.Row, 0)
t.applyHorizontalScroll()
} }
// saveBottomBar saves the current bottomBar values in the tab. // saveBottomBar saves the current bottomBar values in the tab.

View File

@ -14,13 +14,15 @@ import (
// This file contains funcs that are small, self-contained utilities. // This file contains funcs that are small, self-contained utilities.
// makeContentLayout returns a flex that contains the given TextView // makeContentLayout returns a flex that contains the given TextView
// along with the current correct left margin, as well as a single empty // along with the provided left margin, as well as a single empty
// line at the top, for a top margin. // line at the top, for a top margin.
func makeContentLayout(tv *cview.TextView) *cview.Flex { func makeContentLayout(tv *cview.TextView, leftMargin int) *cview.Flex {
// Create horizontal flex with the left margin as an empty space // Create horizontal flex with the left margin as an empty space
horiz := cview.NewFlex() horiz := cview.NewFlex()
horiz.SetDirection(cview.FlexColumn) horiz.SetDirection(cview.FlexColumn)
horiz.AddItem(nil, leftMargin(), 0, false) if leftMargin > 0 {
horiz.AddItem(nil, leftMargin, 0, false)
}
horiz.AddItem(tv, 0, 1, true) horiz.AddItem(tv, 0, 1, true)
// Create a vertical flex with the other one and a top margin // Create a vertical flex with the other one and a top margin

1
go.mod
View File

@ -8,6 +8,7 @@ require (
github.com/gdamore/tcell/v2 v2.1.1-0.20210125004847-19e17097d8fe github.com/gdamore/tcell/v2 v2.1.1-0.20210125004847-19e17097d8fe
github.com/google/go-cmp v0.5.0 // indirect github.com/google/go-cmp v0.5.0 // indirect
github.com/makeworld-the-better-one/go-gemini v0.11.0 github.com/makeworld-the-better-one/go-gemini v0.11.0
github.com/mattn/go-runewidth v0.0.10
github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/mapstructure v1.3.1 // indirect github.com/mitchellh/mapstructure v1.3.1 // indirect
github.com/mmcdole/gofeed v1.1.0 github.com/mmcdole/gofeed v1.1.0

View File

@ -118,13 +118,14 @@ func MakePage(url string, res *gemini.Response, width int, proxied bool) (*struc
} }
if mediatype == "text/gemini" { if mediatype == "text/gemini" {
rendered, links := RenderGemini(utfText, width, proxied) rendered, links, maxPreCols := RenderGemini(utfText, width, proxied)
return &structs.Page{ return &structs.Page{
Mediatype: structs.TextGemini, Mediatype: structs.TextGemini,
RawMediatype: mediatype, RawMediatype: mediatype,
URL: url, URL: url,
Raw: utfText, Raw: utfText,
Content: rendered, Content: rendered,
MaxPreCols: maxPreCols,
Links: links, Links: links,
MadeAt: time.Now(), MadeAt: time.Now(),
}, nil }, nil
@ -137,6 +138,7 @@ func MakePage(url string, res *gemini.Response, width int, proxied bool) (*struc
URL: url, URL: url,
Raw: utfText, Raw: utfText,
Content: RenderANSI(utfText), Content: RenderANSI(utfText),
MaxPreCols: -1,
Links: []string{}, Links: []string{},
MadeAt: time.Now(), MadeAt: time.Now(),
}, nil }, nil
@ -149,6 +151,7 @@ func MakePage(url string, res *gemini.Response, width int, proxied bool) (*struc
URL: url, URL: url,
Raw: utfText, Raw: utfText,
Content: RenderPlainText(utfText), Content: RenderPlainText(utfText),
MaxPreCols: -1,
Links: []string{}, Links: []string{},
MadeAt: time.Now(), MadeAt: time.Now(),
}, nil }, nil

View File

@ -12,6 +12,7 @@ import (
"strings" "strings"
"github.com/makeworld-the-better-one/amfora/config" "github.com/makeworld-the-better-one/amfora/config"
"github.com/mattn/go-runewidth"
"github.com/spf13/viper" "github.com/spf13/viper"
"gitlab.com/tslocum/cview" "gitlab.com/tslocum/cview"
) )
@ -267,19 +268,19 @@ func convertRegularGemini(s string, numLinks, width int, proxied bool) (string,
} }
// RenderGemini converts text/gemini into a cview displayable format. // RenderGemini converts text/gemini into a cview displayable format.
// It also returns a slice of link URLs. // It also returns a slice of link URLs, and the Page.MaxPreCols value.
// //
// width is the number of columns to wrap to. // width is the number of columns to wrap to.
// leftMargin is the number of blank spaces to prepend to each line. // leftMargin is the number of blank spaces to prepend to each line.
// //
// proxied is whether the request is through the gemini:// scheme. // proxied is whether the request is through the gemini:// scheme.
// If it's not a gemini:// page, set this to true. // If it's not a gemini:// page, set this to true.
func RenderGemini(s string, width int, proxied bool) (string, []string) { func RenderGemini(s string, width int, proxied bool) (string, []string, int) {
s = cview.Escape(s) s = cview.Escape(s)
lines := strings.Split(s, "\n") lines := strings.Split(s, "\n")
links := make([]string, 0) links := make([]string, 0)
maxPreCols := 0
// Process and wrap non preformatted lines // Process and wrap non preformatted lines
rendered := "" // Final result rendered := "" // Final result
@ -288,6 +289,11 @@ func RenderGemini(s string, width int, proxied bool) (string, []string) {
// processPre is for rendering preformatted blocks // processPre is for rendering preformatted blocks
processPre := func() { processPre := func() {
lineWidth := runewidth.StringWidth(buf)
if lineWidth > maxPreCols {
maxPreCols = lineWidth
}
// Support ANSI color codes in preformatted blocks - see #59 // Support ANSI color codes in preformatted blocks - see #59
if viper.GetBool("a-general.color") && viper.GetBool("a-general.ansi") { if viper.GetBool("a-general.color") && viper.GetBool("a-general.ansi") {
buf = cview.TranslateANSI(buf) buf = cview.TranslateANSI(buf)
@ -348,5 +354,5 @@ func RenderGemini(s string, width int, proxied bool) (string, []string) {
processRegular() processRegular()
} }
return rendered, links return rendered, links, maxPreCols
} }

View File

@ -25,10 +25,11 @@ type Page struct {
RawMediatype string // The actual mediatype sent by the server RawMediatype string // The actual mediatype sent by the server
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 color tags. It will also have a left margin. Content string // The processed content, NOT raw. Uses cview color tags. It will also have a left margin.
MaxPreCols int // The amount of the terminal columns the longest preformatted line in Raw takes up, used for #197. -1 means infinite length lines, AKA always allow scrolling.
Links []string // URLs, for each region in the content. Links []string // URLs, for each region in the content.
Row int // Scroll position Row int // Vertical scroll position
Column int // ditto Column int // Horizontal scroll position - does not map exactly to a cview.TextView because it includes left margin size changes, see #197
Width int // The terminal width when the Content was set, to know when reformatting should happen. TermWidth int // The terminal width when the Content was set, to know when reformatting should happen.
Selected string // The current text or link selected Selected string // The current text or link selected
SelectedID string // The cview region ID for the selected text/link SelectedID string // The cview region ID for the selected text/link
Mode PageMode Mode PageMode