1
0
mirror of https://github.com/makew0rld/amfora.git synced 2024-11-03 02:37:23 -05:00
amfora/display/display.go
makeworld 850d6f0454 Side-scrolling and margin UX greatly improved
#1 will be fixed completely once margins are added to the new tab page
2020-06-21 16:53:12 -04:00

437 lines
11 KiB
Go

package display
import (
"fmt"
"strconv"
"strings"
"github.com/spf13/viper"
"github.com/makeworld-the-better-one/amfora/renderer"
"github.com/gdamore/tcell"
"github.com/makeworld-the-better-one/amfora/cache"
"github.com/makeworld-the-better-one/amfora/structs"
"gitlab.com/tslocum/cview"
)
var curTab = -1 // What number tab is currently visible, -1 means there are no tabs at all
var tabMap = make(map[int]*structs.Page) // Map of tab number to page
// Holds the actual tab primitives
var tabViews = make(map[int]*cview.TextView)
var termW int
var termH int
// The user input and URL display bar at the bottom
var bottomBar = cview.NewInputField().
SetFieldBackgroundColor(tcell.ColorWhite).
SetFieldTextColor(tcell.ColorBlack).
SetLabelColor(tcell.ColorGreen)
var helpTable = cview.NewTable().
SetSelectable(false, false).
SetFixed(1, 2).
SetBorders(true).
SetBordersColor(tcell.ColorGray)
// Viewer for the tab primitives
// Pages are named as strings of tab numbers - so the textview for the first tab
// is held in the page named "0".
// The only pages that don't confine to this scheme named after the modals above,
// which is used to draw modals on top the current tab.
// Ex: "info", "error", "input", "yesno"
var tabPages = cview.NewPages().
AddPage("help", helpTable, true, false).
AddPage("info", infoModal, false, false).
AddPage("error", errorModal, false, false).
AddPage("input", inputModal, false, false).
AddPage("yesno", yesNoModal, false, false)
// The tabs at the top with titles
var tabRow = cview.NewTextView().
SetDynamicColors(true).
SetRegions(true).
SetScrollable(true).
SetWrap(false).
SetHighlightedFunc(func(added, removed, remaining []string) {
// There will always only be one string in added - never multiple highlights
// Remaining should always be empty
i, _ := strconv.Atoi(added[0])
tabPages.SwitchToPage(strconv.Itoa(i)) // Tab names are just numbers, zero-indexed
})
// Root layout
var layout = cview.NewFlex().
SetDirection(cview.FlexRow).
AddItem(tabRow, 1, 1, false).
AddItem(nil, 1, 1, false). // One line of empty space above the page
AddItem(tabPages, 0, 1, true).
// AddItem(cview.NewFlex(). // The page text in the middle is held in another flex, to center it
// SetDirection(cview.FlexColumn).
// AddItem(nil, 0, 1, false).
// AddItem(tabPages, 0, 7, true). // The text occupies 7/9 of the screen horizontally
// AddItem(nil, 0, 1, false),
// 0, 1, true).
AddItem(nil, 1, 1, false). // One line of empty space before bottomBar
AddItem(bottomBar, 1, 1, false)
var App = cview.NewApplication().
EnableMouse(false).
SetRoot(layout, true).
SetAfterResizeFunc(func(width int, height int) {
// Store width and height for calculations
termW = width
termH = height
})
var renderedNewTabContent string
var newTabLinks []string
var newTabPage structs.Page
func Init() {
tabRow.SetChangedFunc(func() {
App.Draw()
})
// Populate help table
helpTable.SetDoneFunc(func(key tcell.Key) {
if key == tcell.KeyEsc {
tabPages.SwitchToPage(strconv.Itoa(curTab))
}
})
rows := strings.Count(helpCells, "\n") + 1
cells := strings.Split(
strings.ReplaceAll(helpCells, "\n", "|"),
"|")
cell := 0
for r := 0; r < rows; r++ {
for c := 0; c < 2; c++ {
var tableCell *cview.TableCell
if c == 0 {
tableCell = cview.NewTableCell(cells[cell]).
SetAttributes(tcell.AttrBold).
SetExpansion(1)
} else {
tableCell = cview.NewTableCell(cells[cell]).
SetExpansion(2)
}
helpTable.SetCell(r, c, tableCell)
cell++
}
}
bottomBar.SetBackgroundColor(tcell.ColorWhite)
bottomBar.SetDoneFunc(func(key tcell.Key) {
switch key {
case tcell.KeyEnter:
// Figure out whether it's a URL, link number, or search
// And send out a request
query := bottomBar.GetText()
if strings.TrimSpace(query) == "" {
// Ignore
bottomBar.SetLabel("")
bottomBar.SetText(tabMap[curTab].Url)
App.SetFocus(tabViews[curTab])
return
}
i, err := strconv.Atoi(query)
if err != nil {
// It's a full URL or search term
// Detect if it's a search or URL
if strings.Contains(query, " ") || (!strings.Contains(query, "//") && !strings.Contains(query, ".")) {
URL(viper.GetString("a-general.search") + "?" + pathEscape(query))
} else {
// Full URL
URL(query)
}
bottomBar.SetLabel("")
return
}
if i <= len(tabMap[curTab].Links) && i > 0 {
// It's a valid link number
followLink(tabMap[curTab].Url, tabMap[curTab].Links[i-1])
bottomBar.SetLabel("")
return
}
// Invalid link number
bottomBar.SetLabel("")
bottomBar.SetText(tabMap[curTab].Url)
App.SetFocus(tabViews[curTab])
case tcell.KeyEscape:
// Set back to what it was
bottomBar.SetLabel("")
bottomBar.SetText(tabMap[curTab].Url)
App.SetFocus(tabViews[curTab])
}
// Other potential keys are Tab and Backtab, they are ignored
})
// Render the default new tab content ONCE and store it for later
renderedNewTabContent, newTabLinks = renderer.RenderGemini(newTabContent, textWidth())
newTabPage = structs.Page{Content: renderedNewTabContent, Links: newTabLinks}
modalInit()
// Setup map of keys to functions here
// Changing tabs, new tab, etc
App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
_, ok := App.GetFocus().(*cview.Button)
if ok {
// It's focused on a modal right now, nothing should interrupt
return event
}
_, ok = App.GetFocus().(*cview.InputField)
if ok {
// An InputField is in focus, nothing should interrupt
return event
}
switch event.Key() {
case tcell.KeyCtrlT:
NewTab()
return nil
case tcell.KeyCtrlW:
CloseTab()
return nil
case tcell.KeyCtrlR:
Reload()
return nil
case tcell.KeyCtrlH:
URL(viper.GetString("a-general.home"))
return nil
case tcell.KeyCtrlQ:
Stop()
return nil
case tcell.KeyRune:
// Regular key was sent
switch string(event.Rune()) {
case " ":
// Space starts typing, like Bombadillo
bottomBar.SetLabel("[::b]URL or search: [::-]")
bottomBar.SetText("")
App.SetFocus(bottomBar)
return nil
case "q":
Stop()
return nil
case "R":
Reload()
return nil
case "b":
histBack()
return nil
case "f":
histForward()
return nil
case "?":
Help()
return nil
// Shift+NUMBER keys, for switching to a specific tab
case "!":
SwitchTab(0)
return nil
case "@":
SwitchTab(1)
return nil
case "#":
SwitchTab(2)
return nil
case "$":
SwitchTab(3)
return nil
case "%":
SwitchTab(4)
return nil
case "^":
SwitchTab(5)
return nil
case "&":
SwitchTab(6)
return nil
case "*":
SwitchTab(7)
return nil
case "(":
SwitchTab(8)
return nil
case ")": // Zero key goes to the last tab
SwitchTab(NumTabs() - 1)
return nil
}
}
return event
})
}
// Stop stops the app gracefully.
// In the future it will handle things like ongoing downloads, etc
func Stop() {
App.Stop()
}
// NewTab opens a new tab and switches to it, displaying the
// the default empty content because there's no URL.
func NewTab() {
// Create TextView in tabViews and change curTab
// Set the textView options, and the changed func to App.Draw()
// SetDoneFunc to do link highlighting
// Add view to pages and switch to it
curTab = NumTabs()
tabMap[curTab] = &newTabPage
tabViews[curTab] = cview.NewTextView().
SetDynamicColors(true).
SetRegions(true).
SetScrollable(true).
SetWrap(false).
SetText(renderedNewTabContent).
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
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
linkN, _ := strconv.Atoi(currentSelection[0])
followLink(tabMap[curTab].Url, tabMap[curTab].Links[linkN])
return
} else {
tabViews[curTab].Highlight("0").ScrollToHighlight()
}
} else if len(currentSelection) > 0 {
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()
}
})
tabHist[curTab] = []string{}
// Can't go backwards, but this isn't the first page either.
// The first page will be the next one the user goes to.
tabHistPos[curTab] = -1
tabPages.AddAndSwitchToPage(strconv.Itoa(curTab), tabViews[curTab], true)
App.SetFocus(tabViews[curTab])
// 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
fmt.Fprintf(tabRow, `["%d"][darkcyan] %d [white][""]|`, curTab, curTab+1)
tabRow.Highlight(strconv.Itoa(curTab)).ScrollToHighlight()
bottomBar.SetLabel("")
bottomBar.SetText("")
// Force a draw, just in case
App.Draw()
}
// CloseTab closes the current tab and switches to the one to its left.
func CloseTab() {
// Basically the NewTab() func inverted
// TODO: Support closing middle tabs, by renumbering all the maps
// So that tabs to the right of the closed tabs point to the right places
// For now you can only close the right-most tab
if curTab != NumTabs()-1 {
return
}
if NumTabs() <= 1 {
// There's only one tab open, close the app instead
Stop()
return
}
delete(tabMap, curTab)
tabPages.RemovePage(strconv.Itoa(curTab))
delete(tabViews, curTab)
delete(tabHist, curTab)
delete(tabHistPos, curTab)
if curTab <= 0 {
curTab = NumTabs() - 1
} else {
curTab--
}
tabPages.SwitchToPage(strconv.Itoa(curTab)) // Go to previous page
// Rewrite the tab display
tabRow.Clear()
for i := 0; i < NumTabs(); i++ {
fmt.Fprintf(tabRow, `["%d"][darkcyan] %d [white][""]|`, i, i+1)
}
tabRow.Highlight(strconv.Itoa(curTab)).ScrollToHighlight()
bottomBar.SetLabel("")
bottomBar.SetText(tabMap[curTab].Url)
// Just in case
App.Draw()
}
// SwitchTab switches to a specific tab, using its number, 0-indexed.
// The tab numbers are clamped to the end, so for example numbers like -5 and 1000 are still valid.
// This means that calling something like SwitchTab(curTab - 1) will never cause an error.
func SwitchTab(tab int) {
if tab < 0 {
tab = 0
}
if tab > NumTabs()-1 {
tab = NumTabs() - 1
}
curTab = tab % NumTabs()
tabPages.SwitchToPage(strconv.Itoa(curTab))
tabRow.Highlight(strconv.Itoa(curTab)).ScrollToHighlight()
bottomBar.SetLabel("")
bottomBar.SetText(tabMap[curTab].Url)
// Just in case
App.Draw()
}
func Reload() {
cache.Remove(tabMap[curTab].Url)
go handleURL(tabMap[curTab].Url)
}
// URL loads and handles the provided URL for the current tab.
// It should be an absolute URL.
func URL(u string) {
go func() {
final, displayed := handleURL(u)
if displayed {
addToHist(final)
}
}()
}
func NumTabs() int {
return len(tabViews)
}
// Help displays the help and keybindings.
func Help() {
helpTable.ScrollToBeginning()
tabPages.SwitchToPage("help")
App.Draw()
}