diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..4f64e86 --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,15 @@ +name: golangci-lint +on: [push, pull_request] +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: golangci-lint + uses: golangci/golangci-lint-action@v2 + with: + # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. + version: v1.30 + # Optional: show only new issues if it's a pull request. The default value is `false`. + only-new-issues: true diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..e010a47 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,38 @@ +linters: + fast: false + disable-all: true + enable: + - deadcode + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - structcheck + - typecheck + - unused + - varcheck + - dupl + - exhaustive + - exportloopref + - goconst + - gocritic + - goerr113 + - gofmt + - goimports + - golint + - goprintffuncname + - interfacer + - lll + - maligned + - misspell + - nakedret + - nolintlint + - prealloc + - scopelint + - unconvert + - unparam + +issues: + exclude-use-default: true + max-issues-per-linter: 0 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..7935a21 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,27 @@ +language: go + +go: + #- "1.11" # Debian Stable golang version, fails - see below + #- "1.12" # Also fails due to progressbar Millisecond requirement + - "1.13" + - "1.14" + - "1.15" + +script: + - go test ./... + - go build + +env: + GO111MODULE=on + +cache: + directories: + - $HOME/.cache/go-build + - $GOPATH/pkg/mod + +# TODO: GitHub Releases deploy + +notifications: + email: + on_success: never + on_failure: always diff --git a/CHANGELOG.md b/CHANGELOG.md index 98364e2..36b6743 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,14 +9,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - **Feed & page subscription** (#61) - **Emoji favicons** can now be seen if `emoji_favicons` is enabled in the config (#62) +- **Proxy support** - specify a proxy in the config for all requests to go through it (#66) - The `shift_numbers` key in the config was added, so that non US keyboard users can navigate tabs (#64) - F1 and F2 keys for navigating to the previous and next tabs (#64) +- Resolving any relative path (starting with a `.`) in the bottom bar is supported, not just `..` (#71) +- Set programs in config to open other schemes like `gopher://` or `magnet:` (#74) +- Auto-redirecting can be enabled - redirect within Gemini up to 5 times automatically (#75) ### Changed +- Update to [go-gemini](https://github.com/makeworld-the-better-one/go-gemini) v0.8.4 ### Fixed - Two digit (and higher) link texts are now in line with one digit ones (#60) - Race condition when reloading pages, could have caused the cache to still be used +- Prevent panic (crash) when the server sends an error with an empty meta string (#73) +- URLs with with colon-only schemes (like `mailto:`) are properly recognized ## [1.4.0] - 2020-07-28 diff --git a/README.md b/README.md index ffbae0c..b061c3b 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@
Image modified from: amphora by Alvaro Cabrera from the Noun Project
- +[![travis build status](https://img.shields.io/travis/com/makeworld-the-better-one/amfora)](https://https://travis-ci.com/github/makeworld-the-better-one/amfora) [![go reportcard](https://goreportcard.com/badge/github.com/makeworld-the-better-one/amfora)](https://goreportcard.com/report/github.com/makeworld-the-better-one/amfora) [![license GPLv3](https://img.shields.io/github/license/makeworld-the-better-one/amfora)](https://www.gnu.org/licenses/gpl-3.0.en.html) @@ -19,7 +19,7 @@ Amfora aims to be the best looking [Gemini](https://gemini.circumlunar.space/) client with the most features... all in the terminal. It does not support Gopher or other non-Web protocols - check out [Bombadillo](http://bombadillo.colorfield.space/) for that. -It also aims to be completely cross platform, with full Windows support. If you're on Windows, I would not recommend using the default terminal software. Maybe use Powershell (comes with Windows) or [Cmder](https://cmder.net/) instead. Note that some of the application colors will not display correctly on most Windows terminals, but all functionality will still work. +It also aims to be completely cross platform, with full Windows support. If you're on Windows, I would not recommend using the default terminal software. Use [Windows Terminal](https://www.microsoft.com/en-us/p/windows-terminal/9n0dx20hk701) instead. Note that some of the application colors might not display correctly on Windows, but all functionality will still work. It fully passes Sean Conman's client torture test, including the new Unicode tests. It mostly passes the Egsam test. @@ -57,16 +57,16 @@ brew upgrade amfora ``` ### From Source -This section is for programmers who want to install from source. +This section is for programmers who want to install from source. Make sure you're using Go 1.13 at least, as earlier versions will fail to build. Install latest release: ``` -GO111MODULE=on go get -u github.com/makeworld-the-better-one/amfora +GO111MODULE=on go get github.com/makeworld-the-better-one/amfora ``` Install latest commit: ``` -GO111MODULE=on go get -u github.com/makeworld-the-better-one/amfora@master +GO111MODULE=on go get github.com/makeworld-the-better-one/amfora@master ``` ## Usage @@ -99,15 +99,18 @@ Features in *italics* are in the master branch, but not in the latest release. - See `gemini://mozz.us/files/rfc_gemini_favicon.gmi` for details - [x] *Subscribe to RSS and Atom feeds and display them* - Subscribing to page changes, similar to how Spacewalk works, is also supported -- [ ] Stream support +- [x] *Proxying* + - All requests can optionally be sent through another server + - A gemini proxy server implementation currently does not exist, but Amfora will support it when it does! +- [ ] Support Markdown rendering +- [ ] Search in pages with Ctrl-F - [ ] Full client certificate UX within the client - Create transient and permanent certs within the client, per domain - Manage and browse them - Similar to [Kristall](https://github.com/MasterQ32/kristall) - https://lists.orbitalfox.eu/archives/gemini/2020/001400.html +- [ ] Stream support - [ ] Table of contents for pages -- [ ] Search in pages with Ctrl-F -- [ ] Support Markdown rendering - [ ] History browser ## Configuration diff --git a/bookmarks/bookmarks.go b/bookmarks/bookmarks.go index bd22600..194c357 100644 --- a/bookmarks/bookmarks.go +++ b/bookmarks/bookmarks.go @@ -19,7 +19,7 @@ func bkmkKey(url string) string { func Set(url, name string) { bkmkStore.Set(bkmkKey(url), name) - bkmkStore.WriteConfig() + bkmkStore.WriteConfig() //nolint:errcheck } // Get returns the NAME of the bookmark, given the URL. @@ -33,7 +33,7 @@ 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() + bkmkStore.WriteConfig() //nolint:errcheck } // All returns all the bookmarks in a map of URLs to names. @@ -48,9 +48,9 @@ func All() (map[string]string, []string) { return bkmks, []string{} } - inverted := make(map[string]string) // Holds inverted map, name->URL - var names []string // Holds bookmark names, for sorting - var keys []string // Final sorted keys (URLs), for returning at the end + inverted := make(map[string]string) // Holds inverted map, name->URL + names := make([]string, 0, len(bkmksMap)) // Holds bookmark names, for sorting + keys := make([]string, 0, len(bkmksMap)) // Final sorted keys (URLs), for returning at the end for b32Url, name := range bkmksMap { if n, ok := name.(string); n == "" || !ok { diff --git a/cache/cache.go b/cache/cache.go index b9e6378..fa0bc3d 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -33,7 +33,7 @@ func removeIndex(s []string, i int) []string { return s[:len(s)-1] } -func removeUrl(url string) { +func removeURL(url string) { for i := range urls { if urls[i] == url { urls = removeIndex(urls, i) @@ -48,7 +48,7 @@ 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 AddPage(p *structs.Page) { - if p.Url == "" || strings.HasPrefix(p.Url, "about:") { + if p.URL == "" || strings.HasPrefix(p.URL, "about:") { // Just in case, these pages shouldn't be cached return } @@ -71,10 +71,10 @@ func AddPage(p *structs.Page) { lock.Lock() defer lock.Unlock() - pages[p.Url] = p + pages[p.URL] = p // Remove the URL if it was already there, then add it to the end - removeUrl(p.Url) - urls = append(urls, p.Url) + removeURL(p.URL) + urls = append(urls, p.URL) } // RemovePage will remove a page from the cache. @@ -83,7 +83,7 @@ func RemovePage(url string) { lock.Lock() defer lock.Unlock() delete(pages, url) - removeUrl(url) + removeURL(url) } // ClearPages removes all pages from the cache. diff --git a/cache/cache_test.go b/cache/cache_test.go index 98cf480..7fc4d38 100644 --- a/cache/cache_test.go +++ b/cache/cache_test.go @@ -7,9 +7,8 @@ import ( "github.com/stretchr/testify/assert" ) -var p = structs.Page{Url: "example.com"} -var p2 = structs.Page{Url: "example.org"} -var queryPage = structs.Page{Url: "gemini://example.com/test?query"} +var p = structs.Page{URL: "example.com"} +var p2 = structs.Page{URL: "example.org"} func reset() { ClearPages() @@ -33,13 +32,13 @@ func TestMaxSize(t *testing.T) { assert.Equal(1, NumPages(), "one page should be added") AddPage(&p2) assert.Equal(1, NumPages(), "there should still be just one page due to cache size limits") - assert.Equal(p2.Url, urls[0], "the only page url should be the second page one") + assert.Equal(p2.URL, urls[0], "the only page url should be the second page one") } func TestRemove(t *testing.T) { reset() AddPage(&p) - RemovePage(p.Url) + RemovePage(p.URL) assert.Equal(t, 0, NumPages(), "there shouldn't be any pages after the removal") } @@ -62,11 +61,11 @@ func TestGet(t *testing.T) { reset() AddPage(&p) AddPage(&p2) - page, ok := GetPage(p.Url) + page, ok := GetPage(p.URL) if !ok { t.Fatal("Get should say that the page was found") } - if page.Url != p.Url { + if page.URL != p.URL { t.Error("page urls don't match") } } diff --git a/client/client.go b/client/client.go index e9a4fdf..eba7205 100644 --- a/client/client.go +++ b/client/client.go @@ -5,12 +5,20 @@ import ( "net/url" "github.com/makeworld-the-better-one/go-gemini" + "github.com/spf13/viper" ) // Fetch returns response data and an error. // The error text is human friendly and should be displayed. func Fetch(u string) (*gemini.Response, error) { - res, err := gemini.Fetch(u) + var res *gemini.Response + var err error + + if viper.GetString("a-general.proxy") == "" { + res, err = gemini.Fetch(u) + } else { + res, err = gemini.FetchWithHost(viper.GetString("a-general.proxy"), u) + } if err != nil { return nil, err } diff --git a/client/tofu.go b/client/tofu.go index 9fd2d1a..7af273e 100644 --- a/client/tofu.go +++ b/client/tofu.go @@ -39,19 +39,20 @@ func expiryKey(domain string, port string) string { func loadTofuEntry(domain string, port string) (string, time.Time, error) { id := tofuStore.GetString(idKey(domain, port)) // Fingerprint - if len(id) != 64 { + if len(id) != sha256.Size*2 { // Not set, or invalid - return "", time.Time{}, errors.New("not found") + return "", time.Time{}, errors.New("not found") //nolint:goerr113 } expiry := tofuStore.GetTime(expiryKey(domain, port)) if expiry.IsZero() { // Not set - return id, time.Time{}, errors.New("not found") + return id, time.Time{}, errors.New("not found") //nolint:goerr113 } return id, expiry, nil } +//nolint:errcheck // certID returns a generic string representing a cert or domain. func certID(cert *x509.Certificate) string { h := sha256.New() @@ -62,14 +63,14 @@ func certID(cert *x509.Certificate) string { // origCertID uses cert.Raw, which was used in v1.0.0 of the app. func origCertID(cert *x509.Certificate) string { h := sha256.New() - h.Write(cert.Raw) + h.Write(cert.Raw) //nolint:errcheck return fmt.Sprintf("%X", h.Sum(nil)) } func saveTofuEntry(domain, port string, cert *x509.Certificate) { tofuStore.Set(idKey(domain, port), certID(cert)) tofuStore.Set(expiryKey(domain, port), cert.NotAfter.UTC()) - tofuStore.WriteConfig() + tofuStore.WriteConfig() //nolint:errcheck // Not an issue if it's not saved, only cached data } // handleTofu is the abstracted interface for taking care of TOFU. @@ -90,7 +91,7 @@ func handleTofu(domain, port string, cert *x509.Certificate) bool { // Store expiry again in case it changed tofuStore.Set(expiryKey(domain, port), cert.NotAfter.UTC()) - tofuStore.WriteConfig() + tofuStore.WriteConfig() //nolint:errcheck return true } diff --git a/config/config.go b/config/config.go index 7d6cf8a..9917e09 100644 --- a/config/config.go +++ b/config/config.go @@ -27,11 +27,11 @@ var tofuDBDir string var tofuDBPath string // Bookmarks + var BkmkStore = viper.New() var bkmkDir string var bkmkPath string -// For other pkgs to use var DownloadsDir string // Feeds @@ -39,6 +39,7 @@ var FeedJson io.ReadCloser var feedDir string var FeedPath string +//nolint:golint,goerr113 func Init() error { // *** Set paths *** @@ -48,7 +49,7 @@ func Init() error { return err } // Store AppData path - if runtime.GOOS == "windows" { + if runtime.GOOS == "windows" { //nolint:goconst appdata, ok := os.LookupEnv("APPDATA") if ok { amforaAppData = filepath.Join(appdata, "amfora") @@ -220,6 +221,7 @@ func Init() error { // Setup main config viper.SetDefault("a-general.home", "gemini.circumlunar.space") + viper.SetDefault("a-general.auto_redirect", false) viper.SetDefault("a-general.http", "default") viper.SetDefault("a-general.search", "gus.guru/search") viper.SetDefault("a-general.color", true) @@ -231,6 +233,7 @@ func Init() error { viper.SetDefault("a-general.page_max_time", 10) viper.SetDefault("a-general.emoji_favicons", false) viper.SetDefault("keybindings.shift_numbers", "!@#$%^&*()") + viper.SetDefault("url-handlers.other", "off") viper.SetDefault("cache.max_size", 0) viper.SetDefault("cache.max_pages", 20) diff --git a/config/default.go b/config/default.go index b191984..8f68590 100644 --- a/config/default.go +++ b/config/default.go @@ -16,6 +16,11 @@ var defaultConf = []byte(`# This is the default config file. # Press Ctrl-H to access it home = "gemini://gemini.circumlunar.space" +# Follow up to 5 Gemini redirects without prompting. +# A prompt is always shown after the 5th redirect and for redirects to protocols other than Gemini. +# If set to false, a prompt will be shown before following redirects. +auto_redirect = false + # What command to run to open a HTTP URL. Set to "default" to try to guess the browser, # or set to "off" to not open HTTP URLs. # If a command is set, than the URL will be added (in quotes) to the end of the command. @@ -50,6 +55,13 @@ page_max_time = 10 # Whether to replace tab numbers with emoji favicons, which are cached. emoji_favicons = false +# Proxy server, through which all requests would be sent. +# String should be a host: a domain/IP with an optional port. Port 1965 is assumed otherwise. +# The proxy server needs to be a Gemini server that supports proxying. +# By default it is empty, which disables the proxy. +proxy = "" + + [keybindings] # In the future there will be more settings here. @@ -58,6 +70,20 @@ emoji_favicons = false shift_numbers = "!@#$%^&*()" +[url-handlers] +# Allows setting the commands to run for various URL schemes. +# E.g. to open FTP URLs with FileZilla set the following key: +# ftp = "filezilla" +# You can set any scheme to "off" to disable handling it. +# +# DO NOT use this for setting the HTTP command. +# Use the http setting in the "a-general" section above + +# This is a special key that defines the handler for all URL schemes for which +# no handler is defined. +other = "off" + + [cache] # Options for page cache - which is only for text/gemini pages # Increase the cache size to speed up browsing at the expense of memory @@ -135,4 +161,5 @@ max_pages = 30 # The maximum number of pages the cache will store # bkmk_modal_text # bkmk_modal_label # bkmk_modal_field_bg -# bkmk_modal_field_text`) +# bkmk_modal_field_text +`) diff --git a/config/keybindings.go b/config/keybindings.go index ccc7310..e3cd06e 100644 --- a/config/keybindings.go +++ b/config/keybindings.go @@ -20,5 +20,5 @@ func KeyToNum(key rune) (int, error) { return i + 1, nil } } - return -1, errors.New("provided key is invalid") + return -1, errors.New("provided key is invalid") //nolint:goerr113 } diff --git a/default-config.toml b/default-config.toml index 3dff68d..272f381 100644 --- a/default-config.toml +++ b/default-config.toml @@ -13,6 +13,11 @@ # Press Ctrl-H to access it home = "gemini://gemini.circumlunar.space" +# Follow up to 5 Gemini redirects without prompting. +# A prompt is always shown after the 5th redirect and for redirects to protocols other than Gemini. +# If set to false, a prompt will be shown before following redirects. +auto_redirect = false + # What command to run to open a HTTP URL. Set to "default" to try to guess the browser, # or set to "off" to not open HTTP URLs. # If a command is set, than the URL will be added (in quotes) to the end of the command. @@ -47,6 +52,13 @@ page_max_time = 10 # Whether to replace tab numbers with emoji favicons, which are cached. emoji_favicons = false +# Proxy server, through which all requests would be sent. +# String should be a host: a domain/IP with an optional port. Port 1965 is assumed otherwise. +# The proxy server needs to be a Gemini server that supports proxying. +# By default it is empty, which disables the proxy. +proxy = "" + + [keybindings] # In the future there will be more settings here. @@ -55,6 +67,20 @@ emoji_favicons = false shift_numbers = "!@#$%^&*()" +[url-handlers] +# Allows setting the commands to run for various URL schemes. +# E.g. to open FTP URLs with FileZilla set the following key: +# ftp = "filezilla" +# You can set any scheme to "off" to disable handling it. +# +# DO NOT use this for setting the HTTP command. +# Use the http setting in the "a-general" section above + +# This is a special key that defines the handler for all URL schemes for which +# no handler is defined. +other = "off" + + [cache] # Options for page cache - which is only for text/gemini pages # Increase the cache size to speed up browsing at the expense of memory @@ -132,4 +158,4 @@ max_pages = 30 # The maximum number of pages the cache will store # bkmk_modal_text # bkmk_modal_label # bkmk_modal_field_bg -# bkmk_modal_field_text \ No newline at end of file +# bkmk_modal_field_text diff --git a/display/bookmarks.go b/display/bookmarks.go index 5b36e2a..1185362 100644 --- a/display/bookmarks.go +++ b/display/bookmarks.go @@ -121,7 +121,7 @@ func Bookmarks(t *tab) { Raw: bkmkPageRaw, Content: content, Links: links, - Url: "about:bookmarks", + URL: "about:bookmarks", Width: termW, Mediatype: structs.TextGemini, } @@ -133,20 +133,20 @@ func Bookmarks(t *tab) { // 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(tabs[curTab].page.Url, "gemini://") { + if !strings.HasPrefix(tabs[curTab].page.URL, "gemini://") { // Can't make bookmarks for other kinds of URLs return } - name, exists := bookmarks.Get(tabs[curTab].page.Url) + name, exists := bookmarks.Get(tabs[curTab].page.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(tabs[curTab].page.Url, newName) + bookmarks.Set(tabs[curTab].page.URL, newName) case -1: - bookmarks.Remove(tabs[curTab].page.Url) + bookmarks.Remove(tabs[curTab].page.URL) } // Other case is action = 0, meaning "Cancel", so nothing needs to happen } diff --git a/display/display.go b/display/display.go index d48220a..2aa331c 100644 --- a/display/display.go +++ b/display/display.go @@ -3,7 +3,6 @@ package display import ( "fmt" "net/url" - "path" "strconv" "strings" @@ -12,6 +11,7 @@ import ( "github.com/makeworld-the-better-one/amfora/config" "github.com/makeworld-the-better-one/amfora/renderer" "github.com/makeworld-the-better-one/amfora/structs" + "github.com/makeworld-the-better-one/go-gemini" "github.com/spf13/viper" "gitlab.com/tslocum/cview" ) @@ -115,6 +115,7 @@ func Init() { App.SetFocus(tabs[tab].view) } + //nolint:exhaustive switch key { case tcell.KeyEnter: // Figure out whether it's a URL, link number, or search @@ -127,27 +128,19 @@ func Init() { reset() return } - if query == ".." && tabs[tab].hasContent() { - // Go up a directory - parsed, err := url.Parse(tabs[tab].page.Url) + if query[0] == '.' && tabs[tab].hasContent() { + // Relative url + current, err := url.Parse(tabs[tab].page.URL) if err != nil { // This shouldn't occur return } - if parsed.Path == "/" { - // Can't go up further - reset() + target, err := current.Parse(query) + if err != nil { + // Invalid relative url return } - - // Ex: /test/foo/ -> /test/foo//.. -> /test -> /test/ - parsed.Path = path.Clean(parsed.Path+"/..") + "/" - if parsed.Path == "//" { - // Fix double slash that occurs at domain root - parsed.Path = "/" - } - parsed.RawQuery = "" // Remove query - URL(parsed.String()) + URL(target.String()) return } @@ -165,7 +158,7 @@ func Init() { oldTab := tab NewTab() // Resolve and follow link manually - prevParsed, _ := url.Parse(tabs[oldTab].page.Url) + prevParsed, _ := url.Parse(tabs[oldTab].page.URL) nextParsed, err := url.Parse(tabs[oldTab].page.Links[i-1]) if err != nil { Error("URL Error", "link URL could not be parsed") @@ -178,8 +171,9 @@ func Init() { } else { // 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, ".") && !strings.HasPrefix(query, "about:")) { - u := viper.GetString("a-general.search") + "?" + queryEscape(query) + if strings.Contains(query, " ") || + (!strings.Contains(query, "//") && !strings.Contains(query, ".") && !strings.HasPrefix(query, "about:")) { + u := viper.GetString("a-general.search") + "?" + gemini.QueryEscape(query) cache.RemovePage(u) // Don't use the cached version of the search URL(u) } else { @@ -192,7 +186,7 @@ func Init() { } if i <= len(tabs[tab].page.Links) && i > 0 { // It's a valid link number - followLink(tabs[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 } // Invalid link number, don't do anything @@ -213,7 +207,7 @@ func Init() { Raw: newTabContent, Content: renderedNewTabContent, Links: newTabLinks, - Url: "about:newtab", + URL: "about:newtab", Width: -1, // Force reformatting on first display Mediatype: structs.TextGemini, } @@ -254,6 +248,7 @@ func Init() { } } + //nolint:exhaustive switch event.Key() { case tcell.KeyCtrlR: Reload() @@ -321,18 +316,20 @@ func Init() { } if i <= len(tabs[curTab].page.Links) && i > 0 { // It's a valid link number - followLink(tabs[curTab], tabs[curTab].page.Url, tabs[curTab].page.Links[i-1]) + followLink(tabs[curTab], tabs[curTab].page.URL, tabs[curTab].page.Links[i-1]) return nil } } } } + // All the keys and operations that can work while a tab IS loading + //nolint:exhaustive switch event.Key() { case tcell.KeyCtrlT: if tabs[curTab].page.Mode == structs.ModeLinkSelect { - next, err := resolveRelLink(tabs[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 { Error("URL Error", err.Error()) return nil @@ -521,11 +518,11 @@ func Reload() { return } - parsed, _ := url.Parse(tabs[curTab].page.Url) + parsed, _ := url.Parse(tabs[curTab].page.URL) go func(t *tab) { - cache.RemovePage(tabs[curTab].page.Url) + cache.RemovePage(tabs[curTab].page.URL) cache.RemoveFavicon(parsed.Host) - handleURL(t, t.page.Url) // goURL is not used bc history shouldn't be added to + handleURL(t, t.page.URL, 0) // goURL is not used bc history shouldn't be added to if t == tabs[curTab] { // Display the bottomBar state that handleURL set t.applyBottomBar() @@ -538,7 +535,7 @@ func Reload() { func URL(u string) { // Some code is copied in followLink() - if u == "about:bookmarks" { + if u == "about:bookmarks" { //nolint:goconst Bookmarks(tabs[curTab]) tabs[curTab].addToHistory("about:bookmarks") return @@ -553,6 +550,10 @@ func URL(u string) { return } + if !strings.HasPrefix(u, "//") && !strings.HasPrefix(u, "gemini://") && !strings.Contains(u, "://") { + // Assume it's a Gemini URL + u = "gemini://" + u + } go goURL(tabs[curTab], u) } diff --git a/display/download.go b/display/download.go index 2f2f783..a150166 100644 --- a/display/download.go +++ b/display/download.go @@ -142,7 +142,7 @@ func downloadURL(u string, resp *gemini.Response) { progressbar.OptionShowCount(), progressbar.OptionSpinnerType(14), ) - bar.RenderBlank() + bar.RenderBlank() //nolint:errcheck savePath, err := downloadNameFromURL(u, "") if err != nil { @@ -200,9 +200,9 @@ func downloadPage(p *structs.Page) (string, error) { var err error if p.Mediatype == structs.TextGemini { - savePath, err = downloadNameFromURL(p.Url, ".gmi") + savePath, err = downloadNameFromURL(p.URL, ".gmi") } else { - savePath, err = downloadNameFromURL(p.Url, ".txt") + savePath, err = downloadNameFromURL(p.URL, ".txt") } if err != nil { return "", err @@ -261,13 +261,12 @@ func getSafeDownloadName(name string, lastDot bool, n int) (string, error) { if lastDot { ext := filepath.Ext(name) return strings.TrimSuffix(name, ext) + "(" + strconv.Itoa(n) + ")" + ext - } else { - idx := strings.Index(name, ".") - if idx == -1 { - return name + "(" + strconv.Itoa(n) + ")" - } - return name[:idx] + "(" + strconv.Itoa(n) + ")" + name[idx:] } + idx := strings.Index(name, ".") + if idx == -1 { + return name + "(" + strconv.Itoa(n) + ")" + } + return name[:idx] + "(" + strconv.Itoa(n) + ")" + name[idx:] } d, err := os.Open(config.DownloadsDir) diff --git a/display/feeds.go b/display/feeds.go index 9d19389..4f230da 100644 --- a/display/feeds.go +++ b/display/feeds.go @@ -38,7 +38,7 @@ func Feeds(t *tab) { Raw: feedPageRaw, Content: content, Links: links, - Url: "about:feeds", + URL: "about:feeds", Width: termW, Mediatype: structs.TextGemini, } diff --git a/display/history.go b/display/history.go index df60641..333adcc 100644 --- a/display/history.go +++ b/display/history.go @@ -2,7 +2,7 @@ package display // applyHist is a history.go internal function, to load a URL in the history. func applyHist(t *tab) { - handleURL(t, t.history.urls[t.history.pos]) // Load that position in history + handleURL(t, t.history.urls[t.history.pos], 0) // Load that position in history t.applyAll() } diff --git a/display/modals.go b/display/modals.go index 3353a68..5a472d9 100644 --- a/display/modals.go +++ b/display/modals.go @@ -6,7 +6,7 @@ import ( "strings" "time" - "github.com/dustin/go-humanize" + humanize "github.com/dustin/go-humanize" "github.com/gdamore/tcell" "github.com/makeworld-the-better-one/amfora/config" "github.com/spf13/viper" @@ -154,10 +154,13 @@ func modalInit() { // Error displays an error on the screen in a modal. func Error(title, text string) { - // Capitalize and add period if necessary - because most errors don't do that - text = strings.ToUpper(string([]rune(text)[0])) + text[1:] - if !strings.HasSuffix(text, ".") && !strings.HasSuffix(text, "!") && !strings.HasSuffix(text, "?") { - text += "." + if text == "" { + text = "No additional information." + } else { + text = strings.ToUpper(string([]rune(text)[0])) + text[1:] + if !strings.HasSuffix(text, ".") && !strings.HasSuffix(text, "!") && !strings.HasSuffix(text, "?") { + text += "." + } } // Add spaces to title for aesthetic reasons title = " " + strings.TrimSpace(title) + " " @@ -265,6 +268,7 @@ func Tofu(host string, expiry time.Time) bool { } yesNoModal.GetFrame().SetTitle(" TOFU ") yesNoModal.SetText( + //nolint:lll fmt.Sprintf("%s's certificate has changed, possibly indicating an security issue. The certificate would have expired %s. Are you sure you want to continue? ", host, humanize.Time(expiry), diff --git a/display/newtab.go b/display/newtab.go index ee451bd..987f0f6 100644 --- a/display/newtab.go +++ b/display/newtab.go @@ -1,3 +1,4 @@ +//nolint package display var newTabContent = `# New Tab diff --git a/display/private.go b/display/private.go index 355e56c..7795c6f 100644 --- a/display/private.go +++ b/display/private.go @@ -2,6 +2,7 @@ package display import ( "bytes" + "errors" "fmt" "io" "net/url" @@ -70,15 +71,18 @@ func reformatPage(p *structs.Page) { return } + // TODO: Setup a renderer.RenderFromMediatype func so this isn't needed + var rendered string - if p.Mediatype == structs.TextGemini { + switch p.Mediatype { + case structs.TextGemini: // Links are not recorded because they won't change rendered, _ = renderer.RenderGemini(p.Raw, textWidth(), leftMargin()) - } else if p.Mediatype == structs.TextPlain { + case structs.TextPlain: rendered = renderer.RenderPlainText(p.Raw, leftMargin()) - } else if p.Mediatype == structs.TextAnsi { + case structs.TextAnsi: rendered = renderer.RenderANSI(p.Raw, leftMargin()) - } else { + default: // Rendering this type is not implemented return } @@ -117,7 +121,7 @@ func setPage(t *tab, p *structs.Page) { t.page = p go func() { - parsed, _ := url.Parse(p.Url) + parsed, _ := url.Parse(p.URL) handleFavicon(t, parsed.Host, oldFav) }() @@ -131,7 +135,7 @@ func setPage(t *tab, p *structs.Page) { // Save bottom bar for the tab - other funcs will apply/display it t.barLabel = "" - t.barText = p.Url + t.barText = p.URL } // handleHTTP is used by handleURL. @@ -158,6 +162,30 @@ func handleHTTP(u string, showInfo bool) { App.Draw() } +// handleOther is used by handleURL. +// It opens links other than Gemini and HTTP and displays Error modals. +func handleOther(u string) { + // The URL should have a scheme due to a previous call to normalizeURL + parsed, _ := url.Parse(u) + // Search for a handler for the URL scheme + handler := strings.TrimSpace(viper.GetString("url-handlers." + parsed.Scheme)) + if len(handler) == 0 { + handler = strings.TrimSpace(viper.GetString("url-handlers.other")) + } + switch handler { + case "", "off": + Error("URL Error", "Opening "+parsed.Scheme+" URLs is turned off.") + default: + // The config has a custom command to execute for URLs + fields := strings.Fields(handler) + err := exec.Command(fields[0], append(fields[1:], u)...).Start() + if err != nil { + Error("URL Error", "Error executing custom command: "+err.Error()) + } + } + App.Draw() +} + // handleFavicon handles getting and displaying a favicon. // `old` is the previous favicon for the tab. func handleFavicon(t *tab, host, old string) { @@ -239,7 +267,7 @@ func handleFavicon(t *tab, host, old string) { // // It should be called in a goroutine. func goURL(t *tab, u string) { - final, displayed := handleURL(t, u) + final, displayed := handleURL(t, u, 0) if displayed { t.addToHistory(final) } @@ -261,7 +289,10 @@ func goURL(t *tab, u string) { // // The bottomBar is not actually changed in this func, except during loading. // The func that calls this one should apply the bottomBar values if necessary. -func handleURL(t *tab, u string) (string, bool) { +// +// numRedirects is the number of redirects that resulted in the provided URL. +// It should typically be 0. +func handleURL(t *tab, u string, numRedirects int) (string, bool) { defer App.Draw() // Just in case // Save for resetting on error @@ -304,7 +335,7 @@ func handleURL(t *tab, u string) (string, bool) { return ret("", false) } if !strings.HasPrefix(u, "gemini") { - Error("Protocol Error", "Only gemini and HTTP are supported. URL was "+u) + handleOther(u) return ret("", false) } // Gemini URL @@ -328,7 +359,7 @@ func handleURL(t *tab, u string) (string, bool) { return ret("", false) } - if err == client.ErrTofu { + if errors.Is(err, client.ErrTofu) { if Tofu(parsed.Host, client.GetExpiry(parsed.Hostname(), parsed.Port())) { // They want to continue anyway client.ResetTofuEntry(parsed.Hostname(), parsed.Port(), res.Cert) @@ -348,20 +379,20 @@ func handleURL(t *tab, u string) (string, bool) { return ret("", false) } - if err == renderer.ErrTooLarge { + if errors.Is(err, renderer.ErrTooLarge) { // Make new request for downloading purposes res, clientErr := client.Fetch(u) - if clientErr != nil && clientErr != client.ErrTofu { + if clientErr != nil && !errors.Is(clientErr, client.ErrTofu) { Error("URL Fetch Error", err.Error()) return ret("", false) } go dlChoice("That page is too large. What would you like to do?", u, res) return ret("", false) } - if err == renderer.ErrTimedOut { + if errors.Is(err, renderer.ErrTimedOut) { // Make new request for downloading purposes res, clientErr := client.Fetch(u) - if clientErr != nil && clientErr != client.ErrTofu { + if clientErr != nil && !errors.Is(clientErr, client.ErrTofu) { Error("URL Fetch Error", err.Error()) return ret("", false) } @@ -388,13 +419,12 @@ func handleURL(t *tab, u string) (string, bool) { if ok { // Make another request with the query string added // + chars are replaced because PathEscape doesn't do that - parsed.RawQuery = queryEscape(userInput) - if len(parsed.String()) > 1024 { - // 1024 is the max size for URLs in the spec + parsed.RawQuery = gemini.QueryEscape(userInput) + if len(parsed.String()) > gemini.URLMaxLength { Error("Input Error", "URL for that input would be too long.") return ret("", false) } - return ret(handleURL(t, parsed.String())) + return ret(handleURL(t, parsed.String(), 0)) } return ret("", false) case 30: @@ -404,11 +434,22 @@ func handleURL(t *tab, u string) (string, bool) { return ret("", false) } redir := parsed.ResolveReference(parsedMeta).String() - if YesNo("Follow redirect?\n" + redir) { + // Prompt before redirecting to non-Gemini protocol + redirect := false + if !strings.HasPrefix(redir, "gemini") { + if YesNo("Follow redirect to non-Gemini URL?\n" + redir) { + redirect = true + } else { + return ret("", false) + } + } + // Prompt before redirecting + autoRedirect := viper.GetBool("a-general.auto_redirect") + if redirect || (autoRedirect && numRedirects < 5) || YesNo("Follow redirect?\n"+redir) { if res.Status == gemini.StatusRedirectPermanent { go cache.AddRedir(u, redir) } - return ret(handleURL(t, redir)) + return ret(handleURL(t, redir, numRedirects+1)) } return ret("", false) case 40: diff --git a/display/tab.go b/display/tab.go index 2848cf0..b8982aa 100644 --- a/display/tab.go +++ b/display/tab.go @@ -62,13 +62,13 @@ func makeNewTab() *tab { if key == tcell.KeyEsc { // Stop highlighting bottomBar.SetLabel("") - bottomBar.SetText(tabs[tab].page.Url) + bottomBar.SetText(tabs[tab].page.URL) tabs[tab].clearSelected() tabs[tab].saveBottomBar() return } - if len(tabs[tab].page.Links) <= 0 { + if len(tabs[tab].page.Links) == 0 { // No links on page return } @@ -82,10 +82,10 @@ func makeNewTab() *tab { linkN, _ := strconv.Atoi(currentSelection[0]) tabs[tab].page.Selected = tabs[tab].page.Links[linkN] tabs[tab].page.SelectedID = currentSelection[0] - followLink(tabs[tab], tabs[tab].page.Url, tabs[tab].page.Links[linkN]) + followLink(tabs[tab], tabs[tab].page.URL, tabs[tab].page.Links[linkN]) return } - if len(currentSelection) <= 0 && (key == tcell.KeyEnter || key == tcell.KeyTab) { + if len(currentSelection) == 0 && (key == tcell.KeyEnter || key == tcell.KeyTab) { // They've started link highlighting tabs[tab].page.Mode = structs.ModeLinkSelect @@ -102,7 +102,7 @@ func makeNewTab() *tab { // There's still a selection, but a different key was pressed, not Enter index, _ := strconv.Atoi(currentSelection[0]) - if key == tcell.KeyTab { + if key == tcell.KeyTab { //nolint:gocritic index = (index + 1) % numSelections } else if key == tcell.KeyBacktab { index = (index - 1 + numSelections) % numSelections @@ -153,10 +153,10 @@ func (t *tab) hasContent() bool { if t.page == nil || t.view == nil { return false } - if t.page.Url == "" { + if t.page.URL == "" { return false } - if strings.HasPrefix(t.page.Url, "about:") { + if strings.HasPrefix(t.page.URL, "about:") { return false } if t.page.Content == "" { diff --git a/display/util.go b/display/util.go index 37b6f1d..e03fdd6 100644 --- a/display/util.go +++ b/display/util.go @@ -3,7 +3,6 @@ package display import ( "errors" "net/url" - "strings" "github.com/spf13/viper" ) @@ -45,12 +44,6 @@ func textWidth() int { return viper.GetInt("a-general.max_width") } -// queryEscape is the same as url.PathEscape, but it also replaces the +. -// This is because Gemini requires percent-escaping for queries. -func queryEscape(query string) string { - return strings.ReplaceAll(url.PathEscape(query), "+", "%2B") -} - // 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 // to the user. @@ -62,7 +55,7 @@ func resolveRelLink(t *tab, prev, next string) (string, error) { prevParsed, _ := url.Parse(prev) nextParsed, err := url.Parse(next) if err != nil { - return "", errors.New("link URL could not be parsed") + return "", errors.New("link URL could not be parsed") //nolint:goerr113 } return prevParsed.ResolveReference(nextParsed).String(), nil } @@ -83,13 +76,6 @@ func normalizeURL(u string) string { return u } - if !strings.Contains(u, "://") && !strings.HasPrefix(u, "//") { - // No scheme at all in the URL - parsed, err = url.Parse("gemini://" + u) - if err != nil { - return u - } - } if parsed.Scheme == "" { // Always add scheme parsed.Scheme = "gemini" diff --git a/display/util_test.go b/display/util_test.go new file mode 100644 index 0000000..4cc3506 --- /dev/null +++ b/display/util_test.go @@ -0,0 +1,33 @@ +package display + +import ( + "testing" +) + +var normalizeURLTests = []struct { + u string + expected string +}{ + {"gemini://example.com:1965/", "gemini://example.com/"}, + {"gemini://example.com", "gemini://example.com/"}, + {"//example.com", "gemini://example.com/"}, + {"//example.com:1965", "gemini://example.com/"}, + {"//example.com:123/", "gemini://example.com:123/"}, + {"gemini://example.com/", "gemini://example.com/"}, + {"gemini://example.com/#fragment", "gemini://example.com/"}, + {"gemini://example.com#fragment", "gemini://example.com/"}, + {"gemini://user@example.com/", "gemini://example.com/"}, + // Other schemes, URL isn't modified + {"mailto:example@example.com", "mailto:example@example.com"}, + {"magnet:?xt=urn:btih:test", "magnet:?xt=urn:btih:test"}, + {"https://example.com", "https://example.com"}, +} + +func TestNormalizeURL(t *testing.T) { + for _, tt := range normalizeURLTests { + actual := normalizeURL(tt.u) + if actual != tt.expected { + t.Errorf("normalizeURL(%s): expected %s, actual %s", tt.u, tt.expected, actual) + } + } +} diff --git a/go.mod b/go.mod index 9b59051..2f77ed8 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,8 @@ require ( github.com/dustin/go-humanize v1.0.0 github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/gdamore/tcell v1.3.1-0.20200608133353-cb1e5d6fa606 - github.com/makeworld-the-better-one/go-gemini v0.7.0 + github.com/google/go-cmp v0.5.0 // indirect + github.com/makeworld-the-better-one/go-gemini v0.8.4 github.com/makeworld-the-better-one/go-isemoji v1.0.0 github.com/makeworld-the-better-one/progressbar/v3 v3.3.5-0.20200710151429-125743e22b4f github.com/mitchellh/go-homedir v1.1.0 @@ -20,7 +21,8 @@ require ( github.com/spf13/viper v1.7.0 github.com/stretchr/testify v1.6.1 gitlab.com/tslocum/cview v1.4.8-0.20200713214710-cc7796c4ca44 - golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1 // indirect + golang.org/x/sys v0.0.0-20200817155316-9781c653f443 // indirect golang.org/x/text v0.3.3 + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/ini.v1 v1.57.0 // indirect ) diff --git a/go.sum b/go.sum index e188571..129a600 100644 --- a/go.sum +++ b/go.sum @@ -73,8 +73,9 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -129,8 +130,8 @@ github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tW github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/makeworld-the-better-one/go-gemini v0.7.0 h1:TCerE47eYHLXj6RQDjfd5HdGVbcVqpBC6OoPBlyY7q4= -github.com/makeworld-the-better-one/go-gemini v0.7.0/go.mod h1:P7/FbZ+IEIbA/d+A0Y3w2GNgD8SA2AcNv7aDGJbaWG4= +github.com/makeworld-the-better-one/go-gemini v0.8.4 h1:ntsQ9HnlJCmC9PDqXp/f1SCALjBMwh69BbT4BhFRFaw= +github.com/makeworld-the-better-one/go-gemini v0.8.4/go.mod h1:P7/FbZ+IEIbA/d+A0Y3w2GNgD8SA2AcNv7aDGJbaWG4= github.com/makeworld-the-better-one/go-isemoji v1.0.0 h1:W3O4+qwtXeT8PUDzcQ1UjxiupQWgc/oJHpqwrllx3xM= github.com/makeworld-the-better-one/go-isemoji v1.0.0/go.mod h1:FBjkPl9rr0G4vlZCc+Mr+QcnOfGCTbGWYW8/1sp06I0= github.com/makeworld-the-better-one/progressbar/v3 v3.3.5-0.20200710151429-125743e22b4f h1:YEUlTs5gb35UlBLTgqrub9axWTYB3d7/8TxrkJDZpRI= @@ -294,10 +295,9 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w 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= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200610111108-226ff32320da h1:bGb80FudwxpeucJUjPYJXuJ8Hk91vNtfvrymzwiei38= golang.org/x/sys v0.0.0-20200610111108-226ff32320da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1 h1:sIky/MyNRSHTrdxfsiUSS4WIAMvInbeXljJz+jDjeYE= -golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200817155316-9781c653f443 h1:X18bCaipMcoJGm27Nv7zr4XYPKGUy92GtqboKC2Hxaw= +golang.org/x/sys v0.0.0-20200817155316-9781c653f443/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/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -323,6 +323,8 @@ golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -346,8 +348,9 @@ google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiq 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/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/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww= diff --git a/renderer/page.go b/renderer/page.go index 8ae5277..8159ac9 100644 --- a/renderer/page.go +++ b/renderer/page.go @@ -16,6 +16,9 @@ import ( var ErrTooLarge = errors.New("page content would be too large") var ErrTimedOut = errors.New("page download timed out") +var ErrCantDisplay = errors.New("invalid content for a page") +var ErrBadEncoding = errors.New("unsupported encoding") +var ErrBadMediatype = errors.New("displayable mediatype is not handled in the code, implementation error") // isUTF8 returns true for charsets that are compatible with UTF-8 and don't need to be decoded. func isUTF8(charset string) bool { @@ -56,7 +59,7 @@ func CanDisplay(res *gemini.Response) bool { // You must set the Page.Width value yourself. func MakePage(url string, res *gemini.Response, width, leftMargin int) (*structs.Page, error) { if !CanDisplay(res) { - return nil, errors.New("not valid content for a Page") + return nil, ErrCantDisplay } buf := new(bytes.Buffer) @@ -90,7 +93,7 @@ func MakePage(url string, res *gemini.Response, width, leftMargin int) (*structs encoding, err := ianaindex.MIME.Encoding(params["charset"]) if encoding == nil || err != nil { // Some encoding doesn't exist and wasn't caught in CanDisplay() - return nil, errors.New("unsupported encoding") + return nil, ErrBadEncoding } utfText, err = encoding.NewDecoder().String(buf.String()) if err != nil { @@ -102,7 +105,7 @@ func MakePage(url string, res *gemini.Response, width, leftMargin int) (*structs rendered, links := RenderGemini(utfText, width, leftMargin) return &structs.Page{ Mediatype: structs.TextGemini, - Url: url, + URL: url, Raw: utfText, Content: rendered, Links: links, @@ -112,22 +115,22 @@ func MakePage(url string, res *gemini.Response, width, leftMargin int) (*structs // ANSI return &structs.Page{ Mediatype: structs.TextAnsi, - Url: url, + URL: url, Raw: utfText, Content: RenderANSI(utfText, leftMargin), Links: []string{}, }, nil - } else { - // Treated as plaintext - return &structs.Page{ - Mediatype: structs.TextPlain, - Url: url, - Raw: utfText, - Content: RenderPlainText(utfText, leftMargin), - Links: []string{}, - }, nil } + + // Treated as plaintext + return &structs.Page{ + Mediatype: structs.TextPlain, + URL: url, + Raw: utfText, + Content: RenderPlainText(utfText, leftMargin), + Links: []string{}, + }, nil } - return nil, errors.New("displayable mediatype is not handled in the code, implementation error") + return nil, ErrBadMediatype } diff --git a/renderer/renderer.go b/renderer/renderer.go index 4046553..05a17b1 100644 --- a/renderer/renderer.go +++ b/renderer/renderer.go @@ -101,11 +101,11 @@ func convertRegularGemini(s string, numLinks, width int) (string, []string) { for i := range lines { lines[i] = strings.TrimRight(lines[i], " \r\t\n") - if strings.HasPrefix(lines[i], "#") { + if strings.HasPrefix(lines[i], "#") { //nolint:gocritic // Headings var tag string if viper.GetBool("a-general.color") { - if strings.HasPrefix(lines[i], "###") { + if strings.HasPrefix(lines[i], "###") { //nolint:gocritic tag = fmt.Sprintf("[%s::b]", config.GetColorString("hdg_3")) } else if strings.HasPrefix(lines[i], "##") { tag = fmt.Sprintf("[%s::b]", config.GetColorString("hdg_2")) diff --git a/structs/structs.go b/structs/structs.go index a98c479..4dd194c 100644 --- a/structs/structs.go +++ b/structs/structs.go @@ -18,14 +18,14 @@ const ( // Page is for storing UTF-8 text/gemini pages, as well as text/plain pages. type Page struct { - Url string + URL string Mediatype Mediatype Raw string // The raw response, as received over the network - Content string // The processed content, NOT raw. Uses cview color tags. All link/link texts must have region 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. Links []string // URLs, for each region in the content. Row int // Scroll position Column int // ditto - Width int // The width of the terminal at the time when the Content was set. This is to know when reformatting should happen. + Width int // The terminal width when the Content was set, to know when reformatting should happen. Selected string // The current text or link selected SelectedID string // The cview region ID for the selected text/link Mode PageMode @@ -34,7 +34,7 @@ type Page struct { // Size returns an approx. size of a Page in bytes. func (p *Page) Size() int { - n := len(p.Raw) + len(p.Content) + len(p.Url) + len(p.Selected) + len(p.SelectedID) + n := len(p.Raw) + len(p.Content) + len(p.URL) + len(p.Selected) + len(p.SelectedID) for i := range p.Links { n += len(p.Links[i]) } diff --git a/webbrowser/open_browser_unix.go b/webbrowser/open_browser_unix.go index 298abaf..99ecbb5 100644 --- a/webbrowser/open_browser_unix.go +++ b/webbrowser/open_browser_unix.go @@ -1,5 +1,6 @@ // +build linux freebsd netbsd openbsd +//nolint:goerr113 package webbrowser import (