From cfe58cb5f37e8b8cb434e3a64348226c3a2d7f58 Mon Sep 17 00:00:00 2001 From: makeworld Date: Tue, 23 Jun 2020 20:07:25 -0400 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20bookmarks!=20-=20fixes=20#10?= =?UTF-8?q?=20fixes=20#13?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 14 +++-- README.md | 4 +- bookmarks/bookmarks.go | 60 +++++++++++++++++++++ cache/cache.go | 5 +- config/config.go | 53 ++++++++++++++++-- config/default.go | 8 +-- default-config.toml | 8 +-- display/bookmarks.go | 120 +++++++++++++++++++++++++++++++++++++++++ display/display.go | 63 ++++++++++++---------- display/help.go | 32 ++++++++--- display/modals.go | 26 +++++++-- display/newtab.go | 4 +- display/private.go | 58 ++++++++++++++------ go.sum | 16 ------ renderer/renderer.go | 3 +- structs/structs.go | 12 ++--- 16 files changed, 381 insertions(+), 105 deletions(-) create mode 100644 bookmarks/bookmarks.go create mode 100644 display/bookmarks.go diff --git a/CHANGELOG.md b/CHANGELOG.md index bd1a8df..0e1e75f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- Support over 55 charsets (#3) -- Add titles to error modals +- **Bookmarks** (#10) +- **Support over 55 charsets** (#3) +- **Search using the bottom bar** +- Add titles to all modals - Store ports in TOFU database (#7) - Search from bottom bar - Wrapping based on terminal width (#1) -- `left_margin` config option +- `left_margin` config option (#1) - Right margin for text (#1) - Desktop entry file - Option to continue anyway when cert doesn't match TOFU database @@ -21,7 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Connection timeout is 15 seconds (was 5s) - Hash `SubjectPublicKeyInfo` for TOFU instead (#7) -- `wrap_width` config option became `max_width` +- `wrap_width` config option became `max_width` (#1) +- Make the help table look better ### Removed - Opening multiple URLs from the command line (threading issues) @@ -30,7 +33,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Reset bottom bar on error / invalid URL - Side scrolling doesn't cut off text on the left side (#1) - Mark status code 21 as invalid -- You can't type on the bottom bar as it's loading +- Bottom bar is not in focus after clicking Enter +- Badly formed links on pages can no longer crash the browser ## [1.0.0] - 2020-06-18 diff --git a/README.md b/README.md index 5308a92..9c5e228 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,8 @@ Features in *italics* are in the master branch, but not in the latest release. - [x] Basic forward/backward history, for each tab - [x] Input (Status Code 10 & 11) - [x] *Multiple charset support (over 55)* -- [x] *Built-in search using GUS* -- [ ] Bookmarks +- [x] *Built-in search (uses GUS by default)* +- [x] *Bookmarks* - [ ] Search in pages with Ctrl-F - [ ] Download pages and arbitrary data - [ ] Emoji favicons diff --git a/bookmarks/bookmarks.go b/bookmarks/bookmarks.go new file mode 100644 index 0000000..8dd3baf --- /dev/null +++ b/bookmarks/bookmarks.go @@ -0,0 +1,60 @@ +package bookmarks + +import ( + "encoding/base32" + "strings" + + "github.com/makeworld-the-better-one/amfora/config" +) + +var bkmkStore = config.BkmkStore + +// bkmkKey returns the viper key for the given bookmark URL. +// Note that URLs are the keys, NOT the bookmark name. +func bkmkKey(url string) string { + // Keys are base32 encoded URLs to prevent any bad chars like periods from being used + return "bookmarks." + base32.StdEncoding.EncodeToString([]byte(url)) +} + +func Set(url, name string) { + bkmkStore.Set(bkmkKey(url), name) + bkmkStore.WriteConfig() +} + +// Get returns the NAME of the bookmark, given the URL. +// It also returns a bool indicating whether it exists. +func Get(url string) (string, bool) { + name := bkmkStore.GetString(bkmkKey(url)) + return name, name != "" +} + +func Remove(url string) { + // XXX: Viper can't actually delete keys, which means the bookmarks file might get clouded + // with non-entries over time. + bkmkStore.Set(bkmkKey(url), "") + bkmkStore.WriteConfig() +} + +// All returns all the bookmarks in a map of URLs to names. +func All() map[string]string { + ret := make(map[string]string) + + bkmksMap, ok := bkmkStore.AllSettings()["bookmarks"].(map[string]interface{}) + if !ok { + // No bookmarks stored yet, return empty map + return ret + } + for b32Url, name := range bkmksMap { + if n, ok := name.(string); n == "" || !ok { + // name is not a string, or it's empty - ignore + continue + } + url, err := base32.StdEncoding.DecodeString(strings.ToUpper(b32Url)) + if err != nil { + // This would only happen if a user messed around with the bookmarks file + continue + } + ret[string(url)] = name.(string) + } + return ret +} diff --git a/cache/cache.go b/cache/cache.go index c16c826..6710260 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -4,6 +4,7 @@ package cache import ( "net/url" + "strings" "sync" "github.com/makeworld-the-better-one/amfora/structs" @@ -47,8 +48,8 @@ func removeUrl(url string) { // If your page is larger than the max cache size, the provided page // will silently not be added to the cache. func Add(p *structs.Page) { - if p.Url == "" { - // Just in case, don't waste cache on new tab page + if p.Url == "" || strings.HasPrefix(p.Url, "about:") { + // Just in case, these pages shouldn't be cached return } // Never cache pages with query strings, to reduce unexpected behaviour diff --git a/config/config.go b/config/config.go index 99d47a2..b0983d4 100644 --- a/config/config.go +++ b/config/config.go @@ -19,12 +19,17 @@ var TofuStore = viper.New() var tofuDBDir string var tofuDBPath string +// Bookmarks +var BkmkStore = viper.New() +var bkmkDir string +var bkmkPath string + func Init() error { home, err := homedir.Dir() if err != nil { panic(err) } - // Cache AppData path + // Store AppData path if runtime.GOOS == "windows" { appdata, ok := os.LookupEnv("APPDATA") if ok { @@ -33,7 +38,8 @@ func Init() error { amforaAppData = filepath.Join(home, filepath.FromSlash("AppData/Roaming/amfora/")) } } - // Cache config directory and file paths + + // Store config directory and file paths if runtime.GOOS == "windows" { configDir = amforaAppData } else { @@ -48,7 +54,7 @@ func Init() error { } configPath = filepath.Join(configDir, "config.toml") - // Cache TOFU db directory and file paths + // Store TOFU db directory and file paths if runtime.GOOS == "windows" { // Windows just stores it in APPDATA along with other stuff tofuDBDir = amforaAppData @@ -64,6 +70,25 @@ func Init() error { } tofuDBPath = filepath.Join(tofuDBDir, "tofu.toml") + // Store bookmarks dir and path + if runtime.GOOS == "windows" { + // Windows just keeps it in APPDATA along with other Amfora files + bkmkDir = amforaAppData + } else { + // XDG data dir on POSIX systems + xdg_data, ok := os.LookupEnv("XDG_DATA_HOME") + if ok && strings.TrimSpace(xdg_data) != "" { + bkmkDir = filepath.Join(xdg_data, "amfora") + } else { + // Default to ~/.local/share/amfora + bkmkDir = filepath.Join(home, ".local", "share", "amfora") + } + } + bkmkPath = filepath.Join(bkmkDir, "bookmarks.toml") + + // Create necessary files and folders + + // Config err = os.MkdirAll(configDir, 0755) if err != nil { return err @@ -78,12 +103,20 @@ func Init() error { } f.Close() } - + // TOFU err = os.MkdirAll(tofuDBDir, 0755) if err != nil { return err } os.OpenFile(tofuDBPath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666) + // Bookmarks + err = os.MkdirAll(bkmkDir, 0755) + if err != nil { + return err + } + os.OpenFile(bkmkPath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666) + + // Setup vipers TofuStore.SetConfigFile(tofuDBPath) TofuStore.SetConfigType("toml") @@ -92,6 +125,18 @@ func Init() error { return err } + BkmkStore.SetConfigFile(bkmkPath) + BkmkStore.SetConfigType("toml") + err = BkmkStore.ReadInConfig() + if err != nil { + return err + } + BkmkStore.Set("DO NOT TOUCH", true) + err = BkmkStore.WriteConfig() + if err != nil { + return err + } + viper.SetDefault("a-general.home", "gemini.circumlunar.space") viper.SetDefault("a-general.http", "default") viper.SetDefault("a-general.search", "gus.guru/search") diff --git a/config/default.go b/config/default.go index 08688c5..b5f4a60 100644 --- a/config/default.go +++ b/config/default.go @@ -9,7 +9,7 @@ var defaultConf = []byte(`# This is the default config file. # gemini://example.com # //example.com # example.com -# example.com:1901 +# example.com:123 [a-general] home = "gemini://gemini.circumlunar.space" @@ -23,16 +23,10 @@ http = "default" search = "gemini://gus.guru/search" # Any URL that will accept a query string can be put here color = true # Whether colors will be used in the terminal bullets = true # Whether to replace list asterisks with unicode bullets - # A number from 0 to 1, indicating what percentage of the terminal width the left margin should take up. left_margin = 0.15 max_width = 100 # The max number of columns to wrap a page's text to. Preformatted blocks are not wrapped. -[bookmarks] -# Make sure to quote the key names if you edit this part yourself -# Example: -# "CAPCOM" = "gemini://gemini.circumlunar.space/capcom/" - # Options for page cache - which is only for text/gemini pages # Increase the cache size to speed up browsing at the expense of memory [cache] diff --git a/default-config.toml b/default-config.toml index d633b8b..f3b094b 100644 --- a/default-config.toml +++ b/default-config.toml @@ -6,7 +6,7 @@ # gemini://example.com # //example.com # example.com -# example.com:1901 +# example.com:123 [a-general] home = "gemini://gemini.circumlunar.space" @@ -20,16 +20,10 @@ http = "default" search = "gemini://gus.guru/search" # Any URL that will accept a query string can be put here color = true # Whether colors will be used in the terminal bullets = true # Whether to replace list asterisks with unicode bullets - # A number from 0 to 1, indicating what percentage of the terminal width the left margin should take up. left_margin = 0.15 max_width = 100 # The max number of columns to wrap a page's text to. Preformatted blocks are not wrapped. -[bookmarks] -# Make sure to quote the key names if you edit this part yourself -# Example: -# "CAPCOM" = "gemini://gemini.circumlunar.space/capcom/" - # Options for page cache - which is only for text/gemini pages # Increase the cache size to speed up browsing at the expense of memory [cache] diff --git a/display/bookmarks.go b/display/bookmarks.go new file mode 100644 index 0000000..c3f8193 --- /dev/null +++ b/display/bookmarks.go @@ -0,0 +1,120 @@ +package display + +import ( + "fmt" + "strconv" + "strings" + + "github.com/makeworld-the-better-one/amfora/renderer" + "github.com/makeworld-the-better-one/amfora/structs" + + "github.com/gdamore/tcell" + "github.com/makeworld-the-better-one/amfora/bookmarks" + "gitlab.com/tslocum/cview" +) + +// For adding and removing bookmarks, basically a clone of the input modal. +var bkmkModal = cview.NewModal(). + SetBackgroundColor(tcell.ColorTeal). + SetButtonBackgroundColor(tcell.ColorNavy). + SetButtonTextColor(tcell.ColorWhite). + SetTextColor(tcell.ColorWhite) + +// bkmkCh is for the user action +var bkmkCh = make(chan int) // 1, 0, -1 for add/update, cancel, and remove +var bkmkModalText string // The current text of the input field in the modal + +func bkmkInit() { + bkmkModal.SetBorder(true) + bkmkModal.SetBorderColor(tcell.ColorWhite) + bkmkModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) { + switch buttonLabel { + case "Add": + bkmkCh <- 1 + case "Change": + bkmkCh <- 1 + case "Remove": + bkmkCh <- -1 + case "Cancel": + bkmkCh <- 0 + } + + //tabPages.SwitchToPage(strconv.Itoa(curTab)) - handled in bkmk() + }) + bkmkModal.GetFrame().SetTitleColor(tcell.ColorWhite) + bkmkModal.GetFrame().SetTitleAlign(cview.AlignCenter) + bkmkModal.GetFrame().SetTitle(" Add Bookmark ") +} + +// Bkmk displays the "Add a bookmark" modal. +// It accepts the default value for the bookmark name that will be displayed, but can be changed by the user. +// It also accepts a bool indicating whether this page already has a bookmark. +// It returns the bookmark name and the bookmark action: +// 1, 0, -1 for add/update, cancel, and remove +func openBkmkModal(name string, exists bool) (string, int) { + // Basically a copy of Input() + + // Remove and re-add input field - to clear the old text + if bkmkModal.GetForm().GetFormItemCount() > 0 { + bkmkModal.GetForm().RemoveFormItem(0) + } + bkmkModalText = "" + bkmkModal.GetForm().AddInputField("Name: ", name, 0, nil, + func(text string) { + // Store for use later + bkmkModalText = text + }) + + bkmkModal.ClearButtons() + if exists { + bkmkModal.SetText("Change or remove the bookmark for the current page?") + bkmkModal.AddButtons([]string{"Change", "Remove", "Cancel"}) + } else { + bkmkModal.SetText("Create a bookmark for the current page?") + bkmkModal.AddButtons([]string{"Add", "Cancel"}) + } + tabPages.ShowPage("bkmk") + tabPages.SendToFront("bkmk") + App.SetFocus(bkmkModal) + App.Draw() + + action := <-bkmkCh + tabPages.SwitchToPage(strconv.Itoa(curTab)) + + return bkmkModalText, action +} + +// Bookmarks displays the bookmarks page on the current tab. +func Bookmarks() { + // Gather bookmarks + rawContent := "# Bookmarks\r\n\r\n" + for url, name := range bookmarks.All() { + rawContent += fmt.Sprintf("=> %s %s\r\n", url, name) + } + // Render and display + content, links := renderer.RenderGemini(rawContent, textWidth()) + page := structs.Page{Content: content, Links: links, Url: "about:bookmarks"} + setPage(&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 can also be called to edit an existing bookmark. +func addBookmark() { + if !strings.HasPrefix(tabMap[curTab].Url, "gemini://") { + // Can't make bookmarks for other kinds of URLs + return + } + + name, exists := bookmarks.Get(tabMap[curTab].Url) + // Open a bookmark modal with the current name of the bookmark, if it exists + newName, action := openBkmkModal(name, exists) + switch action { + case 1: + // Add/change the bookmark + bookmarks.Set(tabMap[curTab].Url, newName) + case -1: + bookmarks.Remove(tabMap[curTab].Url) + } + // Other case is action = 0, meaning "Cancel", so nothing needs to happen +} diff --git a/display/display.go b/display/display.go index 41b93fe..79e6f5e 100644 --- a/display/display.go +++ b/display/display.go @@ -5,13 +5,11 @@ import ( "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/renderer" "github.com/makeworld-the-better-one/amfora/structs" + "github.com/spf13/viper" "gitlab.com/tslocum/cview" ) @@ -28,24 +26,13 @@ var bottomBar = cview.NewInputField(). 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. +// The only pages that don't confine to this scheme are those named after modals, +// which are 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) +var tabPages = cview.NewPages() // The tabs at the top with titles var tabRow = cview.NewTextView(). @@ -116,15 +103,17 @@ func Init() { if c == 0 { tableCell = cview.NewTableCell(cells[cell]). SetAttributes(tcell.AttrBold). - SetExpansion(1) + SetExpansion(1). + SetAlign(cview.AlignCenter) } else { - tableCell = cview.NewTableCell(cells[cell]). + tableCell = cview.NewTableCell(" " + cells[cell]). SetExpansion(2) } helpTable.SetCell(r, c, tableCell) cell++ } } + tabPages.AddPage("help", helpTable, true, false) bottomBar.SetBackgroundColor(tcell.ColorWhite) bottomBar.SetDoneFunc(func(key tcell.Key) { @@ -147,7 +136,7 @@ func Init() { 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, ".")) { + if strings.Contains(query, " ") || (!strings.Contains(query, "//") && !strings.Contains(query, ".") && !strings.HasPrefix(query, "about:")) { URL(viper.GetString("a-general.search") + "?" + pathEscape(query)) } else { // Full URL @@ -178,7 +167,7 @@ func Init() { // 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} + newTabPage = structs.Page{Content: renderedNewTabContent, Links: newTabLinks, Url: "about:newtab"} modalInit() @@ -212,6 +201,13 @@ func Init() { case tcell.KeyCtrlQ: Stop() return nil + case tcell.KeyCtrlB: + Bookmarks() + addToHist("about:bookmarks") + return nil + case tcell.KeyCtrlD: + go addBookmark() + return nil case tcell.KeyRune: // Regular key was sent switch string(event.Rune()) { @@ -422,6 +418,22 @@ func Reload() { // URL loads and handles the provided URL for the current tab. // It should be an absolute URL. func URL(u string) { + // Some code is copied in followLink() + + if u == "about:bookmarks" { + Bookmarks() + addToHist("about:bookmarks") + return + } + if u == "about:newtab" { + setPage(&newTabPage) + return + } + if strings.HasPrefix(u, "about:") { + Error("Error", "Not a valid 'about:' URL.") + return + } + go func() { final, displayed := handleURL(u) if displayed { @@ -433,10 +445,3 @@ func URL(u string) { func NumTabs() int { return len(tabViews) } - -// Help displays the help and keybindings. -func Help() { - helpTable.ScrollToBeginning() - tabPages.SwitchToPage("help") - App.Draw() -} diff --git a/display/help.go b/display/help.go index d4f84e7..fe17fce 100644 --- a/display/help.go +++ b/display/help.go @@ -1,6 +1,11 @@ package display -import "strings" +import ( + "strings" + + "github.com/gdamore/tcell" + "gitlab.com/tslocum/cview" +) var helpCells = strings.TrimSpace(` ?|Bring up this help. @@ -8,17 +13,32 @@ Esc|Leave the help Arrow keys, h/j/k/l|Scroll and move a page. Tab|Navigate to the next item in a popup. Shift-Tab|Navigate to the previous item in a popup. -Ctrl-H|Go home -Ctrl-T|New tab -Ctrl-W|Close tab. For now, only the right-most tab can be closed. b|Go back a page f|Go forward a page g|Go to top of document G|Go to bottom of document spacebar|Open bar at the bottom - type a URL or link number Enter|On a page this will start link highlighting. Press Tab and Shift-Tab to pick different links. Press enter again to go to one. -Ctrl-R, R|Reload a page. This also clears the cache. -q, Ctrl-Q|Quit Shift-NUMBER|Go to a specific tab. Shift-0, )|Go to the last tab. +Ctrl-H|Go home +Ctrl-T|New tab +Ctrl-W|Close tab. For now, only the right-most tab can be closed. +Ctrl-R, R|Reload a page, discarding the cached version. +Ctrl-B|View bookmarks +Ctrl-D|Add, change, or remove a bookmark for the current page. +q, Ctrl-Q|Quit `) + +var helpTable = cview.NewTable(). + SetSelectable(false, false). + SetFixed(1, 2). + SetBorders(true). + SetBordersColor(tcell.ColorGray) + +// Help displays the help and keybindings. +func Help() { + helpTable.ScrollToBeginning() + tabPages.SwitchToPage("help") + App.Draw() +} diff --git a/display/modals.go b/display/modals.go index 8d6b7a1..bd8bff8 100644 --- a/display/modals.go +++ b/display/modals.go @@ -9,7 +9,8 @@ import ( "gitlab.com/tslocum/cview" ) -// This file contains code for all the popups / modals used in the display +// This file contains code for the popups / modals used in the display. +// The bookmark modal is in bookmarks.go var infoModal = cview.NewModal(). SetBackgroundColor(tcell.ColorGray). @@ -45,20 +46,30 @@ var yesNoModal = cview.NewModal(). var yesNoCh = make(chan bool) func modalInit() { + tabPages.AddPage("info", infoModal, false, false). + AddPage("error", errorModal, false, false). + AddPage("input", inputModal, false, false). + AddPage("yesno", yesNoModal, false, false). + AddPage("bkmk", bkmkModal, false, false) + // Modal functions that can't be added up above, because they return the wrong type + infoModal.SetBorder(true) infoModal.SetBorderColor(tcell.ColorWhite) infoModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) { tabPages.SwitchToPage(strconv.Itoa(curTab)) }) + infoModal.GetFrame().SetTitleColor(tcell.ColorWhite) + infoModal.GetFrame().SetTitleAlign(cview.AlignCenter) + infoModal.GetFrame().SetTitle(" Info ") errorModal.SetBorder(true) errorModal.SetBorderColor(tcell.ColorWhite) - errorModal.GetFrame().SetTitleColor(tcell.ColorWhite) - errorModal.GetFrame().SetTitleAlign(cview.AlignCenter) errorModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) { tabPages.SwitchToPage(strconv.Itoa(curTab)) }) + errorModal.GetFrame().SetTitleColor(tcell.ColorWhite) + errorModal.GetFrame().SetTitleAlign(cview.AlignCenter) inputModal.SetBorder(true) inputModal.SetBorderColor(tcell.ColorWhite) @@ -72,6 +83,9 @@ func modalInit() { //tabPages.SwitchToPage(strconv.Itoa(curTab)) - handled in Input() }) + inputModal.GetFrame().SetTitleColor(tcell.ColorWhite) + inputModal.GetFrame().SetTitleAlign(cview.AlignCenter) + inputModal.GetFrame().SetTitle(" Input ") yesNoModal.SetBorder(true) yesNoModal.SetBorderColor(tcell.ColorWhite) @@ -84,6 +98,10 @@ func modalInit() { //tabPages.SwitchToPage(strconv.Itoa(curTab)) - Handled in YesNo() }) + yesNoModal.GetFrame().SetTitleColor(tcell.ColorWhite) + yesNoModal.GetFrame().SetTitleAlign(cview.AlignCenter) + + bkmkInit() } // Error displays an error on the screen in a modal. @@ -157,7 +175,7 @@ func YesNo(prompt string) bool { // Tofu displays the TOFU warning modal. // It returns a bool indicating whether the user wants to continue. func Tofu(host string) bool { - // Reuses yesno modal, with error colour + // Reuses yesNoModal, with error colour yesNoModal.SetBackgroundColor(tcell.ColorMaroon) yesNoModal.SetText( diff --git a/display/newtab.go b/display/newtab.go index 6a45b73..ee451bd 100644 --- a/display/newtab.go +++ b/display/newtab.go @@ -8,6 +8,8 @@ Press the ? key at any time to bring up the help, and see other keybindings. Mos Happy browsing! -=> //gemini.circumlunar.space Gemini homepage +=> about:bookmarks Bookmarks + +=> //gemini.circumlunar.space Project Gemini => https://github.com/makeworld-the-better-one/amfora Amfora homepage [HTTPS] ` diff --git a/display/private.go b/display/private.go index df38f97..6ef5251 100644 --- a/display/private.go +++ b/display/private.go @@ -61,6 +61,10 @@ func tabHasContent() bool { // 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 } @@ -84,16 +88,44 @@ func applyScroll() { // 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. func followLink(prev, next string) { - saveScroll() // Likely called later on anyway, here just in case - prevParsed, _ := url.Parse(prev) - nextParsed, err := url.Parse(next) + + // Copied from URL() + if next == "about:bookmarks" { + Bookmarks() + addToHist("about:bookmarks") + return + } + if strings.HasPrefix(next, "about:") { + Error("Error", "Not a valid 'about:' URL for linking") + return + } + + if tabHasContent() { + saveScroll() // Likely called later on anyway, here just in case + prevParsed, _ := url.Parse(prev) + nextParsed, err := url.Parse(next) + if err != nil { + Error("URL Error", "Link URL could not be parsed") + return + } + nextURL := prevParsed.ResolveReference(nextParsed).String() + go func() { + final, displayed := handleURL(nextURL) + if displayed { + addToHist(final) + } + }() + return + } + // No content on current tab, so the "prev" URL is not valid. + // An example is the about:newtab page + _, err := url.Parse(next) if err != nil { Error("URL Error", "Link URL could not be parsed") return } - nextURL := prevParsed.ResolveReference(nextParsed).String() go func() { - final, displayed := handleURL(nextURL) + final, displayed := handleURL(next) if displayed { addToHist(final) } @@ -112,15 +144,9 @@ func addLeftMargin(text string) string { func setPage(p *structs.Page) { saveScroll() // Save the scroll of the previous page - if !p.Displayable { - // Add margin to page based on terminal width - p.Content = addLeftMargin(p.Content) - p.Displayable = true - } - // Change page on screen tabMap[curTab] = p - tabViews[curTab].SetText(p.Content) + tabViews[curTab].SetText(addLeftMargin(p.Content)) tabViews[curTab].Highlight("") // Turn off highlights tabViews[curTab].ScrollToBeginning() @@ -144,12 +170,14 @@ func handleURL(u string) (string, bool) { App.SetFocus(tabViews[curTab]) - //logger.Log.Printf("Sent: %s", u) + // To allow linking to the bookmarks page, and history browsing + if u == "about:bookmarks" { + Bookmarks() + return "about:bookmarks", true + } u = normalizeURL(u) - //logger.Log.Printf("Normalized: %s", u) - parsed, err := url.Parse(u) if err != nil { Error("URL Error", err.Error()) diff --git a/go.sum b/go.sum index b7c584f..fa33a90 100644 --- a/go.sum +++ b/go.sum @@ -32,20 +32,17 @@ github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= -github.com/gdamore/tcell v1.3.0 h1:r35w0JBADPZCVQijYebl6YMWWtHRqVEGt7kL2eBADRM= github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM= github.com/gdamore/tcell v1.3.1-0.20200608133353-cb1e5d6fa606 h1:Y00kKKKYVyn7InlCMRcnZbwcjHFIsgkjU0Bn1F5re4o= github.com/gdamore/tcell v1.3.1-0.20200608133353-cb1e5d6fa606/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0= @@ -143,7 +140,6 @@ github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eI github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.3.1 h1:cCBH2gTD2K0OtLlv/Y5H01VQCqmlDxz30kS5Y5bqfLA= github.com/mitchellh/mapstructure v1.3.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= @@ -152,7 +148,6 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.8.0 h1:Keo9qb7iRJs2voHvunFtuuYFsbWeOBh8/P9v/kVMFtw= github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs= @@ -183,25 +178,20 @@ github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIK github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -279,7 +269,6 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -288,7 +277,6 @@ golang.org/x/sys v0.0.0-20200610111108-226ff32320da h1:bGb80FudwxpeucJUjPYJXuJ8H golang.org/x/sys v0.0.0-20200610111108-226ff32320da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -334,23 +322,19 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww= gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200603094226-e3079894b1e8 h1:jL/vaozO53FMfZLySWM+4nulF3gQEC6q5jH90LPomDo= gopkg.in/yaml.v3 v3.0.0-20200603094226-e3079894b1e8/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/renderer/renderer.go b/renderer/renderer.go index f9bac79..6d33e28 100644 --- a/renderer/renderer.go +++ b/renderer/renderer.go @@ -71,7 +71,8 @@ func convertRegularGemini(s string, numLinks int, width int) (string, []string) links = append(links, url) if viper.GetBool("a-general.color") { - if pU, _ := urlPkg.Parse(url); pU.Scheme == "" || pU.Scheme == "gemini" { + pU, err := urlPkg.Parse(url) + if err == nil && (pU.Scheme == "" || pU.Scheme == "gemini" || pU.Scheme == "about") { // A gemini link // Add the link text in blue (in a region), and a gray link number to the left of it lines[i] = `[silver::b][` + strconv.Itoa(numLinks+len(links)) + "[]" + "[-::-] " + diff --git a/structs/structs.go b/structs/structs.go index 5e35b37..1b6b88e 100644 --- a/structs/structs.go +++ b/structs/structs.go @@ -2,12 +2,12 @@ package structs // Page is for storing UTF-8 text/gemini pages, as well as text/plain pages. type Page struct { - Url string - Content string // The processed content, NOT raw. Uses cview colour tags. All link/link texts must have region tags. - Links []string // URLs, for each region in the content. - Row int // Scroll position - Column int // ditto - Displayable bool // Set to true once the content has been modified to display nicely on the screen - margins added + Url string + Content string // The processed content, NOT raw. Uses cview colour tags. All link/link texts must have region tags. + Links []string // URLs, for each region in the content. + Row int // Scroll position + Column int // ditto + //Displayable bool // Set to true once the content has been modified to display nicely on the screen - margins added } // Size returns an approx. size of a Page in bytes.