diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 1311375..2aa1043 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,2 @@ -github: makeworld-the-better-one +github: makew0rld ko_fi: makeworld diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index cad7430..8b4f7dd 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -19,11 +19,14 @@ jobs: name: lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/setup-go@v3 + with: + go-version: 1.22 + - uses: actions/checkout@v3 - name: golangci-lint - uses: golangci/golangci-lint-action@v2 + uses: golangci/golangci-lint-action@v3 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.35 + version: v1.43 # Optional: show only new issues if it's a pull request. The default value is `false`. only-new-issues: true diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index e59692c..c22b6df 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -16,11 +16,11 @@ jobs: - name: Set up Go uses: actions/setup-go@v2 with: - go-version: 1.16 + go-version: 1.22 - name: Run GoReleaser uses: goreleaser/goreleaser-action@v2 with: version: 0.x args: release --rm-dist env: - GITHUB_TOKEN: ${{ secrets.GH_REPOS }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/homebrew.yml b/.github/workflows/homebrew.yml new file mode 100644 index 0000000..2b058de --- /dev/null +++ b/.github/workflows/homebrew.yml @@ -0,0 +1,16 @@ +on: + push: + tags: + - "v*" + +jobs: + homebrew: + name: Bump Homebrew formula + runs-on: ubuntu-latest + steps: + - uses: mislav/bump-homebrew-formula-action@v3 + with: + # A PR will be sent to github.com/Homebrew/homebrew-core to update this formula: + formula-name: amfora + env: + COMMITTER_TOKEN: ${{ secrets.HOMEBREW_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f0927b8..6a7921f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,8 +19,8 @@ jobs: strategy: fail-fast: false matrix: - go-version: ['1.14', '1.15', '1.16'] - os: [ubuntu-latest, macos-latest, windows-latest] + go-version: ['1.21', '1.22'] + os: [ubuntu-latest] runs-on: ${{ matrix.os }} steps: - name: Install Go diff --git a/.golangci.yml b/.golangci.yml index 80cc6f1..e5ca3f7 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -19,15 +19,13 @@ linters: - goerr113 - gofmt - goimports - - golint + - revive - goprintffuncname - - interfacer - lll - - maligned - misspell - nolintlint - prealloc - - scopelint + - exportloopref - unconvert - unparam diff --git a/.goreleaser.yml b/.goreleaser.yml index 2d5add4..c49fc4e 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -43,6 +43,8 @@ builds: goarch: arm - goos: openbsd goarch: arm64 + - goos: windows + goarch: arm archives: - format: binary diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c37d98..ff905ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,50 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [1.10.0] - 2024-03-17 +### Added +- Syntax highlighting for preformatted text blocks with alt text (#252, #263, [wiki page](https://github.com/makeworld-the-better-one/amfora/wiki/Source-Code-Highlighting)) +- [Client certificates](https://github.com/makeworld-the-better-one/amfora/wiki/Client-Certificates) can be restricted to certain paths of a host (#115) +- `header` config option in `[subscriptions]` to allow disabling the header text on the subscriptions page (#191) +- Selected link and scroll position stays for non-cached pages (#122) +- Keybinding to open URL with URL handler instead of configured proxy (#143) +- `include` theme key to import themes from an external file (#154, #290) +- Support SOCKS5 proxying by setting `AMFORA_SOCKS5` environment variable (#155) +- When bookmarking a page, the first level one heading is suggested as the name (#267, #293) +- Confirmation prompts for URL schemes in new `[url-prompts]` config section (#301, #302) + +### Changed +- Center text automatically, removing `left_margin` from the config (#233) +- `max_width` defaults to 80 columns instead of 100 (#233) +- Tabs have the domain of the current page instead of numbers (#202) +- Closing Amfora with q was removed in favor of Shift-q (#243) +- Paging up or down scrolls by 50% instead of 75%, to match `less` (#303) +- Update deps, require Go 1.17 (#336) +- Show local directory index file if available (#319) +- Updated Project Gemini URLs (#342) + +### Fixed +- Modal can't be closed when opening non-gemini text URLs from the commandline (#283, #284) +- External programs started by Amfora remain as zombie processes (#219) +- Prevent link lines (and other types) from being wider than the `max_width` setting (#280) +- `new:7` on new tab page fails to open link (#306) +- Slashes aren't decoded in redirect URLs (#322, #324) +- Typing `localhost` in the bottom bar actually loads localhost instead of searching (#326, #327) + + +## [1.9.2] - 2021-12-10 +### Fixed +- Preformatted text color showing even when `color = false` (bug since v1.8.0 at least) (#278) +- Link numbers and link text in color even when `color = false` (regression in v1.9.0) (#278) + + +## [1.9.1] - 2021-12-08 +### Fixed +- Deadlock when loading an invalid `about:` URL (#277) +- Crash when rendering text from stdin + + +## [1.9.0] - 2021-12-07 ### Added - Support for version 1.1 JSON feeds - Copy current URL or selected URL to clipboard (#220, #225) @@ -12,21 +55,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Configurable keybindings for scrolling on pages (#211, #222) - Ability to save `about:` pages (#210, #236) - `bind_beginning` and `bind_end` keybindings +- Display gemtext from stdin (#205, #242) +- Specifying `default` in the theme config uses the terminal's default background color, including transparency (#244, #245) +- Redirects occur automatically if it only adds a trailing slash (#271) +- Non-gemini links are underlined by default to help color blind users (#189) +- Text and element colors of default theme change to be black on terminals with light backgrounds (#181) +- Support paths with spaces in `[url-handlers]` config settings (#214) +- Display info modal when opening URL with custom application +- Files can be opened by relative path on the commandline (#231, #257) +- Support keybindings that use Shift (#269) ### Changed -- Favicon support removed (#199) - Bookmarks are stored using XML in the XBEL format, old bookmarks are transferred (#68) -- Text no longer disappears under the left margin when scrolling (regression from v1.8.0) (#197) +- Text no longer disappears under the left margin when scrolling (regression in v1.8.0) (#197) - Default search engine changed to geminispace.info from gus.guru +- The user's terminal theme colors are used by default (#181) +- By default, non-gemini URI schemes are opened in the default application. This requires a config change for previous users, see the [wiki](https://github.com/makeworld-the-better-one/amfora/wiki/Handling-Other-URL-Schemes) (#207) +- Windows uses paths set by `XDG` variables over `APPDATA` if they are set (#255) +- Treat status codes like 22 as equivalent to 20 as per the latest spec (#266) +- Show minimal loading page instead of `about:newtab` when loading a URL in a new tab (#272) + +## Removed +- Favicon support (#199) +- The default Amfora theme, get it back [here](https://github.com/makeworld-the-better-one/amfora/blob/master/contrib/themes/amfora.toml) (#181) ### Fixed - Help text is now the same color as `regular_text` in the theme config - Non-ASCII (multibyte) characters can now be used as keybindings (#198, #200) - Possible subscription update race condition on startup -- Plaintext documents are escaped properly (regression from v1.8.0) +- Plaintext documents are escaped properly (regression in v1.8.0) - Help page scrollbar color matches what's in the theme config - Regression where lists would not appear if `bullets = false` (#234, #235) - Support multiple bookmarks with the same name +- Cert change message grammar: "an security" -> "a security" (#274) +- Display an error modal for status codes that can't be handled +- Prevent user from getting trapped in the help menu when keybindings are pressed (#241, #261) ## [1.8.0] - 2021-02-17 diff --git a/Makefile b/Makefile index 6606467..4220d7e 100644 --- a/Makefile +++ b/Makefile @@ -21,15 +21,15 @@ clean: .PHONY: install install: amfora amfora.desktop - $(INSTALL) -d $(PREFIX)/bin/ - $(INSTALL) -m 755 amfora $(PREFIX)/bin/amfora - $(INSTALL) -d $(PREFIX)/share/applications/ - $(INSTALL) -m 644 amfora.desktop $(PREFIX)/share/applications/amfora.desktop + $(INSTALL) -d $(DESTDIR)$(PREFIX)/bin/ + $(INSTALL) -m 755 amfora $(DESTDIR)$(PREFIX)/bin/amfora + $(INSTALL) -d $(DESTDIR)$(PREFIX)/share/applications/ + $(INSTALL) -m 644 amfora.desktop $(DESTDIR)$(PREFIX)/share/applications/amfora.desktop .PHONY: uninstall uninstall: - $(RM) -f $(PREFIX)/bin/amfora - $(RM) -f $(PREFIX)/share/applications/amfora.desktop + $(RM) -f $(DESTDIR)$(PREFIX)/bin/amfora + $(RM) -f $(DESTDIR)$(PREFIX)/share/applications/amfora.desktop # Development helpers diff --git a/NOTES.md b/NOTES.md index 6836011..d321e3f 100644 --- a/NOTES.md +++ b/NOTES.md @@ -6,7 +6,6 @@ ## Upstream Bugs - Bookmark keys aren't deleted, just set to `""` - Waiting on [this viper PR](https://github.com/spf13/viper/pull/519) to be merged -- [cview.Styles not being used](https://code.rocketnine.space/tslocum/cview/issues/47) - issue is circumvented in Amfora - [ANSI conversion is messed up](https://code.rocketnine.space/tslocum/cview/issues/48) - [WordWrap is broken in some cases](https://code.rocketnine.space/tslocum/cview/issues/27) - close #156 if this is fixed - [Prevent panic when reformatting](https://code.rocketnine.space/tslocum/cview/issues/50) - can't reliably reproduce or debug diff --git a/README.md b/README.md index bcccc20..4a73792 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,16 @@ ###### Recording of v1.0.0 -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. +Amfora aims to be the best looking [Gemini](https://geminiquickst.art/) 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. Use [Windows Terminal](https://www.microsoft.com/en-us/p/windows-terminal/9n0dx20hk701) instead, and make sure it [works with UTF-8](https://akr.am/blog/posts/using-utf-8-in-the-windows-terminal). 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, as well as the Egsam one. +## Project Status + +Amfora is in maintenance mode. When possible, I’ll make/merge bug fixes, and maybe slowly merge feature PRs by others. See my [blog post](https://www.makeworld.space/2023/08/bye_gemini.html) for details. + ## Installation ### Binary @@ -44,11 +48,10 @@ Make sure to click "Watch" in the top right, then "Custom" > "Releases" to get n Amfora is packaged in many Linux distros. It's also on [Scoop](https://scoop.sh/) for Windows users. -### Homebrew +### macOS (Homebrew) -If you use [Homebrew](https://brew.sh/), you can install Amfora through the my personal tap. +If you use [Homebrew](https://brew.sh/), you can install Amfora with: ``` -brew tap makeworld-the-better-one/tap brew install amfora ``` You can update it with: @@ -56,6 +59,19 @@ You can update it with: brew upgrade amfora ``` +### macOS (MacPorts) + +On macOS, Amfora can also be installed through [MacPorts](https://www.macports.org): +``` +sudo port install amfora +``` +You can update it with: +``` +sudo port selfupdate +sudo port upgrade amfora +``` +**NOTE:** this installation source is community-maintained. More information [here](https://ports.macports.org/port/amfora/). + ### Termux If you're using [Termux](https://termux.com/) on Android you can't just run Amfora like normal. After installing Amfora, run `pkg install proot`. Then run `termux-chroot` before running the Amfora binary. You can exit out of the chroot after closing Amfora. See [here](https://stackoverflow.com/q/38959067/7361270) for why this is needed. @@ -68,7 +84,7 @@ This section is for advanced users who want to install the latest (possibly unst Click to expand **Requirements:** -- Go 1.14 or later +- Go 1.15 or later - GNU Make Please note the Makefile does not intend to support Windows, and so there may be issues. @@ -92,7 +108,6 @@ yay -S amfora-git MacOS users can also use [Homebrew](https://brew.sh/) to install the latest commit of Amfora: ``` -brew tap makeworld-the-better-one/tap brew install --HEAD amfora ``` You can update it with: @@ -131,6 +146,7 @@ Features in *italics* are in the master branch, but not in the latest release. - So is subscribing to a page, to know when it changes - [x] Open non-text files in another application - [x] Ability to stream content instead of downloading it first +- [x] *Highlighting of preformatted code blocks that list a language in the alt text* - [ ] Stream support - [ ] Table of contents for pages - [ ] Search in pages with Ctrl-F @@ -151,6 +167,9 @@ Amfora ❤️ open source! - [progressbar](https://github.com/schollz/progressbar) - [go-humanize](https://github.com/dustin/go-humanize) - [gofeed](https://github.com/mmcdole/gofeed) +- [chroma](https://github.com/alecthomas/chroma) for source code syntax highlighting +- [clipboard](https://github.com/atotto/clipboard) +- [termenv](https://github.com/muesli/termenv) ## License This project is licensed under the GPL v3.0. See the [LICENSE](./LICENSE) file for details. diff --git a/THANKS.md b/THANKS.md index b6d6366..2ba5025 100644 --- a/THANKS.md +++ b/THANKS.md @@ -21,3 +21,12 @@ Thank you to the following contributors, who have helped make Amfora great. FOSS * Himanshu (@singalhimanshu) * @regr4 * Anas Mohamed (@amohamed11) +* David Jimenez (@dvejmz) +* Michael McDonagh (@m-mcdonagh) +* mooff (@awfulcooking) +* Josias (@justjosias) +* mntn (@mntn-xyz) +* Maxime Bouillot (@Arkaeriit) +* Emily (@emily-is-my-username) +* Autumn! (@autumnull) +* William Rehwinkel (@FiskFan1999) diff --git a/amfora.go b/amfora.go index db53fbc..641a300 100644 --- a/amfora.go +++ b/amfora.go @@ -2,26 +2,35 @@ package main import ( "fmt" + "io" "os" + "path/filepath" + "strings" "github.com/makeworld-the-better-one/amfora/bookmarks" "github.com/makeworld-the-better-one/amfora/client" "github.com/makeworld-the-better-one/amfora/config" "github.com/makeworld-the-better-one/amfora/display" + "github.com/makeworld-the-better-one/amfora/logger" "github.com/makeworld-the-better-one/amfora/subscriptions" ) var ( - version = "v1.8.0" + version = "v1.10.0" commit = "unknown" builtBy = "unknown" ) func main() { - // err := logger.Init() - // if err != nil { - // panic(err) - // } + log, err := logger.GetLogger() + if err != nil { + panic(err) + } + + debugModeEnabled := os.Getenv("AMFORA_DEBUG") == "1" + if debugModeEnabled { + log.Println("Debug mode enabled") + } if len(os.Args) > 1 { if os.Args[1] == "--version" || os.Args[1] == "-v" { @@ -40,12 +49,17 @@ func main() { } } - err := config.Init() + err = config.Init() if err != nil { fmt.Fprintf(os.Stderr, "Config error: %v\n", err) os.Exit(1) } - client.Init() + + err = client.Init() + if err != nil { + fmt.Fprintf(os.Stderr, "Client error: %v\n", err) + os.Exit(1) + } err = subscriptions.Init() if err != nil { @@ -65,9 +79,30 @@ func main() { // Initialize Amfora's settings display.Init(version, commit, builtBy) - display.NewTab() + + // Load a URL, file, or render from stdin if len(os.Args[1:]) > 0 { - display.URL(os.Args[1]) + url := os.Args[1] + if !strings.Contains(url, "://") || strings.HasPrefix(url, "../") || strings.HasPrefix(url, "./") { + fileName := url + if _, err := os.Stat(fileName); err == nil { + if !strings.HasPrefix(fileName, "/") { + cwd, err := os.Getwd() + if err != nil { + fmt.Fprintf(os.Stderr, "error getting working directory path: %v\n", err) + os.Exit(1) + } + fileName = filepath.Join(cwd, fileName) + } + url = "file://" + fileName + } + } + display.NewTabWithURL(url) + } else if !isStdinEmpty() { + display.NewTab() + renderFromStdin() + } else { + display.NewTab() } // Start @@ -75,3 +110,20 @@ func main() { panic(err) } } + +func isStdinEmpty() bool { + stat, _ := os.Stdin.Stat() + return (stat.Mode() & os.ModeCharDevice) != 0 +} + +func renderFromStdin() { + stdinTextBuilder := new(strings.Builder) + _, err := io.Copy(stdinTextBuilder, os.Stdin) + if err != nil { + fmt.Fprintf(os.Stderr, "error reading from standard input: %v\n", err) + os.Exit(1) + } + + stdinText := stdinTextBuilder.String() + display.RenderFromString(stdinText) +} diff --git a/bookmarks/bookmarks.go b/bookmarks/bookmarks.go index a38c548..b7e4771 100644 --- a/bookmarks/bookmarks.go +++ b/bookmarks/bookmarks.go @@ -61,7 +61,6 @@ func Init() error { err = os.Remove(config.OldBkmkPath) if err != nil { - //nolint:goerr113 return fmt.Errorf( "couldn't delete old bookmarks file (%s), you must delete it yourself to prevent duplicate bookmarks: %w", config.OldBkmkPath, diff --git a/client/client.go b/client/client.go index f216fcc..59b4474 100644 --- a/client/client.go +++ b/client/client.go @@ -2,51 +2,128 @@ package client import ( + "errors" "io/ioutil" "net" "net/url" + "os" + "strings" "sync" "time" "github.com/makeworld-the-better-one/go-gemini" + gemsocks5 "github.com/makeworld-the-better-one/go-gemini-socks5" "github.com/mitchellh/go-homedir" "github.com/spf13/viper" ) +// Simple key for certCache map and others, instead of a full URL +// Only uses the part of the URL relevant to matching certs to a URL +type certMapKey struct { + host string + path string +} + var ( - certCache = make(map[string][][]byte) + // [auth] section of config put into maps + confCerts = make(map[certMapKey]string) + confKeys = make(map[certMapKey]string) + + // Cache the cert and key assigned to different URLs + certCache = make(map[certMapKey][][]byte) certCacheMu = &sync.RWMutex{} fetchClient *gemini.Client ) -func Init() { +func Init() error { fetchClient = &gemini.Client{ ConnectTimeout: 10 * time.Second, // Default is 15 ReadTimeout: time.Duration(viper.GetInt("a-general.page_max_time")) * time.Second, } + + if socksHost := os.Getenv("AMFORA_SOCKS5"); socksHost != "" { + fetchClient.Proxy = gemsocks5.ProxyFunc(socksHost, nil) + } + + // Populate config maps + + certsViper := viper.Sub("auth.certs") + for _, certURL := range certsViper.AllKeys() { + // Normalize URL so that it can be matched no matter how it was written + // in the config + pu, _ := normalizeURL(FixUserURL(certURL)) + if pu == nil { + //nolint:goerr113 + return errors.New("[auth.certs]: couldn't normalize URL: " + certURL) + } + confCerts[certMapKey{pu.Host, pu.Path}] = certsViper.GetString(certURL) + } + + keysViper := viper.Sub("auth.keys") + for _, keyURL := range keysViper.AllKeys() { + pu, _ := normalizeURL(FixUserURL(keyURL)) + if pu == nil { + //nolint:goerr113 + return errors.New("[auth.keys]: couldn't normalize URL: " + keyURL) + } + confKeys[certMapKey{pu.Host, pu.Path}] = keysViper.GetString(keyURL) + } + + return nil } -func clientCert(host string) ([]byte, []byte) { +// getCertPath returns the path of the cert from the config. +// It returns "" if no config value exists. +func getCertPath(host string, path string) string { + for k, v := range confCerts { + if k.host == host && (k.path == path || strings.HasPrefix(path, k.path)) { + // Either exact match to what's in config, or a subpath + return v + } + } + // No matches + return "" +} + +// getKeyPath returns the path of the key from the config. +// It returns "" if no config value exists. +func getKeyPath(host string, path string) string { + for k, v := range confKeys { + if k.host == host && (k.path == path || strings.HasPrefix(path, k.path)) { + // Either exact match to what's in config, or a subpath + return v + } + } + // No matches + return "" +} + +func clientCert(host string, path string) ([]byte, []byte) { + mkey := certMapKey{host, path} + certCacheMu.RLock() - pair, ok := certCache[host] + pair, ok := certCache[mkey] certCacheMu.RUnlock() if ok { return pair[0], pair[1] } + ogCertPath := getCertPath(host, path) // Expand paths starting with ~/ - certPath, err := homedir.Expand(viper.GetString("auth.certs." + host)) + certPath, err := homedir.Expand(ogCertPath) if err != nil { - certPath = viper.GetString("auth.certs." + host) + certPath = ogCertPath } - keyPath, err := homedir.Expand(viper.GetString("auth.keys." + host)) + ogKeyPath := getKeyPath(host, path) + keyPath, err := homedir.Expand(ogKeyPath) if err != nil { - keyPath = viper.GetString("auth.keys." + host) + keyPath = ogKeyPath } + if certPath == "" && keyPath == "" { certCacheMu.Lock() - certCache[host] = [][]byte{nil, nil} + certCache[mkey] = [][]byte{nil, nil} certCacheMu.Unlock() return nil, nil } @@ -54,33 +131,33 @@ func clientCert(host string) ([]byte, []byte) { cert, err := ioutil.ReadFile(certPath) if err != nil { certCacheMu.Lock() - certCache[host] = [][]byte{nil, nil} + certCache[mkey] = [][]byte{nil, nil} certCacheMu.Unlock() return nil, nil } key, err := ioutil.ReadFile(keyPath) if err != nil { certCacheMu.Lock() - certCache[host] = [][]byte{nil, nil} + certCache[mkey] = [][]byte{nil, nil} certCacheMu.Unlock() return nil, nil } certCacheMu.Lock() - certCache[host] = [][]byte{cert, key} + certCache[mkey] = [][]byte{cert, key} certCacheMu.Unlock() return cert, key } -// HasClientCert returns whether or not a client certificate exists for a host. -func HasClientCert(host string) bool { - cert, _ := clientCert(host) +// HasClientCert returns whether or not a client certificate exists for a host and path. +func HasClientCert(host string, path string) bool { + cert, _ := clientCert(host, path) return cert != nil } func fetch(u string, c *gemini.Client) (*gemini.Response, error) { parsed, _ := url.Parse(u) - cert, key := clientCert(parsed.Host) + cert, key := clientCert(parsed.Host, parsed.Path) var res *gemini.Response var err error @@ -109,7 +186,7 @@ func Fetch(u string) (*gemini.Response, error) { func fetchWithProxy(proxyHostname, proxyPort, u string, c *gemini.Client) (*gemini.Response, error) { parsed, _ := url.Parse(u) - cert, key := clientCert(parsed.Host) + cert, key := clientCert(parsed.Host, parsed.Path) var res *gemini.Response var err error diff --git a/client/tofu.go b/client/tofu.go index 479be52..d5a6bf5 100644 --- a/client/tofu.go +++ b/client/tofu.go @@ -62,7 +62,6 @@ func loadTofuEntry(domain string, port string) (string, time.Time, error) { 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() @@ -73,7 +72,7 @@ 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) //nolint:errcheck + h.Write(cert.Raw) return fmt.Sprintf("%X", h.Sum(nil)) } diff --git a/client/url.go b/client/url.go new file mode 100644 index 0000000..5f7f3c4 --- /dev/null +++ b/client/url.go @@ -0,0 +1,97 @@ +package client + +// Functions that transform and normalize URLs +// Originally used to be in display/util.go +// Moved here for #115, so URLs in the [auth] config section could be normalized + +import ( + "net/url" + "strings" + + "github.com/makeworld-the-better-one/go-gemini" + "golang.org/x/text/unicode/norm" +) + +// See doc for NormalizeURL +func normalizeURL(u string) (*url.URL, string) { + u = norm.NFC.String(u) + + tmp, err := gemini.GetPunycodeURL(u) + if err != nil { + return nil, u + } + u = tmp + parsed, _ := url.Parse(u) + + if parsed.Scheme == "" { + // Always add scheme + parsed.Scheme = "gemini" + } else if parsed.Scheme != "gemini" { + // Not a gemini URL, nothing to do + return nil, u + } + + parsed.User = nil // No passwords in Gemini + parsed.Fragment = "" // No fragments either + if parsed.Port() == "1965" { + // Always remove default port + hostname := parsed.Hostname() + if strings.Contains(hostname, ":") { + parsed.Host = "[" + parsed.Hostname() + "]" + } else { + parsed.Host = parsed.Hostname() + } + } + + // Add slash to the end of a URL with just a domain + // gemini://example.com -> gemini://example.com/ + if parsed.Path == "" { + parsed.Path = "/" + } + + // Do the same to the query string + un, err := gemini.QueryUnescape(parsed.RawQuery) + if err == nil { + parsed.RawQuery = gemini.QueryEscape(un) + } + + return parsed, "" +} + +// NormalizeURL attempts to make URLs that are different strings +// but point to the same place all look the same. +// +// Example: gemini://gus.guru:1965/ and //gus.guru/. +// This function will take both output the same URL each time. +// +// It will also percent-encode invalid characters, and decode chars +// that don't need to be encoded. It will also apply Unicode NFC +// normalization. +// +// The string passed must already be confirmed to be a URL. +// Detection of a search string vs. a URL must happen elsewhere. +// +// It only works with absolute URLs. +func NormalizeURL(u string) string { + pu, s := normalizeURL(u) + if pu != nil { + // Could be normalized, return it + return pu.String() + } + // Return the best URL available up to that point + return s +} + +// FixUserURL will take a user-typed URL and add a gemini scheme to it if +// necessary. It is not the same as normalizeURL, and that func should still +// be used, afterward. +// +// For example "example.com" will become "gemini://example.com", but +// "//example.com" will be left untouched. +func FixUserURL(u string) string { + if !strings.HasPrefix(u, "//") && !strings.HasPrefix(u, "gemini://") && !strings.Contains(u, "://") { + // Assume it's a Gemini URL + u = "gemini://" + u + } + return u +} diff --git a/display/util_test.go b/client/url_test.go similarity index 80% rename from display/util_test.go rename to client/url_test.go index 02e1bca..6455cd5 100644 --- a/display/util_test.go +++ b/client/url_test.go @@ -1,5 +1,5 @@ //nolint: lll -package display +package client import ( "testing" @@ -23,9 +23,10 @@ var normalizeURLTests = []struct { {"magnet:?xt=urn:btih:test", "magnet:?xt=urn:btih:test"}, {"https://example.com", "https://example.com"}, // Fixing URL tests - {"gemini://gemini.circumlunar.space/%64%6f%63%73/%66%61%71%2e%67%6d%69", "gemini://gemini.circumlunar.space/docs/faq.gmi"}, + // Some commented out due to #324 + //{"gemini://geminiprotocol.net/%64%6f%63%73/%66%61%71%2e%67%6d%69", "gemini://geminiprotocol.net/docs/faq.gmi"}, {"gemini://example.com/蛸", "gemini://example.com/%E8%9B%B8"}, - {"gemini://gemini.circumlunar.space/%64%6f%63%73/;;.'%66%61%71蛸%2e%67%6d%69", "gemini://gemini.circumlunar.space/docs/%3B%3B.%27faq%E8%9B%B8.gmi"}, + //{"gemini://geminiprotocol.net/%64%6f%63%73/;;.'%66%61%71蛸%2e%67%6d%69", "gemini://geminiprotocol.net/docs/%3B%3B.%27faq%E8%9B%B8.gmi"}, {"gemini://example.com/?%2Ch%64ello蛸", "gemini://example.com/?%2Chdello%E8%9B%B8"}, // IPv6 tests, see #195 {"gemini://[::1]", "gemini://[::1]/"}, @@ -36,7 +37,7 @@ var normalizeURLTests = []struct { func TestNormalizeURL(t *testing.T) { for _, tt := range normalizeURLTests { - actual := normalizeURL(tt.u) + actual := NormalizeURL(tt.u) if actual != tt.expected { t.Errorf("normalizeURL(%s): expected %s, actual %s", tt.u, tt.expected, actual) } diff --git a/config/config.go b/config/config.go index b17ea26..09da380 100644 --- a/config/config.go +++ b/config/config.go @@ -15,6 +15,7 @@ import ( "github.com/gdamore/tcell/v2" "github.com/makeworld-the-better-one/amfora/cache" homedir "github.com/mitchellh/go-homedir" + "github.com/muesli/termenv" "github.com/rkoesters/xdg/basedir" "github.com/rkoesters/xdg/userdirs" "github.com/spf13/viper" @@ -59,9 +60,16 @@ var MediaHandlers = make(map[string]MediaHandler) // Defaults to ScrollBarAuto on an invalid value var ScrollBar cview.ScrollBarVisibility +// Whether the user's terminal is dark or light +// Defaults to dark, but is determined in Init() +// Used to prevent white text on a white background with the default theme +var hasDarkTerminalBackground bool + func Init() error { // *** Set paths *** + // Windows uses paths under APPDATA, Unix systems use XDG paths + // Windows systems use XDG paths if variables are defined, see #255 home, err := homedir.Dir() if err != nil { @@ -78,10 +86,10 @@ func Init() error { } // Store config directory and file paths - if runtime.GOOS == "windows" { + if runtime.GOOS == "windows" && os.Getenv("XDG_CONFIG_HOME") == "" { configDir = amforaAppData } else { - // Unix / POSIX system + // Unix / POSIX system, or Windows with XDG_CONFIG_HOME defined configDir = filepath.Join(basedir.ConfigHome, "amfora") } configPath = filepath.Join(configDir, "config.toml") @@ -94,7 +102,7 @@ func Init() error { } // Store TOFU db directory and file paths - if runtime.GOOS == "windows" { + if runtime.GOOS == "windows" && os.Getenv("XDG_CACHE_HOME") == "" { // Windows just stores it in APPDATA along with other stuff tofuDBDir = amforaAppData } else { @@ -104,7 +112,7 @@ func Init() error { tofuDBPath = filepath.Join(tofuDBDir, "tofu.toml") // Store bookmarks dir and path - if runtime.GOOS == "windows" { + if runtime.GOOS == "windows" && os.Getenv("XDG_DATA_HOME") == "" { // Windows just keeps it in APPDATA along with other Amfora files bkmkDir = amforaAppData } else { @@ -115,18 +123,12 @@ func Init() error { BkmkPath = filepath.Join(bkmkDir, "bookmarks.xml") // Feeds dir and path - if runtime.GOOS == "windows" { + if runtime.GOOS == "windows" && os.Getenv("XDG_DATA_HOME") == "" { // In APPDATA beside other Amfora files subscriptionDir = amforaAppData } else { // XDG data dir on POSIX systems - xdg_data, ok := os.LookupEnv("XDG_DATA_HOME") - if ok && strings.TrimSpace(xdg_data) != "" { - subscriptionDir = filepath.Join(xdg_data, "amfora") - } else { - // Default to ~/.local/share/amfora - subscriptionDir = filepath.Join(home, ".local", "share", "amfora") - } + subscriptionDir = filepath.Join(basedir.DataHome, "amfora") } SubscriptionPath = filepath.Join(subscriptionDir, "subscriptions.json") @@ -188,21 +190,23 @@ func Init() error { // Setup main config - viper.SetDefault("a-general.home", "gemini://gemini.circumlunar.space") + viper.SetDefault("a-general.home", "gemini://geminiprotocol.net") viper.SetDefault("a-general.auto_redirect", false) viper.SetDefault("a-general.http", "default") viper.SetDefault("a-general.search", "gemini://geminispace.info/search") viper.SetDefault("a-general.color", true) viper.SetDefault("a-general.ansi", true) + viper.SetDefault("a-general.highlight_code", true) + viper.SetDefault("a-general.highlight_style", "monokai") viper.SetDefault("a-general.bullets", true) viper.SetDefault("a-general.show_link", false) - viper.SetDefault("a-general.left_margin", 0.15) - viper.SetDefault("a-general.max_width", 100) + viper.SetDefault("a-general.max_width", 80) viper.SetDefault("a-general.downloads", "") viper.SetDefault("a-general.temp_downloads", "") viper.SetDefault("a-general.page_max_size", 2097152) viper.SetDefault("a-general.page_max_time", 10) viper.SetDefault("a-general.scrollbar", "auto") + viper.SetDefault("a-general.underline", true) viper.SetDefault("keybindings.bind_reload", []string{"R", "Ctrl-R"}) viper.SetDefault("keybindings.bind_home", "Backspace") viper.SetDefault("keybindings.bind_bookmarks", "Ctrl-B") @@ -224,7 +228,7 @@ func Init() error { viper.SetDefault("keybindings.bind_close_tab", "Ctrl-W") viper.SetDefault("keybindings.bind_next_tab", "F2") viper.SetDefault("keybindings.bind_prev_tab", "F1") - viper.SetDefault("keybindings.bind_quit", []string{"Ctrl-C", "Ctrl-Q", "q"}) + viper.SetDefault("keybindings.bind_quit", []string{"Ctrl-C", "Ctrl-Q", "Q"}) viper.SetDefault("keybindings.bind_help", "?") viper.SetDefault("keybindings.bind_link1", "1") viper.SetDefault("keybindings.bind_link2", "2") @@ -254,7 +258,9 @@ func Init() error { viper.SetDefault("keybindings.bind_next_match", "n") viper.SetDefault("keybindings.bind_prev_match", "p") viper.SetDefault("keybindings.shift_numbers", "") - viper.SetDefault("url-handlers.other", "off") + viper.SetDefault("keybindings.bind_url_handler_open", "Ctrl-U") + viper.SetDefault("url-handlers.other", "default") + viper.SetDefault("url-prompts.other", false) viper.SetDefault("cache.max_size", 0) viper.SetDefault("cache.max_pages", 20) viper.SetDefault("cache.timeout", 1800) @@ -262,6 +268,7 @@ func Init() error { viper.SetDefault("subscriptions.update_interval", 1800) viper.SetDefault("subscriptions.workers", 3) viper.SetDefault("subscriptions.entries_per_page", 20) + viper.SetDefault("subscriptions.header", true) viper.SetConfigFile(configPath) viper.SetConfigType("toml") @@ -344,24 +351,77 @@ func Init() error { cache.SetMaxPages(viper.GetInt("cache.max_pages")) cache.SetTimeout(viper.GetInt("cache.timeout")) + setColor := func(k string, colorStr string) error { + if k == "include" { + return nil + } + colorStr = strings.ToLower(colorStr) + var color tcell.Color + if colorStr == "default" { + if strings.HasSuffix(k, "bg") { + color = tcell.ColorDefault + } else { + return fmt.Errorf(`"default" is only valid for a background color (color ending in "bg"), not "%s"`, k) + } + } else { + color = tcell.GetColor(colorStr) + if color == tcell.ColorDefault { + return fmt.Errorf(`invalid color format for "%s": %s`, k, colorStr) + } + } + SetColor(k, color) + return nil + } + // Setup theme configTheme := viper.Sub("theme") if configTheme != nil { + // Include key comes first + if incPath := configTheme.GetString("include"); incPath != "" { + incViper := viper.New() + newIncPath, err := homedir.Expand(incPath) + if err == nil { + incViper.SetConfigFile(newIncPath) + } else { + incViper.SetConfigFile(incPath) + } + incViper.SetConfigType("toml") + err = incViper.ReadInConfig() + if err != nil { + return err + } + + for k2, v2 := range incViper.AllSettings() { + colorStr, ok := v2.(string) + if !ok { + return fmt.Errorf(`include: value for "%s" is not a string: %v`, k2, v2) + } + if err := setColor(k2, colorStr); err != nil { + return err + } + } + } for k, v := range configTheme.AllSettings() { colorStr, ok := v.(string) if !ok { return fmt.Errorf(`value for "%s" is not a string: %v`, k, v) } - color := tcell.GetColor(strings.ToLower(colorStr)) - if color == tcell.ColorDefault { - return fmt.Errorf(`invalid color format for "%s": %s`, k, colorStr) + if err := setColor(k, colorStr); err != nil { + return err } - SetColor(k, color) } } if viper.GetBool("a-general.color") { cview.Styles.PrimitiveBackgroundColor = GetColor("bg") - } // Otherwise it's black by default + } else { + // No colors allowed, set background to black instead of default + themeMu.Lock() + theme["bg"] = tcell.ColorBlack + cview.Styles.PrimitiveBackgroundColor = tcell.ColorBlack + themeMu.Unlock() + } + + hasDarkTerminalBackground = termenv.HasDarkBackground() // Parse HTTP command HTTPCommand = viper.GetStringSlice("a-general.http") diff --git a/config/default.go b/config/default.go index 96d968e..f481f23 100644 --- a/config/default.go +++ b/config/default.go @@ -3,6 +3,17 @@ package config //go:generate ./default.sh var defaultConf = []byte(`# This is the default config file. # It also shows all the default values, if you don't create the file. +# You can edit this file to set your own configuration for Amfora. + +# When Amfora updates, defaults may change, but this file on your drive will not. +# You can always get the latest defaults on GitHub. +# https://github.com/makeworld-the-better-one/amfora/blob/master/default-config.toml + +# Please also check out the Amfora Wiki for more help +# https://github.com/makeworld-the-better-one/amfora/wiki +# gemini://makeworld.space/amfora-wiki/ + + # All URL values may omit the scheme and/or port, as well as the beginning double slash # Valid URL examples: @@ -14,7 +25,7 @@ var defaultConf = []byte(`# This is the default config file. [a-general] # Press Ctrl-H to access it -home = "gemini://gemini.circumlunar.space" +home = "gemini://geminiprotocol.net" # 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. @@ -26,7 +37,7 @@ auto_redirect = false # If a command is set, than the URL will be added (in quotes) to the end of the command. # A space will be prepended to the URL. # -# The best to define a command is using a string array. +# The best way to define a command is using a string array. # Examples: # http = ['firefox'] # http = ['custom-browser', '--flag', '--option=2'] @@ -47,17 +58,20 @@ color = true # Whether ANSI color codes from the page content should be rendered ansi = true +# Whether or not to support source code highlighting in preformatted blocks based on alt text +highlight_code = true + +# Which highlighting style to use (see https://xyproto.github.io/splash/docs/) +highlight_style = "monokai" + # Whether to replace list asterisks with unicode bullets bullets = true # Whether to show link after link text show_link = false -# A number from 0 to 1, indicating what percentage of the terminal width the left margin should take up. -left_margin = 0.15 - # The max number of columns to wrap a page's text to. Preformatted blocks are not wrapped. -max_width = 100 +max_width = 80 # 'downloads' is the path to a downloads folder. # An empty value means the code will find the default downloads folder for your system. @@ -74,6 +88,10 @@ page_max_time = 10 # "auto" means the scrollbar only appears when the page is longer than the window. scrollbar = "auto" +# Underline non-gemini URLs +# This is done to help color blind users +underline = true + [auth] # Authentication settings @@ -81,13 +99,17 @@ scrollbar = "auto" [auth.certs] # Client certificates -# Set domain name equal to path to client cert -# "example.com" = 'mycert.crt' +# Set URL equal to path to client cert file +# +# "example.com" = 'mycert.crt' # Cert is used for all paths on this domain +# "example.com/dir/"= 'mycert.crt' # Cert is used for /dir/ and everything below only +# +# See the comment at the beginning of this file for examples of all valid types of +# URLs, ports and schemes can be used too [auth.keys] # Client certificate keys -# Set domain name equal to path to key for the client cert above -# "example.com" = 'mycert.key' +# Same as [auth.certs] but the path is to the client key file. [keybindings] @@ -166,24 +188,48 @@ scrollbar = "auto" # bind_copy_target_url # bind_beginning: moving to beginning of page (top left) # bind_end: same but the for the end (bottom left) +# bind_url_handler_open: Open highlighted URL with URL handler (#143) [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" or "" to disable handling it, or +# ftp = ['filezilla'] +# You can set any scheme to 'off' or '' to disable handling it, or # just leave the key unset. # # DO NOT use this for setting the HTTP command. # Use the http setting in the "a-general" section above. # # NOTE: These settings are overrided by the ones in the proxies section. +# +# The best way to define a command is using a string array. +# Examples: +# magnet = ['transmission'] +# foo = ['custom-browser', '--flag', '--option=2'] +# tel = ['/path/with spaces/in it/telephone'] +# # Note the use of single quotes, so that backslashes will not be escaped. +# Using just a string will also work, but it is deprecated, and will degrade if +# you use paths with spaces. # This is a special key that defines the handler for all URL schemes for which # no handler is defined. -other = 'off' +# It uses the special value 'default', which will try and use the default +# application on your computer for opening this kind of URI. +other = 'default' +[url-prompts] +# Specify whether a confirmation prompt should be shown before following URL schemes. +# The special key 'other' matches all schemes that don't match any other key. +# +# Example: prompt on every non-gemini URL +# other = true +# gemini = false +# +# Example: only prompt on HTTP(S) +# other = false +# http = true +# https = true # [[mediatype-handlers]] section # --------------------------------- @@ -300,11 +346,18 @@ workers = 3 # The number of subscription updates displayed per page. entries_per_page = 20 +# Set to false to remove the explanatory text from the top of the subscription page +header = true + [theme] # This section is for changing the COLORS used in Amfora. # These colors only apply if 'color' is enabled above. # Colors can be set using a W3C color name, or a hex value such as "#ffffff". +# Setting a background to "default" keeps the terminal default +# If your terminal has transparency, set any background to "default" to keep it transparent +# The key "bg" is already set to "default", but this can be used on other backgrounds, +# like for modals. # Note that not all colors will work on terminals that do not have truecolor support. # If you want to stick to the standard 16 or 256 colors, you can get @@ -323,6 +376,7 @@ entries_per_page = 20 # EXAMPLES: # hdg_1 = "green" # hdg_2 = "#5f0000" +# bg = "default" # Available keys to set: @@ -334,6 +388,15 @@ entries_per_page = 20 # bottombar_bg # scrollbar: The scrollbar that appears on the right for long pages +# You can also set an 'include' key to process another TOML file that contains theme keys. +# Example: +# include = "my/path/to/special-theme.toml" +# +# Any other theme keys will override this external file. +# You can use this special key to switch between themes easily. +# Download other themes here: https://github.com/makeworld-the-better-one/amfora/tree/master/contrib/themes + + # hdg_1 # hdg_2 # hdg_3 diff --git a/config/keybindings.go b/config/keybindings.go index c81a89a..10d49f0 100644 --- a/config/keybindings.go +++ b/config/keybindings.go @@ -61,6 +61,7 @@ const ( CmdCopyTargetURL CmdBeginning CmdEnd + CmdURLHandlerOpen // See #143 CmdSearch CmdNextMatch CmdPrevMatch @@ -83,7 +84,7 @@ var tcellKeys map[string]tcell.Key // a string in the format used by the configuration file. Support // function for GetKeyBinding(), used to make the help panel helpful. func keyBindingToString(kb keyBinding) (string, bool) { - var prefix string = "" + var prefix string if kb.mod&tcell.ModAlt == tcell.ModAlt { prefix = "Alt-" @@ -106,7 +107,7 @@ func keyBindingToString(kb keyBinding) (string, bool) { // Used by the help panel so bindable keys display with their // bound values rather than hardcoded defaults. func GetKeyBinding(cmd Command) string { - var s string = "" + var s string for kb, c := range bindings { if c == cmd { t, ok := keyBindingToString(kb) @@ -125,14 +126,19 @@ func GetKeyBinding(cmd Command) string { // Parse a single keybinding string and add it to the binding map func parseBinding(cmd Command, binding string) { var k tcell.Key - var m tcell.ModMask = 0 - var r rune = 0 + var m tcell.ModMask + var r rune if strings.HasPrefix(binding, "Alt-") { m = tcell.ModAlt binding = binding[4:] } + if strings.HasPrefix(binding, "Shift-") { + m += tcell.ModShift + binding = binding[6:] + } + if len([]rune(binding)) == 1 { k = tcell.KeyRune r = []rune(binding)[0] @@ -159,46 +165,47 @@ func parseBinding(cmd Command, binding string) { // Called by config.Init() func KeyInit() { configBindings := map[Command]string{ - CmdLink1: "keybindings.bind_link1", - CmdLink2: "keybindings.bind_link2", - CmdLink3: "keybindings.bind_link3", - CmdLink4: "keybindings.bind_link4", - CmdLink5: "keybindings.bind_link5", - CmdLink6: "keybindings.bind_link6", - CmdLink7: "keybindings.bind_link7", - CmdLink8: "keybindings.bind_link8", - CmdLink9: "keybindings.bind_link9", - CmdLink0: "keybindings.bind_link0", - CmdBottom: "keybindings.bind_bottom", - CmdEdit: "keybindings.bind_edit", - CmdHome: "keybindings.bind_home", - CmdBookmarks: "keybindings.bind_bookmarks", - CmdAddBookmark: "keybindings.bind_add_bookmark", - CmdSave: "keybindings.bind_save", - CmdReload: "keybindings.bind_reload", - CmdBack: "keybindings.bind_back", - CmdForward: "keybindings.bind_forward", - CmdMoveUp: "keybindings.bind_moveup", - CmdMoveDown: "keybindings.bind_movedown", - CmdMoveLeft: "keybindings.bind_moveleft", - CmdMoveRight: "keybindings.bind_moveright", - CmdPgup: "keybindings.bind_pgup", - CmdPgdn: "keybindings.bind_pgdn", - CmdNewTab: "keybindings.bind_new_tab", - CmdCloseTab: "keybindings.bind_close_tab", - CmdNextTab: "keybindings.bind_next_tab", - CmdPrevTab: "keybindings.bind_prev_tab", - CmdQuit: "keybindings.bind_quit", - CmdHelp: "keybindings.bind_help", - CmdSub: "keybindings.bind_sub", - CmdAddSub: "keybindings.bind_add_sub", - CmdCopyPageURL: "keybindings.bind_copy_page_url", - CmdCopyTargetURL: "keybindings.bind_copy_target_url", - CmdBeginning: "keybindings.bind_beginning", - CmdEnd: "keybindings.bind_end", - CmdSearch: "keybindings.bind_search", - CmdNextMatch: "keybindings.bind_next_match", - CmdPrevMatch: "keybindings.bind_prev_match", + CmdLink1: "keybindings.bind_link1", + CmdLink2: "keybindings.bind_link2", + CmdLink3: "keybindings.bind_link3", + CmdLink4: "keybindings.bind_link4", + CmdLink5: "keybindings.bind_link5", + CmdLink6: "keybindings.bind_link6", + CmdLink7: "keybindings.bind_link7", + CmdLink8: "keybindings.bind_link8", + CmdLink9: "keybindings.bind_link9", + CmdLink0: "keybindings.bind_link0", + CmdBottom: "keybindings.bind_bottom", + CmdEdit: "keybindings.bind_edit", + CmdHome: "keybindings.bind_home", + CmdBookmarks: "keybindings.bind_bookmarks", + CmdAddBookmark: "keybindings.bind_add_bookmark", + CmdSave: "keybindings.bind_save", + CmdReload: "keybindings.bind_reload", + CmdBack: "keybindings.bind_back", + CmdForward: "keybindings.bind_forward", + CmdMoveUp: "keybindings.bind_moveup", + CmdMoveDown: "keybindings.bind_movedown", + CmdMoveLeft: "keybindings.bind_moveleft", + CmdMoveRight: "keybindings.bind_moveright", + CmdPgup: "keybindings.bind_pgup", + CmdPgdn: "keybindings.bind_pgdn", + CmdNewTab: "keybindings.bind_new_tab", + CmdCloseTab: "keybindings.bind_close_tab", + CmdNextTab: "keybindings.bind_next_tab", + CmdPrevTab: "keybindings.bind_prev_tab", + CmdQuit: "keybindings.bind_quit", + CmdHelp: "keybindings.bind_help", + CmdSub: "keybindings.bind_sub", + CmdAddSub: "keybindings.bind_add_sub", + CmdCopyPageURL: "keybindings.bind_copy_page_url", + CmdCopyTargetURL: "keybindings.bind_copy_target_url", + CmdBeginning: "keybindings.bind_beginning", + CmdEnd: "keybindings.bind_end", + CmdURLHandlerOpen: "keybindings.bind_url_handler_open", + CmdSearch: "keybindings.bind_search", + CmdNextMatch: "keybindings.bind_next_match", + CmdPrevMatch: "keybindings.bind_prev_match", } // This is split off to allow shift_numbers to override bind_tab[1-90] // (This is needed for older configs so that the default bind_tab values diff --git a/config/theme.go b/config/theme.go index f17e4cb..61ab5d9 100644 --- a/config/theme.go +++ b/config/theme.go @@ -8,81 +8,336 @@ import ( ) // Functions to allow themeing configuration. -// UI element colors are mapped to a string key, such as "error" or "tab_bg" +// UI element tcell.Colors are mapped to a string key, such as "error" or "tab_bg" // These are the same keys used in the config file. +// Special color with no real color value +// Used for a default foreground color +// White is the terminal background is black, black if the terminal background is white +// Converted to a real color in this file before being sent out to other modules +const ColorFg = tcell.ColorSpecial | 2 + +// The same as ColorFg, but inverted +const ColorBg = tcell.ColorSpecial | 3 + var themeMu = sync.RWMutex{} var theme = map[string]tcell.Color{ - // Default values below + // Map these for special uses in code + "ColorBg": ColorBg, + "ColorFg": ColorFg, - "bg": tcell.ColorBlack, // Used for cview.Styles.PrimitiveBackgroundColor - "tab_num": tcell.Color30, // xterm:Turquoise4, #008787 - "tab_divider": tcell.ColorWhite, - "bottombar_label": tcell.Color30, - "bottombar_text": tcell.ColorBlack, - "bottombar_bg": tcell.ColorWhite, - "scrollbar": tcell.ColorWhite, + // Default values below + // Only the 16 Xterm system tcell.Colors are used, because those are the tcell.Colors overrided + // by the user's default terminal theme + + // Used for cview.Styles.PrimitiveBackgroundColor + // Set to tcell.ColorDefault because that allows transparent terminals to work + // The rest of this theme assumes that the background is equivalent to black, but + // white colors switched to black later if the background is determined to be white. + // + // Also, this is set to tcell.ColorBlack in config.go if colors are disabled in the config. + "bg": tcell.ColorDefault, + + "tab_num": tcell.ColorTeal, + "tab_divider": ColorFg, + "bottombar_label": tcell.ColorTeal, + "bottombar_text": ColorBg, + "bottombar_bg": ColorFg, + "scrollbar": ColorFg, // Modals - "btn_bg": tcell.ColorNavy, // All modal buttons - "btn_text": tcell.ColorWhite, + "btn_bg": tcell.ColorTeal, // All modal buttons + "btn_text": tcell.ColorWhite, // White instead of ColorFg because background is known to be Teal - "dl_choice_modal_bg": tcell.ColorPurple, + "dl_choice_modal_bg": tcell.ColorOlive, "dl_choice_modal_text": tcell.ColorWhite, - "dl_modal_bg": tcell.Color130, // xterm:DarkOrange3, #af5f00 + "dl_modal_bg": tcell.ColorOlive, "dl_modal_text": tcell.ColorWhite, "info_modal_bg": tcell.ColorGray, "info_modal_text": tcell.ColorWhite, "error_modal_bg": tcell.ColorMaroon, "error_modal_text": tcell.ColorWhite, - "yesno_modal_bg": tcell.ColorPurple, + "yesno_modal_bg": tcell.ColorTeal, "yesno_modal_text": tcell.ColorWhite, "tofu_modal_bg": tcell.ColorMaroon, "tofu_modal_text": tcell.ColorWhite, - "subscription_modal_bg": tcell.Color61, // xterm:SlateBlue3, #5f5faf + "subscription_modal_bg": tcell.ColorTeal, "subscription_modal_text": tcell.ColorWhite, "input_modal_bg": tcell.ColorGreen, "input_modal_text": tcell.ColorWhite, - "input_modal_field_bg": tcell.ColorBlue, + "input_modal_field_bg": tcell.ColorNavy, "input_modal_field_text": tcell.ColorWhite, "bkmk_modal_bg": tcell.ColorTeal, "bkmk_modal_text": tcell.ColorWhite, "bkmk_modal_label": tcell.ColorYellow, - "bkmk_modal_field_bg": tcell.ColorBlue, + "bkmk_modal_field_bg": tcell.ColorNavy, "bkmk_modal_field_text": tcell.ColorWhite, "hdg_1": tcell.ColorRed, "hdg_2": tcell.ColorLime, "hdg_3": tcell.ColorFuchsia, - "amfora_link": tcell.Color33, // xterm:DodgerBlue1, #0087ff - "foreign_link": tcell.Color92, // xterm:DarkViolet, #8700d7 + "amfora_link": tcell.ColorBlue, + "foreign_link": tcell.ColorPurple, "link_number": tcell.ColorSilver, - "regular_text": tcell.ColorWhite, - "quote_text": tcell.ColorWhite, - "preformatted_text": tcell.Color229, // xterm:Wheat1, #ffffaf - "list_text": tcell.ColorWhite, + "regular_text": ColorFg, + "quote_text": ColorFg, + "preformatted_text": ColorFg, + "list_text": ColorFg, } func SetColor(key string, color tcell.Color) { themeMu.Lock() - theme[key] = color + // Use truecolor because this is only called with user-set tcell.Colors + // Which should be represented exactly + theme[key] = color.TrueColor() themeMu.Unlock() } -// GetColor will return tcell.ColorBlack if there is no color for the provided key. +// GetColor will return tcell.ColorBlack if there is no tcell.Color for the provided key. func GetColor(key string) tcell.Color { themeMu.RLock() defer themeMu.RUnlock() - return theme[key].TrueColor() + + color := theme[key] + + if color == ColorFg { + if hasDarkTerminalBackground { + return tcell.ColorWhite + } + return tcell.ColorBlack + } + if color == ColorBg { + if hasDarkTerminalBackground { + return tcell.ColorBlack + } + return tcell.ColorWhite + } + + return color } -// GetColorString returns a string that can be used in a cview color tag, +// colorToString converts a color to a string for use in a cview tag +func colorToString(color tcell.Color) string { + if color == tcell.ColorDefault { + return "-" + } + + if color == ColorFg { + if hasDarkTerminalBackground { + return "white" + } + return "black" + } + if color == ColorBg { + if hasDarkTerminalBackground { + return "black" + } + return "white" + } + + if color&tcell.ColorIsRGB == 0 { + // tcell.Color is not RGB/TrueColor, it's a tcell.Color from the default terminal + // theme as set above + // Return a tcell.Color name instead of a hex code, so that cview doesn't use TrueColor + return ColorToColorName[color] + } + + // Color set by user, must be respected exactly so hex code is used + return fmt.Sprintf("#%06x", color.Hex()) +} + +// GetColorString returns a string that can be used in a cview tcell.Color tag, // for the given theme key. -// It will return "#000000" if there is no color for the provided key. +// It will return "#000000" if there is no tcell.Color for the provided key. func GetColorString(key string) string { themeMu.RLock() defer themeMu.RUnlock() - return fmt.Sprintf("#%06x", theme[key].TrueColor().Hex()) + + return colorToString(theme[key]) +} + +// GetContrastingColor returns tcell.ColorBlack if tcell.Color is brighter than gray +// otherwise returns tcell.ColorWhite if tcell.Color is dimmer than gray +// if tcell.Color is tcell.ColorDefault (undefined luminance) this returns tcell.ColorDefault +func GetContrastingColor(color tcell.Color) tcell.Color { + if color == tcell.ColorDefault { + // tcell.Color should never be tcell.ColorDefault + // only config keys which end in bg are allowed to be set to default + // and the only way the argument of this function is set to tcell.ColorDefault + // is if both the text and bg of an element in the UI are set to default + return tcell.ColorDefault + } + r, g, b := color.RGB() + luminance := (77*r + 150*g + 29*b + 1<<7) >> 8 + const gray = 119 // The middle gray + if luminance > gray { + return tcell.ColorBlack + } + return tcell.ColorWhite +} + +// GetTextColor is the Same as GetColor, unless the key is "default". +// This happens on focus of a UI element which has a bg of default, in which case +// It return tcell.ColorBlack or tcell.ColorWhite, depending on which is more readable +func GetTextColor(key, bg string) tcell.Color { + themeMu.RLock() + defer themeMu.RUnlock() + color := theme[key].TrueColor() + if color != tcell.ColorDefault { + return color + } + return GetContrastingColor(theme[bg].TrueColor()) +} + +// GetTextColorString is the Same as GetColorString, unless the key is "default". +// This happens on focus of a UI element which has a bg of default, in which case +// It return tcell.ColorBlack or tcell.ColorWhite, depending on which is more readable +func GetTextColorString(key, bg string) string { + return colorToString(GetTextColor(key, bg)) +} + +// Inverted version of a tcell map +// https://github.com/gdamore/tcell/blob/v2.3.3/color.go#L845 +var ColorToColorName = map[tcell.Color]string{ + tcell.ColorBlack: "black", + tcell.ColorMaroon: "maroon", + tcell.ColorGreen: "green", + tcell.ColorOlive: "olive", + tcell.ColorNavy: "navy", + tcell.ColorPurple: "purple", + tcell.ColorTeal: "teal", + tcell.ColorSilver: "silver", + tcell.ColorGray: "gray", + tcell.ColorRed: "red", + tcell.ColorLime: "lime", + tcell.ColorYellow: "yellow", + tcell.ColorBlue: "blue", + tcell.ColorFuchsia: "fuchsia", + tcell.ColorAqua: "aqua", + tcell.ColorWhite: "white", + tcell.ColorAliceBlue: "aliceblue", + tcell.ColorAntiqueWhite: "antiquewhite", + tcell.ColorAquaMarine: "aquamarine", + tcell.ColorAzure: "azure", + tcell.ColorBeige: "beige", + tcell.ColorBisque: "bisque", + tcell.ColorBlanchedAlmond: "blanchedalmond", + tcell.ColorBlueViolet: "blueviolet", + tcell.ColorBrown: "brown", + tcell.ColorBurlyWood: "burlywood", + tcell.ColorCadetBlue: "cadetblue", + tcell.ColorChartreuse: "chartreuse", + tcell.ColorChocolate: "chocolate", + tcell.ColorCoral: "coral", + tcell.ColorCornflowerBlue: "cornflowerblue", + tcell.ColorCornsilk: "cornsilk", + tcell.ColorCrimson: "crimson", + tcell.ColorDarkBlue: "darkblue", + tcell.ColorDarkCyan: "darkcyan", + tcell.ColorDarkGoldenrod: "darkgoldenrod", + tcell.ColorDarkGray: "darkgray", + tcell.ColorDarkGreen: "darkgreen", + tcell.ColorDarkKhaki: "darkkhaki", + tcell.ColorDarkMagenta: "darkmagenta", + tcell.ColorDarkOliveGreen: "darkolivegreen", + tcell.ColorDarkOrange: "darkorange", + tcell.ColorDarkOrchid: "darkorchid", + tcell.ColorDarkRed: "darkred", + tcell.ColorDarkSalmon: "darksalmon", + tcell.ColorDarkSeaGreen: "darkseagreen", + tcell.ColorDarkSlateBlue: "darkslateblue", + tcell.ColorDarkSlateGray: "darkslategray", + tcell.ColorDarkTurquoise: "darkturquoise", + tcell.ColorDarkViolet: "darkviolet", + tcell.ColorDeepPink: "deeppink", + tcell.ColorDeepSkyBlue: "deepskyblue", + tcell.ColorDimGray: "dimgray", + tcell.ColorDodgerBlue: "dodgerblue", + tcell.ColorFireBrick: "firebrick", + tcell.ColorFloralWhite: "floralwhite", + tcell.ColorForestGreen: "forestgreen", + tcell.ColorGainsboro: "gainsboro", + tcell.ColorGhostWhite: "ghostwhite", + tcell.ColorGold: "gold", + tcell.ColorGoldenrod: "goldenrod", + tcell.ColorGreenYellow: "greenyellow", + tcell.ColorHoneydew: "honeydew", + tcell.ColorHotPink: "hotpink", + tcell.ColorIndianRed: "indianred", + tcell.ColorIndigo: "indigo", + tcell.ColorIvory: "ivory", + tcell.ColorKhaki: "khaki", + tcell.ColorLavender: "lavender", + tcell.ColorLavenderBlush: "lavenderblush", + tcell.ColorLawnGreen: "lawngreen", + tcell.ColorLemonChiffon: "lemonchiffon", + tcell.ColorLightBlue: "lightblue", + tcell.ColorLightCoral: "lightcoral", + tcell.ColorLightCyan: "lightcyan", + tcell.ColorLightGoldenrodYellow: "lightgoldenrodyellow", + tcell.ColorLightGray: "lightgray", + tcell.ColorLightGreen: "lightgreen", + tcell.ColorLightPink: "lightpink", + tcell.ColorLightSalmon: "lightsalmon", + tcell.ColorLightSeaGreen: "lightseagreen", + tcell.ColorLightSkyBlue: "lightskyblue", + tcell.ColorLightSlateGray: "lightslategray", + tcell.ColorLightSteelBlue: "lightsteelblue", + tcell.ColorLightYellow: "lightyellow", + tcell.ColorLimeGreen: "limegreen", + tcell.ColorLinen: "linen", + tcell.ColorMediumAquamarine: "mediumaquamarine", + tcell.ColorMediumBlue: "mediumblue", + tcell.ColorMediumOrchid: "mediumorchid", + tcell.ColorMediumPurple: "mediumpurple", + tcell.ColorMediumSeaGreen: "mediumseagreen", + tcell.ColorMediumSlateBlue: "mediumslateblue", + tcell.ColorMediumSpringGreen: "mediumspringgreen", + tcell.ColorMediumTurquoise: "mediumturquoise", + tcell.ColorMediumVioletRed: "mediumvioletred", + tcell.ColorMidnightBlue: "midnightblue", + tcell.ColorMintCream: "mintcream", + tcell.ColorMistyRose: "mistyrose", + tcell.ColorMoccasin: "moccasin", + tcell.ColorNavajoWhite: "navajowhite", + tcell.ColorOldLace: "oldlace", + tcell.ColorOliveDrab: "olivedrab", + tcell.ColorOrange: "orange", + tcell.ColorOrangeRed: "orangered", + tcell.ColorOrchid: "orchid", + tcell.ColorPaleGoldenrod: "palegoldenrod", + tcell.ColorPaleGreen: "palegreen", + tcell.ColorPaleTurquoise: "paleturquoise", + tcell.ColorPaleVioletRed: "palevioletred", + tcell.ColorPapayaWhip: "papayawhip", + tcell.ColorPeachPuff: "peachpuff", + tcell.ColorPeru: "peru", + tcell.ColorPink: "pink", + tcell.ColorPlum: "plum", + tcell.ColorPowderBlue: "powderblue", + tcell.ColorRebeccaPurple: "rebeccapurple", + tcell.ColorRosyBrown: "rosybrown", + tcell.ColorRoyalBlue: "royalblue", + tcell.ColorSaddleBrown: "saddlebrown", + tcell.ColorSalmon: "salmon", + tcell.ColorSandyBrown: "sandybrown", + tcell.ColorSeaGreen: "seagreen", + tcell.ColorSeashell: "seashell", + tcell.ColorSienna: "sienna", + tcell.ColorSkyblue: "skyblue", + tcell.ColorSlateBlue: "slateblue", + tcell.ColorSlateGray: "slategray", + tcell.ColorSnow: "snow", + tcell.ColorSpringGreen: "springgreen", + tcell.ColorSteelBlue: "steelblue", + tcell.ColorTan: "tan", + tcell.ColorThistle: "thistle", + tcell.ColorTomato: "tomato", + tcell.ColorTurquoise: "turquoise", + tcell.ColorViolet: "violet", + tcell.ColorWheat: "wheat", + tcell.ColorWhiteSmoke: "whitesmoke", + tcell.ColorYellowGreen: "yellowgreen", } diff --git a/contrib/themes/README.md b/contrib/themes/README.md index fb548c8..b1720fa 100644 --- a/contrib/themes/README.md +++ b/contrib/themes/README.md @@ -2,6 +2,14 @@ You can use these themes by replacing the `[theme]` section of your [config](https://github.com/makeworld-the-better-one/amfora/wiki/Configuration) with their contents. Some themes won't display properly on terminals that do not have truecolor support. +## Amfora + +This is the original Amfora theme we all know and love. From v1.9.0 and onwards, the user's terminal theme is used by default. Use this theme to restore the original Amfora look. + + +Demo GIF + + ## Nord Contributed by **[@lokesh-krishna](https://github.com/lokesh-krishna)**. @@ -21,6 +29,22 @@ Contributed by **[@crdpa](https://github.com/crdpa)**. ![screenshot of dracula theme](https://user-images.githubusercontent.com/61637474/99983210-53d2e900-2d8a-11eb-9ab7-12dc10c2933a.png) +## Dracula variant + +Contributed by **[@marcransome](https://github.com/marcransome)**. + +![screenshot of dracula variant theme](https://user-images.githubusercontent.com/679401/132952433-563501ef-4d98-4d43-988e-f15bab7cb155.png) + +
+More screenshots + +![screenshot of dracula variant theme](https://user-images.githubusercontent.com/679401/132952340-96840ad8-fb78-499d-bf6b-3fcdf659edc7.png) +![screenshot of dracula variant theme](https://user-images.githubusercontent.com/679401/132952347-6b93d985-afc8-47b4-9569-1775ce4f37e7.png) +![screenshot of dracula variant theme](https://user-images.githubusercontent.com/679401/132952348-ffcbcc7a-f9ad-41c6-a7d2-5c870754c4c9.png) +![screenshot of dracula variant theme](https://user-images.githubusercontent.com/679401/132952352-50ca16f3-d255-4a1d-a25b-ccf53116957d.png) + +
+ ## Greyscale Light Contributed by **[@leifmetcalf](https://github.com/leifmetcalf)**. @@ -82,6 +106,19 @@ Contributed by **[@sergetymo](https://github.com/sergetymo)**. ![screenshot of error modal](https://user-images.githubusercontent.com/65758149/101183206-da73aa00-3657-11eb-8733-5040c8aefb99.png) +## Ayu Light +Contributed by **[@sergetymo](https://github.com/sergetymo)**. + +![screenshot of Ayu Light theme](https://user-images.githubusercontent.com/65758149/181745417-48a92840-10d2-4659-950d-fbc9b3588d5c.png) + +
+More screenshots + +![screenshot of bookmark modal](https://user-images.githubusercontent.com/65758149/181745413-b5a15120-2ff6-4879-8539-0f02f0eece21.png) +![screenshot of error modal](https://user-images.githubusercontent.com/65758149/181745400-c3e9ba95-aee4-4956-91a8-3dddcbad48cc.png) +
+ + ## Atelier Forest Contributed by **[@joyalicegu](https://github.com/joyalicegu)**. @@ -127,6 +164,28 @@ Contributed by **[@knix3](https://github.com/knix3)** ![screenshot of error](https://user-images.githubusercontent.com/69134168/118543250-096f6b00-b722-11eb-9dca-d2b1bd6a8885.png) +## Tokyo Night + +Contributed by **[@luetage](https://github.com/luetage)** + +![screenshot of Tokyo Night theme](https://user-images.githubusercontent.com/13988217/130348393-69986b51-ddd7-4310-90ae-382461502535.png) + +## Rosé Pine + +Contributed by **[@mvllow](https://github.com/mvllow)**. + +### Rosé Pine + +screenshot of Rosé Pine theme + +### Rosé Pine Moon + +screenshot of Rosé Pine Moon theme + +### Rosé Pine Dawn + +screenshot of Rosé Pine Dawn theme + ## Yours? Contribute your own theme by opening a PR. diff --git a/contrib/themes/amfora.toml b/contrib/themes/amfora.toml new file mode 100644 index 0000000..792a0e2 --- /dev/null +++ b/contrib/themes/amfora.toml @@ -0,0 +1,50 @@ +#[theme] + +# Only the 256 xterm colors are used, so truecolor support is not needed + +bg = "black" +tab_num = "#008787" +tab_divider = "white" +bottombar_label = "#008787" +bottombar_text = "black" +bottombar_bg = "white" +scrollbar = "white" + +btn_bg = "#000080" +btn_text = "white" + +dl_choice_modal_bg = "#800080" +dl_choice_modal_text = "white" +dl_modal_bg = "#af5f00" +dl_modal_text = "white" +info_modal_bg = "#808080" +info_modal_text = "white" +error_modal_bg = "#800000" +error_modal_text = "white" +yesno_modal_bg = "#800080" +yesno_modal_text = "white" +tofu_modal_bg = "#800000" +tofu_modal_text = "white" +subscription_modal_bg = "#5f5faf" +subscription_modal_text = "white" +input_modal_bg = "#008000" +input_modal_text = "white" +input_modal_field_bg = "#0000ff" +input_modal_field_text = "white" + +bkmk_modal_bg = "#008080" +bkmk_modal_text = "white" +bkmk_modal_label = "#ffff00" +bkmk_modal_field_bg = "#0000ff" +bkmk_modal_field_text = "white" + +hdg_1 = "#ff0000" +hdg_2 = "#00ff00" +hdg_3 = "#ff00ff" +amfora_link = "#0087ff" +foreign_link = "#8700d7" +link_number = "#c0c0c0" +regular_text = "white" +quote_text = "white" +preformatted_text = "#ffffaf" +list_text = "white" diff --git a/contrib/themes/atelier-forest-light.toml b/contrib/themes/atelier-forest-light.toml index 1dd29bc..25e714d 100644 --- a/contrib/themes/atelier-forest-light.toml +++ b/contrib/themes/atelier-forest-light.toml @@ -1,4 +1,4 @@ -[theme] +#[theme] # atelier forest light diff --git a/contrib/themes/atelier-forest.toml b/contrib/themes/atelier-forest.toml index 36ed036..ac811bf 100644 --- a/contrib/themes/atelier-forest.toml +++ b/contrib/themes/atelier-forest.toml @@ -1,4 +1,4 @@ -[theme] +#[theme] # atelier forest diff --git a/contrib/themes/ayu_light.toml b/contrib/themes/ayu_light.toml new file mode 100644 index 0000000..e5d22bd --- /dev/null +++ b/contrib/themes/ayu_light.toml @@ -0,0 +1,56 @@ +# Ayu Light theme ported to Amfora +# by Serge Tymoshenko + +bg = "#fcfcfc" +fg = "#5c6166" +tab_num = "#5c6166" +tab_divider = "#5c6166" +bottombar_bg = "#fcfcfc" +bottombar_text = "#5c6166" +bottombar_label = "#5c6166" + +hdg_1 = "#fa8d3e" +hdg_2 = "#f2ae49" +hdg_3 = "#f2ae49" +amfora_link = "#399ee6" +foreign_link = "#a37acc" +link_number = "#5c6166" +regular_text = "#5c6166" +quote_text = "#4cbf99" +preformatted_text = "#86b300" +list_text = "#5c6166" + +btn_bg = "#55b4d4" +btn_text = "#fcfcfc" + +dl_choice_modal_bg = "#f2ae49" +dl_choice_modal_text = "#fcfcfc" + +dl_modal_bg = "#f2ae49" +dl_modal_text = "#fcfcfc" + +info_modal_bg = "#f2ae49" +info_modal_text = "#fcfcfc" + +error_modal_bg = "#f07171" +error_modal_text = "#fcfcfc" + +yesno_modal_bg = "#f2ae49" +yesno_modal_text = "#fcfcfc" + +tofu_modal_bg = "#ed9366" +tofu_modal_text = "#282c34" + +input_modal_bg = "#f2ae49" +input_modal_text = "#fcfcfc" +input_modal_field_bg = "#e6ba7e" +input_modal_field_text = "#5c6166" + +bkmk_modal_bg = "#f2ae49" +bkmk_modal_text = "#fcfcfc" +bkmk_modal_label = "#fcfcfc" +bkmk_modal_field_bg = "#e6ba7e" +bkmk_modal_field_text = "#5c6166" + +subscription_modal_bg = "#f2ae49" +subscription_modal_text = "#5c6166" diff --git a/contrib/themes/dracula-variant.toml b/contrib/themes/dracula-variant.toml new file mode 100644 index 0000000..b56dcca --- /dev/null +++ b/contrib/themes/dracula-variant.toml @@ -0,0 +1,48 @@ +#[theme] +bg = "#282a36" +tab_num = "#bd93f9" +tab_divider = "#f8f8f2" +bottombar_label = "#bd93f9" +bottombar_text = "#8be9fd" +bottombar_bg = "#44475a" +scrollbar = "#44475a" + +hdg_1 = "#bd93f9" +hdg_2 = "#bd93f9" +hdg_3 = "#bd93f9" +amfora_link = "#ff79c6" +foreign_link = "#ffb86c" +link_number = "#8be9fd" +regular_text = "#f8f8f2" +quote_text = "#f1fa8c" +preformatted_text = "#ffb86c" +list_text = "#f8f8f2" + +btn_bg = "#44475a" +btn_text = "#f8f8f2" + +dl_choice_modal_bg = "#6272a4" +dl_choice_modal_text = "#f8f8f2" +dl_modal_bg = "#6272a4" +dl_modal_text = "#f8f8f2" +info_modal_bg = "#6272a4" +info_modal_text = "#f8f8f2" +error_modal_bg = "#ff5555" +error_modal_text = "#f8f8f2" +yesno_modal_bg = "#6272a4" +yesno_modal_text = "#f8f8f2" +tofu_modal_bg = "#6272a4" +tofu_modal_text = "#f8f8f2" +subscription_modal_bg = "#6272a4" +subscription_modal_text = "#f8f8f2" + +input_modal_bg = "#6272a4" +input_modal_text = "#f8f8f2" +input_modal_field_bg = "#44475a" +input_modal_field_text = "#f8f8f2" + +bkmk_modal_bg = "#6272a4" +bkmk_modal_text = "#f8f8f2" +bkmk_modal_label = "#f8f8f2" +bkmk_modal_field_bg = "#44475a" +bkmk_modal_field_text = "#f8f8f2" diff --git a/contrib/themes/dracula.toml b/contrib/themes/dracula.toml index d2073be..8f1692e 100644 --- a/contrib/themes/dracula.toml +++ b/contrib/themes/dracula.toml @@ -1,4 +1,4 @@ -[theme] +#[theme] # This section is for changing the COLORS used in Amfora. # These colors only apply if 'color' is enabled above. # Colors can be set using a W3C color name, or a hex value such as "#ffffff". diff --git a/contrib/themes/greyscale-light.toml b/contrib/themes/greyscale-light.toml index 19a3710..b245589 100644 --- a/contrib/themes/greyscale-light.toml +++ b/contrib/themes/greyscale-light.toml @@ -1,4 +1,4 @@ -[theme] +#[theme] bg = "#ffffff" tab_num = "#000000" tab_divider = "#000000" diff --git a/contrib/themes/gruvbox.toml b/contrib/themes/gruvbox.toml index 9f150d2..d4b95fd 100644 --- a/contrib/themes/gruvbox.toml +++ b/contrib/themes/gruvbox.toml @@ -1,4 +1,4 @@ -[theme] +#[theme] # This section is for changing the COLORS used in Amfora. # These colors only apply if 'color' is enabled above. # Colors can be set using a W3C color name, or a hex value such as "#ffffff". diff --git a/contrib/themes/gruvbox_dark.toml b/contrib/themes/gruvbox_dark.toml index e2052ad..d6ba8a3 100644 --- a/contrib/themes/gruvbox_dark.toml +++ b/contrib/themes/gruvbox_dark.toml @@ -1,4 +1,4 @@ -[theme] +#[theme] # Gruvbox Dark theme diff --git a/contrib/themes/iceberg.toml b/contrib/themes/iceberg.toml index 4baa260..2db2ac8 100644 --- a/contrib/themes/iceberg.toml +++ b/contrib/themes/iceberg.toml @@ -1,4 +1,4 @@ -[theme] +#[theme] # This section is for changing the COLORS used in Amfora. # These colors only apply if 'color' is enabled above. # Colors can be set using a W3C color name, or a hex value such as "#ffffff". diff --git a/contrib/themes/nord.toml b/contrib/themes/nord.toml index 0516b62..8b0182e 100644 --- a/contrib/themes/nord.toml +++ b/contrib/themes/nord.toml @@ -1,4 +1,4 @@ -[theme] +#[theme] # This section is for changing the COLORS used in Amfora. # These colors only apply if 'color' is enabled above. # Colors can be set using a W3C color name, or a hex value such as "#ffffff". diff --git a/contrib/themes/one_dark.toml b/contrib/themes/one_dark.toml index 227f641..9b99d43 100644 --- a/contrib/themes/one_dark.toml +++ b/contrib/themes/one_dark.toml @@ -1,7 +1,7 @@ # Atom One Dark theme ported to Amfora # by Serge Tymoshenko -[theme] +#[theme] # This section is for changing the COLORS used in Amfora. # These colors only apply if 'color' is enabled above. # Colors can be set using a W3C color name, or a hex value such as "#ffffff". diff --git a/contrib/themes/rose-pine-dawn.toml b/contrib/themes/rose-pine-dawn.toml new file mode 100644 index 0000000..7f77177 --- /dev/null +++ b/contrib/themes/rose-pine-dawn.toml @@ -0,0 +1,46 @@ +## name: Rosé Pine +## upstream: https://github.com/rose-pine/amfora/blob/main/themes/rose-pine-dawn.toml +## description: All natural pine, faux fur and a bit of soho vibes for the classy minimalist + +bg = "#faf4ed" +tab_num = "#907aa9" +tab_divider = "#dfdad9" +bottombar_label = "#907aa9" +bottombar_text = "#575279" +bottombar_bg = "#fffaf3" +scrollbar = "#f4ede8" +hdg_1 = "#907aa9" +hdg_2 = "#56949f" +hdg_3 = "#d7827e" +amfora_link = "#ea9d34" +foreign_link = "#797593" +link_number = "#9893a5" +regular_text = "#575279" +quote_text = "#575279" +preformatted_text = "#575279" +list_text = "#575279" +btn_bg = "#286983" +btn_text = "#575279" +dl_choice_modal_bg = "#fffaf3" +dl_choice_modal_text = "#575279" +dl_modal_bg = "#fffaf3" +dl_modal_text = "#575279" +info_modal_bg = "#fffaf3" +info_modal_text = "#575279" +error_modal_bg = "#fffaf3" +error_modal_text = "#b4637a" +yesno_modal_bg = "#fffaf3" +yesno_modal_text = "#575279" +tofu_modal_bg = "#fffaf3" +tofu_modal_text = "#575279" +subscription_modal_bg = "#fffaf3" +subscription_modal_text = "#575279" +input_modal_bg = "#fffaf3" +input_modal_text = "#575279" +input_modal_field_bg = "#f2e9e1" +input_modal_field_text = "#575279" +bkmk_modal_bg = "#fffaf3" +bkmk_modal_text = "#575279" +bkmk_modal_label = "#907aa9" +bkmk_modal_field_bg = "#f2e9e1" +bkmk_modal_field_text = "#575279" diff --git a/contrib/themes/rose-pine-moon.toml b/contrib/themes/rose-pine-moon.toml new file mode 100644 index 0000000..9075177 --- /dev/null +++ b/contrib/themes/rose-pine-moon.toml @@ -0,0 +1,46 @@ +## name: Rosé Pine +## upstream: https://github.com/rose-pine/amfora/blob/main/themes/rose-pine-moon.toml +## description: All natural pine, faux fur and a bit of soho vibes for the classy minimalist + +bg = "#232136" +tab_num = "#c4a7e7" +tab_divider = "#44415a" +bottombar_label = "#c4a7e7" +bottombar_text = "#e0def4" +bottombar_bg = "#2a273f" +scrollbar = "#2a283e" +hdg_1 = "#c4a7e7" +hdg_2 = "#9ccfd8" +hdg_3 = "#ea9a97" +amfora_link = "#f6c177" +foreign_link = "#908caa" +link_number = "#6e6a86" +regular_text = "#e0def4" +quote_text = "#e0def4" +preformatted_text = "#e0def4" +list_text = "#e0def4" +btn_bg = "#3e8fb0" +btn_text = "#e0def4" +dl_choice_modal_bg = "#2a273f" +dl_choice_modal_text = "#e0def4" +dl_modal_bg = "#2a273f" +dl_modal_text = "#e0def4" +info_modal_bg = "#2a273f" +info_modal_text = "#e0def4" +error_modal_bg = "#2a273f" +error_modal_text = "#eb6f92" +yesno_modal_bg = "#2a273f" +yesno_modal_text = "#e0def4" +tofu_modal_bg = "#2a273f" +tofu_modal_text = "#e0def4" +subscription_modal_bg = "#2a273f" +subscription_modal_text = "#e0def4" +input_modal_bg = "#2a273f" +input_modal_text = "#e0def4" +input_modal_field_bg = "#393552" +input_modal_field_text = "#e0def4" +bkmk_modal_bg = "#2a273f" +bkmk_modal_text = "#e0def4" +bkmk_modal_label = "#c4a7e7" +bkmk_modal_field_bg = "#393552" +bkmk_modal_field_text = "#e0def4" diff --git a/contrib/themes/rose-pine.toml b/contrib/themes/rose-pine.toml new file mode 100644 index 0000000..28010b5 --- /dev/null +++ b/contrib/themes/rose-pine.toml @@ -0,0 +1,46 @@ +## name: Rosé Pine +## upstream: https://github.com/rose-pine/amfora/blob/main/themes/rose-pine.toml +## description: All natural pine, faux fur and a bit of soho vibes for the classy minimalist + +bg = "#191724" +tab_num = "#c4a7e7" +tab_divider = "#403d52" +bottombar_label = "#c4a7e7" +bottombar_text = "#e0def4" +bottombar_bg = "#1f1d2e" +scrollbar = "#21202e" +hdg_1 = "#c4a7e7" +hdg_2 = "#9ccfd8" +hdg_3 = "#ebbcba" +amfora_link = "#f6c177" +foreign_link = "#908caa" +link_number = "#6e6a86" +regular_text = "#e0def4" +quote_text = "#e0def4" +preformatted_text = "#e0def4" +list_text = "#e0def4" +btn_bg = "#31748f" +btn_text = "#e0def4" +dl_choice_modal_bg = "#1f1d2e" +dl_choice_modal_text = "#e0def4" +dl_modal_bg = "#1f1d2e" +dl_modal_text = "#e0def4" +info_modal_bg = "#1f1d2e" +info_modal_text = "#e0def4" +error_modal_bg = "#1f1d2e" +error_modal_text = "#eb6f92" +yesno_modal_bg = "#1f1d2e" +yesno_modal_text = "#e0def4" +tofu_modal_bg = "#1f1d2e" +tofu_modal_text = "#e0def4" +subscription_modal_bg = "#1f1d2e" +subscription_modal_text = "#e0def4" +input_modal_bg = "#1f1d2e" +input_modal_text = "#e0def4" +input_modal_field_bg = "#26233a" +input_modal_field_text = "#e0def4" +bkmk_modal_bg = "#1f1d2e" +bkmk_modal_text = "#e0def4" +bkmk_modal_label = "#c4a7e7" +bkmk_modal_field_bg = "#26233a" +bkmk_modal_field_text = "#e0def4" diff --git a/contrib/themes/slimey.toml b/contrib/themes/slimey.toml index cde9602..4f26d1e 100644 --- a/contrib/themes/slimey.toml +++ b/contrib/themes/slimey.toml @@ -1,4 +1,4 @@ -[theme] +#[theme] # This section is for changing the COLORS used in Amfora. # These colors only apply if 'color' is enabled above. # Colors can be set using a W3C color name, or a hex value such as "#ffffff". diff --git a/contrib/themes/solarized_dark.toml b/contrib/themes/solarized_dark.toml index 68d20fc..8e26e5e 100644 --- a/contrib/themes/solarized_dark.toml +++ b/contrib/themes/solarized_dark.toml @@ -1,4 +1,4 @@ -[theme] +#[theme] # This section is for changing the COLORS used in Amfora. # These colors only apply if 'color' is enabled above. # Colors can be set using a W3C color name, or a hex value such as "#ffffff". diff --git a/contrib/themes/solarized_light.toml b/contrib/themes/solarized_light.toml index 12dfbb6..da879e3 100644 --- a/contrib/themes/solarized_light.toml +++ b/contrib/themes/solarized_light.toml @@ -1,4 +1,4 @@ -[theme] +#[theme] # This section is for changing the COLORS used in Amfora. # These colors only apply if 'color' is enabled above. # Colors can be set using a W3C color name, or a hex value such as "#ffffff". diff --git a/contrib/themes/tokyo-night.toml b/contrib/themes/tokyo-night.toml new file mode 100644 index 0000000..b2b8340 --- /dev/null +++ b/contrib/themes/tokyo-night.toml @@ -0,0 +1,52 @@ +#[theme] + +# Tokyo Night + +bg = "#1a1b26" +fg = "#a9b1d6" +tab_num = "#565f89" +tab_divider = "#3b4261" +bottombar_label = "#7aa2f7" +bottombar_text = "#7aa2f7" +bottombar_bg = "#1f2335" +scrollbar = "#565f89" + +hdg_1 = "#f7768e" +hdg_2 = "#7dcfff" +hdg_3 = "#bb9af7" +amfora_link = "#73daca" +foreign_link = "#b4f9f8" +link_number = "#ff9e64" +regular_text = "#a9b1d6" +quote_text = "#e0af68" +preformatted_text = "#2ac3de" +list_text = "#a9b1d6" + +btn_bg = "#414868" +btn_text = "#7aa2f7" + +dl_choice_modal_bg = "#414868" +dl_choice_modal_text = "#c0caf5" +dl_modal_bg = "#414868" +dl_modal_text = "#c0caf5" +info_modal_bg = "#414868" +info_modal_text = "#c0caf5" +error_modal_bg = "#414868" +error_modal_text = "#f7768e" +yesno_modal_bg = "#414868" +yesno_modal_text = "#e0af68" +tofu_modal_bg = "#414868" +tofu_modal_text = "#2ac3de" +subscription_modal_bg = "#414868" +subscription_modal_text = "#bb9af7" + +input_modal_bg = "#414868" +input_modal_text = "#c0caf5" +input_modal_field_bg = "#33467c" +input_modal_field_text = "#a9b1d6" + +bkmk_modal_bg = "#414868" +bkmk_modal_text = "#c0caf5" +bkmk_modal_label = "#c0caf5" +bkmk_modal_field_bg = "#33467c" +bkmk_modal_field_text = "#a9b1d6" diff --git a/default-config.toml b/default-config.toml index 93c3f70..3a4ca62 100644 --- a/default-config.toml +++ b/default-config.toml @@ -1,5 +1,16 @@ # This is the default config file. # It also shows all the default values, if you don't create the file. +# You can edit this file to set your own configuration for Amfora. + +# When Amfora updates, defaults may change, but this file on your drive will not. +# You can always get the latest defaults on GitHub. +# https://github.com/makeworld-the-better-one/amfora/blob/master/default-config.toml + +# Please also check out the Amfora Wiki for more help +# https://github.com/makeworld-the-better-one/amfora/wiki +# gemini://makeworld.space/amfora-wiki/ + + # All URL values may omit the scheme and/or port, as well as the beginning double slash # Valid URL examples: @@ -11,7 +22,7 @@ [a-general] # Press Ctrl-H to access it -home = "gemini://gemini.circumlunar.space" +home = "gemini://geminiprotocol.net" # 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. @@ -23,7 +34,7 @@ auto_redirect = false # If a command is set, than the URL will be added (in quotes) to the end of the command. # A space will be prepended to the URL. # -# The best to define a command is using a string array. +# The best way to define a command is using a string array. # Examples: # http = ['firefox'] # http = ['custom-browser', '--flag', '--option=2'] @@ -44,17 +55,20 @@ color = true # Whether ANSI color codes from the page content should be rendered ansi = true +# Whether or not to support source code highlighting in preformatted blocks based on alt text +highlight_code = true + +# Which highlighting style to use (see https://xyproto.github.io/splash/docs/) +highlight_style = "monokai" + # Whether to replace list asterisks with unicode bullets bullets = true # Whether to show link after link text show_link = false -# A number from 0 to 1, indicating what percentage of the terminal width the left margin should take up. -left_margin = 0.15 - # The max number of columns to wrap a page's text to. Preformatted blocks are not wrapped. -max_width = 100 +max_width = 80 # 'downloads' is the path to a downloads folder. # An empty value means the code will find the default downloads folder for your system. @@ -71,6 +85,10 @@ page_max_time = 10 # "auto" means the scrollbar only appears when the page is longer than the window. scrollbar = "auto" +# Underline non-gemini URLs +# This is done to help color blind users +underline = true + [auth] # Authentication settings @@ -78,13 +96,17 @@ scrollbar = "auto" [auth.certs] # Client certificates -# Set domain name equal to path to client cert -# "example.com" = 'mycert.crt' +# Set URL equal to path to client cert file +# +# "example.com" = 'mycert.crt' # Cert is used for all paths on this domain +# "example.com/dir/"= 'mycert.crt' # Cert is used for /dir/ and everything below only +# +# See the comment at the beginning of this file for examples of all valid types of +# URLs, ports and schemes can be used too [auth.keys] # Client certificate keys -# Set domain name equal to path to key for the client cert above -# "example.com" = 'mycert.key' +# Same as [auth.certs] but the path is to the client key file. [keybindings] @@ -163,24 +185,48 @@ scrollbar = "auto" # bind_copy_target_url # bind_beginning: moving to beginning of page (top left) # bind_end: same but the for the end (bottom left) +# bind_url_handler_open: Open highlighted URL with URL handler (#143) [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" or "" to disable handling it, or +# ftp = ['filezilla'] +# You can set any scheme to 'off' or '' to disable handling it, or # just leave the key unset. # # DO NOT use this for setting the HTTP command. # Use the http setting in the "a-general" section above. # # NOTE: These settings are overrided by the ones in the proxies section. +# +# The best way to define a command is using a string array. +# Examples: +# magnet = ['transmission'] +# foo = ['custom-browser', '--flag', '--option=2'] +# tel = ['/path/with spaces/in it/telephone'] +# # Note the use of single quotes, so that backslashes will not be escaped. +# Using just a string will also work, but it is deprecated, and will degrade if +# you use paths with spaces. # This is a special key that defines the handler for all URL schemes for which # no handler is defined. -other = 'off' +# It uses the special value 'default', which will try and use the default +# application on your computer for opening this kind of URI. +other = 'default' +[url-prompts] +# Specify whether a confirmation prompt should be shown before following URL schemes. +# The special key 'other' matches all schemes that don't match any other key. +# +# Example: prompt on every non-gemini URL +# other = true +# gemini = false +# +# Example: only prompt on HTTP(S) +# other = false +# http = true +# https = true # [[mediatype-handlers]] section # --------------------------------- @@ -297,11 +343,18 @@ workers = 3 # The number of subscription updates displayed per page. entries_per_page = 20 +# Set to false to remove the explanatory text from the top of the subscription page +header = true + [theme] # This section is for changing the COLORS used in Amfora. # These colors only apply if 'color' is enabled above. # Colors can be set using a W3C color name, or a hex value such as "#ffffff". +# Setting a background to "default" keeps the terminal default +# If your terminal has transparency, set any background to "default" to keep it transparent +# The key "bg" is already set to "default", but this can be used on other backgrounds, +# like for modals. # Note that not all colors will work on terminals that do not have truecolor support. # If you want to stick to the standard 16 or 256 colors, you can get @@ -320,6 +373,7 @@ entries_per_page = 20 # EXAMPLES: # hdg_1 = "green" # hdg_2 = "#5f0000" +# bg = "default" # Available keys to set: @@ -331,6 +385,15 @@ entries_per_page = 20 # bottombar_bg # scrollbar: The scrollbar that appears on the right for long pages +# You can also set an 'include' key to process another TOML file that contains theme keys. +# Example: +# include = "my/path/to/special-theme.toml" +# +# Any other theme keys will override this external file. +# You can use this special key to switch between themes easily. +# Download other themes here: https://github.com/makeworld-the-better-one/amfora/tree/master/contrib/themes + + # hdg_1 # hdg_2 # hdg_3 diff --git a/display/bookmarks.go b/display/bookmarks.go index 62f7803..7a7f628 100644 --- a/display/bookmarks.go +++ b/display/bookmarks.go @@ -2,6 +2,8 @@ package display import ( "fmt" + "regexp" + "strings" "code.rocketnine.space/tslocum/cview" "github.com/gdamore/tcell/v2" @@ -28,8 +30,11 @@ const ( var bkmkCh = make(chan bkmkAction) var bkmkModalText string // The current text of the input field in the modal +// Regex for extracting top level 1 heading. The title will extracted from the 1st submatch. +var topHeadingRegex = regexp.MustCompile(`(?m)^#[^#][\t ]*[^\s].*$`) + func bkmkInit() { - panels.AddPanel("bkmk", bkmkModal, false, false) + panels.AddPanel(PanelBookmarks, bkmkModal, false, false) m := bkmkModal if viper.GetBool("a-general.color") { @@ -41,8 +46,10 @@ func bkmkInit() { form.SetLabelColor(config.GetColor("bkmk_modal_label")) form.SetFieldBackgroundColor(config.GetColor("bkmk_modal_field_bg")) form.SetFieldTextColor(config.GetColor("bkmk_modal_field_text")) + form.SetFieldBackgroundColorFocused(config.GetColor("bkmk_modal_field_text")) + form.SetFieldTextColorFocused(config.GetTextColor("bkmk_modal_field_bg", "bkmk_modal_field_text")) form.SetButtonBackgroundColorFocused(config.GetColor("btn_text")) - form.SetButtonTextColorFocused(config.GetColor("btn_bg")) + form.SetButtonTextColorFocused(config.GetTextColor("btn_bg", "btn_text")) frame := m.GetFrame() frame.SetBorderColor(config.GetColor("bkmk_modal_text")) frame.SetTitleColor(config.GetColor("bkmk_modal_text")) @@ -109,13 +116,13 @@ func openBkmkModal(name string, exists bool) (string, bkmkAction) { bkmkModalText = text }) - panels.ShowPanel("bkmk") - panels.SendToFront("bkmk") + panels.ShowPanel(PanelBookmarks) + panels.SendToFront(PanelBookmarks) App.SetFocus(bkmkModal) App.Draw() action := <-bkmkCh - panels.HidePanel("bkmk") + panels.HidePanel(PanelBookmarks) App.SetFocus(tabs[curTab].view) App.Draw() @@ -157,7 +164,17 @@ func addBookmark() { return } name, exists := bookmarks.Get(p.URL) + + // Retrieve & use top level 1 heading for name if bookmark does not already exist. + if !exists { + match := topHeadingRegex.FindString(p.Raw) + if match != "" { + name = strings.TrimSpace(match[1:]) + } + } + // Open a bookmark modal with the current name of the bookmark, if it exists + // otherwise use the top level 1 heading as a suggested name newName, action := openBkmkModal(name, exists) //nolint:exhaustive diff --git a/display/display.go b/display/display.go index bf4b7ca..e8f6352 100644 --- a/display/display.go +++ b/display/display.go @@ -6,15 +6,16 @@ import ( "regexp" "strconv" "strings" - "sync" "code.rocketnine.space/tslocum/cview" "github.com/gdamore/tcell/v2" "github.com/makeworld-the-better-one/amfora/cache" + "github.com/makeworld-the-better-one/amfora/client" "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/muesli/termenv" "github.com/spf13/viper" ) @@ -58,14 +59,23 @@ var layout = cview.NewFlex() var newTabPage structs.Page -// Global mutex for changing the size of the left margin on all tabs. -var reformatMu = sync.Mutex{} - var App = cview.NewApplication() func Init(version, commit, builtBy string) { aboutInit(version, commit, builtBy) + // Detect terminal colors for syntax highlighting + switch termenv.ColorProfile() { + case termenv.TrueColor: + renderer.TermColor = "terminal16m" + case termenv.ANSI256: + renderer.TermColor = "terminal256" + case termenv.ANSI: + renderer.TermColor = "terminal16" + case termenv.Ascii: + renderer.TermColor = "" + } + App.EnableMouse(false) App.SetRoot(layout, true) App.SetAfterResizeFunc(func(width int, height int) { @@ -74,26 +84,21 @@ func Init(version, commit, builtBy string) { termH = height // Make sure the current tab content is reformatted when the terminal size changes - go func(t *tab) { - reformatMu.Lock() // Only allow one reformat job at a time - for i := range tabs { - // Overwrite all tabs with a new, differently sized, left margin - browser.AddTab( - strconv.Itoa(i), - makeTabLabel(strconv.Itoa(i+1)), - makeContentLayout(tabs[i].view, leftMargin()), - ) - if tabs[i] == t { - // Reformat page ASAP, in the middle of loop - reformatPageAndSetView(t, t.page) - } + for i := range tabs { + // Overwrite all tabs with a new, differently sized, left margin + browser.AddTab( + strconv.Itoa(i), + tabs[i].label(), + makeContentLayout(tabs[i].view, leftMargin()), + ) + if tabs[i] == tabs[curTab] { + // Reformat page ASAP, in the middle of loop + reformatPageAndSetView(tabs[curTab], tabs[curTab].page) } - App.Draw() - reformatMu.Unlock() - }(tabs[curTab]) + } }) - panels.AddPanel("browser", browser, true, true) + panels.AddPanel(PanelBrowser, browser, true, true) helpInit() @@ -102,8 +107,6 @@ func Init(version, commit, builtBy string) { layout.AddItem(bottomBar, 1, 1, false) if viper.GetBool("a-general.color") { - layout.SetBackgroundColor(config.GetColor("bg")) - bottomBar.SetBackgroundColor(config.GetColor("bottombar_bg")) bottomBar.SetLabelColor(config.GetColor("bottombar_label")) bottomBar.SetFieldBackgroundColor(config.GetColor("bottombar_bg")) @@ -112,7 +115,7 @@ func Init(version, commit, builtBy string) { browser.SetTabBackgroundColor(config.GetColor("bg")) browser.SetTabBackgroundColorFocused(config.GetColor("tab_num")) browser.SetTabTextColor(config.GetColor("tab_num")) - browser.SetTabTextColorFocused(config.GetColor("bg")) + browser.SetTabTextColorFocused(config.GetColor("ColorBg")) browser.SetTabSwitcherDivider( "", fmt.Sprintf("[%s:%s]|[-]", config.GetColorString("tab_divider"), config.GetColorString("bg")), @@ -195,16 +198,19 @@ func Init(version, commit, builtBy string) { if i <= len(tabs[tab].page.Links) && i > 0 { // Open new tab and load link oldTab := tab - NewTab() // Resolve and follow link manually - 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") reset() return } - URL(prevParsed.ResolveReference(nextParsed).String()) + if tabs[oldTab].hasContent() && !tabs[oldTab].isAnAboutPage() { + prevParsed, _ := url.Parse(tabs[oldTab].page.URL) + NewTabWithURL(prevParsed.ResolveReference(nextParsed).String()) + } else { + NewTabWithURL(nextParsed.String()) + } return } } else { @@ -215,21 +221,22 @@ func Init(version, commit, builtBy string) { // We don't want to convert legitimate // :// links to search terms. query := strings.TrimSpace(query) - if (strings.Contains(query, " ") && !hasSpaceisURL.MatchString(query)) || + if ((strings.Contains(query, " ") && !hasSpaceisURL.MatchString(query)) || (!strings.HasPrefix(query, "//") && !strings.Contains(query, "://") && - !strings.Contains(query, ".")) && !strings.HasPrefix(query, "about:") { + !strings.Contains(query, ".")) && !strings.HasPrefix(query, "about:")) && + !(query == "localhost" || strings.HasPrefix(query, "localhost/") || strings.HasPrefix(query, "localhost:")) { // Has a space and follows regex, OR // doesn't start with "//", contain "://", and doesn't have a dot either. // Then it's a search u := viper.GetString("a-general.search") + "?" + gemini.QueryEscape(query) // Don't use the cached version of the search - cache.RemovePage(normalizeURL(u)) + cache.RemovePage(client.NormalizeURL(u)) URL(u) } else { // Full URL // Don't use cached version for manually entered URL - cache.RemovePage(normalizeURL(fixUserURL(query))) + cache.RemovePage(client.NormalizeURL(client.FixUserURL(query))) URL(query) } return @@ -237,7 +244,7 @@ func Init(version, commit, builtBy string) { } 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]) + go followLink(tabs[tab], tabs[tab].page.URL, tabs[tab].page.Links[i-1]) return } // Invalid link number, don't do anything @@ -368,9 +375,16 @@ func Init(version, commit, builtBy string) { // It's focused on a modal right now, nothing should interrupt return event } - _, ok = App.GetFocus().(*cview.Table) - if ok { + frontPanelName, _ := panels.GetFrontPanel() + if frontPanelName == PanelHelp { // It's focused on help right now + if config.TranslateKeyEvent(event) == config.CmdQuit { + // Allow quit key to work, but nothing else + Stop() + return nil + } + // Pass everything else directly, inhibiting other keybindings + // like for editing the URL return event } @@ -452,8 +466,7 @@ func Init(version, commit, builtBy string) { Error("URL Error", err.Error()) return nil } - NewTab() - URL(next) + NewTabWithURL(next) } else { NewTab() } @@ -501,6 +514,17 @@ func Stop() { // NewTab opens a new tab and switches to it, displaying the // the default empty content because there's no URL. func NewTab() { + NewTabWithURL("about:newtab") + + bottomBar.SetLabel("") + bottomBar.SetText("") + tabs[NumTabs()-1].saveBottomBar() + +} + +// NewTabWithURL opens a new tab and switches to it, displaying the +// the URL provided. +func NewTabWithURL(url string) { // Create TextView and change curTab // Set the TextView options, and the changed func to App.Draw() // SetDoneFunc to do link highlighting @@ -517,22 +541,28 @@ func NewTab() { curTab = NumTabs() tabs = append(tabs, makeNewTab()) - temp := newTabPage // Copy - setPage(tabs[curTab], &temp) + + var interstitial string + if !strings.HasPrefix(url, "about:") { + interstitial = "Loading " + url + "..." + } + + setPage(tabs[curTab], renderPageFromString(interstitial)) + + // Regardless of the starting URL, about:newtab will + // be the history root. tabs[curTab].addToHistory("about:newtab") tabs[curTab].history.pos = 0 // Manually set as first page browser.AddTab( strconv.Itoa(curTab), - makeTabLabel(strconv.Itoa(curTab+1)), + tabs[curTab].label(), makeContentLayout(tabs[curTab].view, leftMargin()), ) browser.SetCurrentTab(strconv.Itoa(curTab)) App.SetFocus(tabs[curTab].view) - bottomBar.SetLabel("") - bottomBar.SetText("") - tabs[curTab].saveBottomBar() + URL(url) // Draw just in case App.Draw() @@ -642,13 +672,29 @@ func Reload() { func URL(u string) { t := tabs[curTab] if strings.HasPrefix(u, "about:") { - if final, ok := handleAbout(t, u); ok { - t.addToHistory(final) - } - return + go goURL(t, u) + } else { + go goURL(t, client.FixUserURL(u)) + } +} + +func RenderFromString(str string) { + t := tabs[curTab] + page := renderPageFromString(str) + setPage(t, page) +} + +func renderPageFromString(str string) *structs.Page { + rendered, links := renderer.RenderGemini(str, textWidth(), false) + page := &structs.Page{ + Mediatype: structs.TextGemini, + Raw: str, + Content: rendered, + Links: links, + TermWidth: termW, } - go goURL(t, fixUserURL(u)) + return page } func NumTabs() int { diff --git a/display/download.go b/display/download.go index b57a097..41c1301 100644 --- a/display/download.go +++ b/display/download.go @@ -33,8 +33,8 @@ var dlChoiceCh = make(chan string) var dlModal = cview.NewModal() func dlInit() { - panels.AddPanel("dl", dlModal, false, false) - panels.AddPanel("dlChoice", dlChoiceModal, false, false) + panels.AddPanel(PanelDownload, dlModal, false, false) + panels.AddPanel(PanelDownloadChoiceModal, dlChoiceModal, false, false) dlm := dlModal chm := dlChoiceModal @@ -45,7 +45,7 @@ func dlInit() { chm.SetTextColor(config.GetColor("dl_choice_modal_text")) form := chm.GetForm() form.SetButtonBackgroundColorFocused(config.GetColor("btn_text")) - form.SetButtonTextColorFocused(config.GetColor("btn_bg")) + form.SetButtonTextColorFocused(config.GetTextColor("btn_bg", "btn_text")) frame := chm.GetFrame() frame.SetBorderColor(config.GetColor("dl_choice_modal_text")) frame.SetTitleColor(config.GetColor("dl_choice_modal_text")) @@ -56,7 +56,7 @@ func dlInit() { dlm.SetTextColor(config.GetColor("dl_modal_text")) form = dlm.GetForm() form.SetButtonBackgroundColorFocused(config.GetColor("btn_text")) - form.SetButtonTextColorFocused(config.GetColor("btn_bg")) + form.SetButtonTextColorFocused(config.GetTextColor("btn_bg", "btn_text")) frame = dlm.GetFrame() frame.SetBorderColor(config.GetColor("dl_modal_text")) frame.SetTitleColor(config.GetColor("dl_modal_text")) @@ -96,7 +96,7 @@ func dlInit() { frame.SetTitle(" Download ") dlm.SetDoneFunc(func(buttonIndex int, buttonLabel string) { if buttonLabel == "Ok" { - panels.HidePanel("dl") + panels.HidePanel(PanelDownload) App.SetFocus(tabs[curTab].view) App.Draw() } @@ -141,29 +141,29 @@ func dlChoice(text, u string, resp *gemini.Response) { choice = "Open" } else { dlChoiceModal.SetText(text) - panels.ShowPanel("dlChoice") - panels.SendToFront("dlChoice") + panels.ShowPanel(PanelDownloadChoiceModal) + panels.SendToFront(PanelDownloadChoiceModal) App.SetFocus(dlChoiceModal) App.Draw() choice = <-dlChoiceCh } if choice == "Download" { - panels.HidePanel("dlChoice") + panels.HidePanel(PanelDownloadChoiceModal) App.Draw() downloadURL(config.DownloadsDir, u, resp) resp.Body.Close() // Only close when the file is downloaded return } if choice == "Open" { - panels.HidePanel("dlChoice") + panels.HidePanel(PanelDownloadChoiceModal) App.Draw() open(u, resp) return } // They chose the "Cancel" button - panels.HidePanel("dlChoice") + panels.HidePanel(PanelDownloadChoiceModal) App.SetFocus(tabs[curTab].view) App.Draw() } @@ -191,6 +191,8 @@ func open(u string, resp *gemini.Response) { Error("File Opening Error", "Error executing custom command: "+err.Error()) return } + //nolint:errcheck + go proc.Wait() // Prevent zombies, see #219 Info("Opened with " + cmd[0]) return } @@ -200,7 +202,7 @@ func open(u string, resp *gemini.Response) { return } - panels.HidePanel("dl") + panels.HidePanel(PanelDownload) App.SetFocus(tabs[curTab].view) App.Draw() @@ -214,11 +216,14 @@ func open(u string, resp *gemini.Response) { Info("Opened in default system viewer") } else { cmd := mediaHandler.Cmd - err := exec.Command(cmd[0], append(cmd[1:], path)...).Start() + proc := exec.Command(cmd[0], append(cmd[1:], path)...) + err := proc.Start() if err != nil { Error("File Opening Error", "Error executing custom command: "+err.Error()) return } + //nolint:errcheck + go proc.Wait() // Prevent zombies, see #219 Info("Opened with " + cmd[0]) } App.Draw() @@ -267,15 +272,15 @@ func downloadURL(dir, u string, resp *gemini.Response) string { // Display dlModal.ClearButtons() dlModal.AddButtons([]string{"Downloading..."}) - panels.ShowPanel("dl") - panels.SendToFront("dl") + panels.ShowPanel(PanelDownload) + panels.SendToFront(PanelDownload) App.SetFocus(dlModal) App.Draw() _, err = io.Copy(io.MultiWriter(f, bar), resp.Body) done = true if err != nil { - panels.HidePanel("dl") + panels.HidePanel(PanelDownload) Error("Download Error", err.Error()) f.Close() os.Remove(savePath) // Remove partial file diff --git a/display/file.go b/display/file.go index df9cb59..7fef92e 100644 --- a/display/file.go +++ b/display/file.go @@ -35,6 +35,12 @@ func handleFile(u string) (*structs.Page, bool) { if u[len(u)-1] != '/' { u += "/" } + for _, index := range []string{"index.gmi", "index.gemini"} { + m, err := os.Stat(uri.Path + "/" + index) + if err == nil && !m.IsDir() { + return handleFile(u + index) + } + } return createDirectoryListing(u) case mode.IsRegular(): if fi.Size() > viper.GetInt64("a-general.page_max_size") { diff --git a/display/handlers.go b/display/handlers.go index 8deaf90..3f72166 100644 --- a/display/handlers.go +++ b/display/handlers.go @@ -2,6 +2,7 @@ package display import ( "errors" + "fmt" "mime" "net" "net/url" @@ -13,11 +14,12 @@ import ( "github.com/makeworld-the-better-one/amfora/client" "github.com/makeworld-the-better-one/amfora/config" "github.com/makeworld-the-better-one/amfora/renderer" - "github.com/makeworld-the-better-one/amfora/rr" "github.com/makeworld-the-better-one/amfora/structs" "github.com/makeworld-the-better-one/amfora/subscriptions" + "github.com/makeworld-the-better-one/amfora/sysopen" "github.com/makeworld-the-better-one/amfora/webbrowser" "github.com/makeworld-the-better-one/go-gemini" + "github.com/makeworld-the-better-one/rr" "github.com/spf13/viper" ) @@ -46,16 +48,20 @@ func handleHTTP(u string, showInfo bool) bool { } // Custom command - var err error = nil + var proc *exec.Cmd if len(config.HTTPCommand) > 1 { - err = exec.Command(config.HTTPCommand[0], append(config.HTTPCommand[1:], u)...).Start() + proc = exec.Command(config.HTTPCommand[0], append(config.HTTPCommand[1:], u)...) } else { - err = exec.Command(config.HTTPCommand[0], u).Start() + proc = exec.Command(config.HTTPCommand[0], u) } + err := proc.Start() if err != nil { Error("HTTP Error", "Error executing custom browser command: "+err.Error()) return false } + //nolint:errcheck + go proc.Wait() // Prevent zombies, see #219 + Info("Opened with: " + config.HTTPCommand[0]) App.Draw() return true @@ -68,21 +74,52 @@ func handleOther(u string) { parsed, _ := url.Parse(u) // Search for a handler for the URL scheme - handler := strings.TrimSpace(viper.GetString("url-handlers." + parsed.Scheme)) + handler := viper.GetStringSlice("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()) + // A string and not a list of strings, use old method of parsing + // #214 + handler = strings.Fields(viper.GetString("url-handlers." + parsed.Scheme)) + if len(handler) == 0 { + handler = viper.GetStringSlice("url-handlers.other") + if len(handler) == 0 { + handler = strings.Fields(viper.GetString("url-handlers.other")) + } } } + + if len(handler) == 1 { + // Maybe special key + + switch strings.TrimSpace(handler[0]) { + case "", "off": + Error("URL Error", "Opening "+parsed.Scheme+" URLs is turned off.") + return + case "default": + _, err := sysopen.Open(u) + if err != nil { + Error("Application Error", err.Error()) + return + } + Info("Opened in default application") + return + } + } + + // Custom application command + + var proc *exec.Cmd + if len(handler) > 1 { + proc = exec.Command(handler[0], append(handler[1:], u)...) + } else { + proc = exec.Command(handler[0], u) + } + err := proc.Start() + if err != nil { + Error("URL Error", "Error executing custom command: "+err.Error()) + } + //nolint:errcheck + go proc.Wait() // Prevent zombies, see #219 + Info("Opened with: " + handler[0]) App.Draw() } @@ -179,6 +216,8 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) { } t.mode = tabModeDone + t.preferURLHandler = false + go func(p *structs.Page) { if b && t.hasContent() && !t.isAnAboutPage() && viper.GetBool("subscriptions.popup") { // The current page might be an untracked feed, and the user wants @@ -204,7 +243,7 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) { return ret(handleAbout(t, u)) } - u = normalizeURL(u) + u = client.NormalizeURL(u) u = cache.Redirect(u) parsed, err := url.Parse(u) @@ -213,6 +252,15 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) { return ret("", false) } + // check if a prompt is needed to handle this url + prompt := viper.GetBool("url-prompts.other") + if viper.IsSet("url-prompts." + parsed.Scheme) { + prompt = viper.GetBool("url-prompts." + parsed.Scheme) + } + if prompt && !(YesNo("Follow URL?\n" + u)) { + return ret("", false) + } + proxy := strings.TrimSpace(viper.GetString("proxies." + parsed.Scheme)) usingProxy := false @@ -224,7 +272,7 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) { } if strings.HasPrefix(u, "http") { - if proxy == "" || proxy == "off" { + if proxy == "" || proxy == "off" || t.preferURLHandler { // No proxy available handleHTTP(u, true) return ret("", false) @@ -243,7 +291,7 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) { if !strings.HasPrefix(u, "http") && !strings.HasPrefix(u, "gemini") && !strings.HasPrefix(u, "file") { // Not a Gemini URL - if proxy == "" || proxy == "off" { + if proxy == "" || proxy == "off" || t.preferURLHandler { // No proxy available handleOther(u) return ret("", false) @@ -321,7 +369,7 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) { // Disable read timeout and go back to start res.SetReadTimeout(0) //nolint: errcheck res.Body.(*rr.RestartReader).Restart() - go dlChoice("That page is too large. What would you like to do?", u, res) + dlChoice("That page is too large. What would you like to do?", u, res) return ret("", false) } if errors.Is(err, renderer.ErrTimedOut) { @@ -329,7 +377,7 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) { // Disable read timeout and go back to start res.SetReadTimeout(0) //nolint: errcheck res.Body.(*rr.RestartReader).Restart() - go dlChoice("Loading that page timed out. What would you like to do?", u, res) + dlChoice("Loading that page timed out. What would you like to do?", u, res) return ret("", false) } if err != nil { @@ -339,7 +387,7 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) { page.TermWidth = termW - if !client.HasClientCert(parsed.Host) { + if !client.HasClientCert(parsed.Host, parsed.Path) { // Don't cache pages with client certs go cache.AddPage(page) } @@ -351,12 +399,14 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) { // Could be a non 20 status code, or a different kind of document // Handle each status code - switch res.Status { + // Except 20, that's handled after the switch + status := gemini.CleanStatus(res.Status) + switch status { case 10, 11: var userInput string var ok bool - if res.Status == 10 { + if status == 10 { // Regular input userInput, ok = Input(res.Meta, false) } else { @@ -380,9 +430,10 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) { return ret("", false) } redir := parsed.ResolveReference(parsedMeta).String() + justAddsSlash := (redir == u+"/") // Prompt before redirecting to non-Gemini protocol redirect := false - if !strings.HasPrefix(redir, "gemini") { + if !justAddsSlash && !strings.HasPrefix(redir, "gemini") { if YesNo("Follow redirect to non-Gemini URL?\n" + redir) { redirect = true } else { @@ -390,9 +441,9 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) { } } // Prompt before redirecting - autoRedirect := viper.GetBool("a-general.auto_redirect") + autoRedirect := justAddsSlash || viper.GetBool("a-general.auto_redirect") if redirect || (autoRedirect && numRedirects < 5) || YesNo("Follow redirect?\n"+redir) { - if res.Status == gemini.StatusRedirectPermanent { + if status == gemini.StatusRedirectPermanent { go cache.AddRedir(u, redir) } return ret(handleURL(t, redir, numRedirects+1)) @@ -437,6 +488,12 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) { case 62: Error("Certificate Not Valid", escapeMeta(res.Meta)) return ret("", false) + default: + if !gemini.StatusInRange(status) { + // Status code not in a valid range + Error("Status Code Error", fmt.Sprintf("Out of range status code: %d", status)) + return ret("", false) + } } // Status code 20, but not a document that can be displayed @@ -453,7 +510,7 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) { // Disable read timeout and go back to start res.SetReadTimeout(0) //nolint: errcheck res.Body.(*rr.RestartReader).Restart() - go dlChoice("That file could not be displayed. What would you like to do?", u, res) + dlChoice("That file could not be displayed. What would you like to do?", u, res) } }() return ret("", false) @@ -463,6 +520,6 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) { // Disable read timeout and go back to start res.SetReadTimeout(0) //nolint: errcheck res.Body.(*rr.RestartReader).Restart() - go dlChoice("That file could not be displayed. What would you like to do?", u, res) + dlChoice("That file could not be displayed. What would you like to do?", u, res) return ret("", false) } diff --git a/display/help.go b/display/help.go index 209a6df..a6646a4 100644 --- a/display/help.go +++ b/display/help.go @@ -33,6 +33,7 @@ var helpCells = strings.TrimSpace( "Enter, Tab\tOn a page this will start link highlighting.\n" + "\tPress Tab and Shift-Tab to pick different links.\n" + "\tPress Enter again to go to one, or Esc to stop.\n" + + "%s\tOpen the highlighted URL with a URL handler instead of the configured proxy\n" + "%s\tGo to a specific tab. (Default: Shift-NUMBER)\n" + "%s\tGo to the last tab.\n" + "%s\tPrevious tab\n" + @@ -55,8 +56,8 @@ var helpTable = cview.NewTextView() // Help displays the help and keybindings. func Help() { helpTable.ScrollToBeginning() - panels.ShowPanel("help") - panels.SendToFront("help") + panels.ShowPanel(PanelHelp) + panels.SendToFront(PanelHelp) App.SetFocus(helpTable) } @@ -67,7 +68,7 @@ func helpInit() { helpTable.SetPadding(0, 0, 1, 1) helpTable.SetDoneFunc(func(key tcell.Key) { if key == tcell.KeyEsc || key == tcell.KeyEnter { - panels.HidePanel("help") + panels.HidePanel(PanelHelp) App.SetFocus(tabs[curTab].view) App.Draw() } @@ -95,6 +96,7 @@ func helpInit() { config.GetKeyBinding(config.CmdEdit), config.GetKeyBinding(config.CmdCopyPageURL), config.GetKeyBinding(config.CmdCopyTargetURL), + config.GetKeyBinding(config.CmdURLHandlerOpen), tabKeys, config.GetKeyBinding(config.CmdTab0), config.GetKeyBinding(config.CmdPrevTab), @@ -122,5 +124,5 @@ func helpInit() { w.Flush() - panels.AddPanel("help", helpTable, true, false) + panels.AddPanel(PanelHelp, helpTable, true, false) } diff --git a/display/history.go b/display/history.go index 333adcc..313d941 100644 --- a/display/history.go +++ b/display/history.go @@ -3,6 +3,18 @@ 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], 0) // Load that position in history + + // Set page's scroll and link info from history cache, in case it didn't have it in the page already + // Like for non-cached pages like about: pages + // This fixes #122 + pg := t.history.pageCache[t.history.pos] + p := t.page + p.Row = pg.row + p.Column = pg.column + p.Selected = pg.selected + p.SelectedID = pg.selectedID + p.Mode = pg.mode + t.applyAll() } @@ -11,6 +23,10 @@ func histForward(t *tab) { // Already on the most recent URL in the history return } + + // Update page cache in history for #122 + t.historyCachePage() + t.history.pos++ go applyHist(t) } @@ -20,6 +36,10 @@ func histBack(t *tab) { // First tab in history return } + + // Update page cache in history for #122 + t.historyCachePage() + t.history.pos-- go applyHist(t) } diff --git a/display/modals.go b/display/modals.go index bae710b..f2df3cb 100644 --- a/display/modals.go +++ b/display/modals.go @@ -16,29 +16,27 @@ import ( // The bookmark modal is in bookmarks.go var infoModal = cview.NewModal() - var errorModal = cview.NewModal() - var inputModal = cview.NewModal() -var inputCh = make(chan string) -var inputModalText string // The current text of the input field in the modal - var yesNoModal = cview.NewModal() -// Channel to receive yesNo answer on +var inputCh = make(chan string) var yesNoCh = make(chan bool) +var inputModalText string // The current text of the input field in the modal + +// Internal channel used to know when a modal has been dismissed +var modalDone = make(chan struct{}) + func modalInit() { infoModal.AddButtons([]string{"Ok"}) - errorModal.AddButtons([]string{"Ok"}) - yesNoModal.AddButtons([]string{"Yes", "No"}) - panels.AddPanel("info", infoModal, false, false) - panels.AddPanel("error", errorModal, false, false) - panels.AddPanel("input", inputModal, false, false) - panels.AddPanel("yesno", yesNoModal, false, false) + panels.AddPanel(PanelInfoModal, infoModal, false, false) + panels.AddPanel(PanelErrorModal, errorModal, false, false) + panels.AddPanel(PanelInputModal, inputModal, false, false) + panels.AddPanel(PanelYesNoModal, yesNoModal, false, false) // Color setup if viper.GetBool("a-general.color") { @@ -49,7 +47,7 @@ func modalInit() { m.SetTextColor(config.GetColor("info_modal_text")) form := m.GetForm() form.SetButtonBackgroundColorFocused(config.GetColor("btn_text")) - form.SetButtonTextColorFocused(config.GetColor("btn_bg")) + form.SetButtonTextColorFocused(config.GetTextColor("btn_bg", "btn_text")) frame := m.GetFrame() frame.SetBorderColor(config.GetColor("info_modal_text")) frame.SetTitleColor(config.GetColor("info_modal_text")) @@ -61,7 +59,7 @@ func modalInit() { m.SetTextColor(config.GetColor("error_modal_text")) form = m.GetForm() form.SetButtonBackgroundColorFocused(config.GetColor("btn_text")) - form.SetButtonTextColorFocused(config.GetColor("btn_bg")) + form.SetButtonTextColorFocused(config.GetTextColor("btn_bg", "btn_text")) frame = errorModal.GetFrame() frame.SetBorderColor(config.GetColor("error_modal_text")) frame.SetTitleColor(config.GetColor("error_modal_text")) @@ -78,14 +76,14 @@ func modalInit() { form.SetFieldBackgroundColor(config.GetColor("input_modal_field_bg")) form.SetFieldTextColor(config.GetColor("input_modal_field_text")) form.SetButtonBackgroundColorFocused(config.GetColor("btn_text")) - form.SetButtonTextColorFocused(config.GetColor("btn_bg")) + form.SetButtonTextColorFocused(config.GetTextColor("btn_bg", "btn_text")) m = yesNoModal m.SetButtonBackgroundColor(config.GetColor("btn_bg")) m.SetButtonTextColor(config.GetColor("btn_text")) form = m.GetForm() form.SetButtonBackgroundColorFocused(config.GetColor("btn_text")) - form.SetButtonTextColorFocused(config.GetColor("btn_bg")) + form.SetButtonTextColorFocused(config.GetTextColor("btn_bg", "btn_text")) } else { m := infoModal m.SetBackgroundColor(tcell.ColorBlack) @@ -141,17 +139,19 @@ func modalInit() { frame.SetTitleAlign(cview.AlignCenter) frame.SetTitle(" Info ") infoModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) { - panels.HidePanel("info") + panels.HidePanel(PanelInfoModal) App.SetFocus(tabs[curTab].view) App.Draw() + modalDone <- struct{}{} }) errorModal.SetBorder(true) errorModal.GetFrame().SetTitleAlign(cview.AlignCenter) errorModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) { - panels.HidePanel("error") + panels.HidePanel(PanelErrorModal) App.SetFocus(tabs[curTab].view) App.Draw() + modalDone <- struct{}{} }) inputModal.SetBorder(true) @@ -181,7 +181,7 @@ func modalInit() { dlInit() } -// Error displays an error on the screen in a modal. +// Error displays an error on the screen in a modal, and blocks until dismissed by the user. func Error(title, text string) { if text == "" { text = "No additional information." @@ -196,22 +196,26 @@ func Error(title, text string) { errorModal.GetFrame().SetTitle(title) errorModal.SetText(text) - panels.ShowPanel("error") - panels.SendToFront("error") + panels.ShowPanel(PanelErrorModal) + panels.SendToFront(PanelErrorModal) App.SetFocus(errorModal) App.Draw() + + <-modalDone } -// Info displays some info on the screen in a modal. +// Info displays some info on the screen in a modal, and blocks until dismissed by the user. func Info(s string) { infoModal.SetText(s) - panels.ShowPanel("info") - panels.SendToFront("info") + panels.ShowPanel(PanelInfoModal) + panels.SendToFront(PanelInfoModal) App.SetFocus(infoModal) App.Draw() + + <-modalDone } -// Input pulls up a modal that asks for input, and returns the user's input. +// Input pulls up a modal that asks for input, waits for that input, and returns it. // It returns an bool indicating if the user chose to send input or not. func Input(prompt string, sensitive bool) (string, bool) { // Remove elements and re-add them - to clear input text and keep input in focus @@ -236,14 +240,14 @@ func Input(prompt string, sensitive bool) (string, bool) { } inputModal.SetText(prompt + " ") - panels.ShowPanel("input") - panels.SendToFront("input") + panels.ShowPanel(PanelInputModal) + panels.SendToFront(PanelInputModal) App.SetFocus(inputModal) App.Draw() resp := <-inputCh - panels.HidePanel("input") + panels.HidePanel(PanelInputModal) App.SetFocus(tabs[curTab].view) App.Draw() @@ -253,7 +257,7 @@ func Input(prompt string, sensitive bool) (string, bool) { return resp, true } -// YesNo displays a modal asking a yes-or-no question. +// YesNo displays a modal asking a yes-or-no question, waits for an answer, then returns it as a bool. func YesNo(prompt string) bool { if viper.GetBool("a-general.color") { m := yesNoModal @@ -272,20 +276,20 @@ func YesNo(prompt string) bool { } yesNoModal.GetFrame().SetTitle("") yesNoModal.SetText(prompt) - panels.ShowPanel("yesno") - panels.SendToFront("yesno") + panels.ShowPanel(PanelYesNoModal) + panels.SendToFront(PanelYesNoModal) App.SetFocus(yesNoModal) App.Draw() resp := <-yesNoCh - panels.HidePanel("yesno") + panels.HidePanel(PanelYesNoModal) App.SetFocus(tabs[curTab].view) App.Draw() return resp } // Tofu displays the TOFU warning modal. -// It returns a bool indicating whether the user wants to continue. +// It blocks then returns a bool indicating whether the user wants to continue. func Tofu(host string, expiry time.Time) bool { // Reuses yesNoModal, with error color @@ -305,18 +309,18 @@ func Tofu(host string, expiry time.Time) bool { frame.SetTitle(" TOFU ") m.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? ", + fmt.Sprintf("%s's certificate has changed, possibly indicating a security issue. The certificate would have expired %s. Are you sure you want to continue? ", host, humanize.Time(expiry), ), ) - panels.ShowPanel("yesno") - panels.SendToFront("yesno") + panels.ShowPanel(PanelYesNoModal) + panels.SendToFront(PanelYesNoModal) App.SetFocus(yesNoModal) App.Draw() resp := <-yesNoCh - panels.HidePanel("yesno") + panels.HidePanel(PanelYesNoModal) App.SetFocus(tabs[curTab].view) App.Draw() return resp diff --git a/display/newtab.go b/display/newtab.go index c85d027..7999eb9 100644 --- a/display/newtab.go +++ b/display/newtab.go @@ -29,7 +29,7 @@ Happy browsing! => https://github.com/makeworld-the-better-one/amfora/wiki Amfora Wiki [GitHub] => gemini://makeworld.space/amfora-wiki/ Amfora Wiki [On Gemini!] -=> //gemini.circumlunar.space Project Gemini +=> gemini://geminiprotocol.net Project Gemini ` // Read the new tab content from a file if it exists or fallback to a default page. diff --git a/display/panels.go b/display/panels.go new file mode 100644 index 0000000..9e18b66 --- /dev/null +++ b/display/panels.go @@ -0,0 +1,14 @@ +package display + +const ( + PanelBrowser = "browser" + PanelBookmarks = "bkmk" + PanelDownload = "dl" + PanelDownloadChoiceModal = "dlChoice" + PanelHelp = "help" + + PanelYesNoModal = "yesno" + PanelInfoModal = "info" + PanelErrorModal = "error" + PanelInputModal = "input" +) diff --git a/display/private.go b/display/private.go index ecf4491..f5e138d 100644 --- a/display/private.go +++ b/display/private.go @@ -1,5 +1,8 @@ package display +// This file contains the functions that aren't part of the public API. +// The funcs are for network and displaying. + import ( "net/url" "strconv" @@ -9,17 +12,20 @@ import ( "github.com/makeworld-the-better-one/amfora/structs" ) -// This file contains the functions that aren't part of the public API. -// The funcs are for network and displaying. - -// 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. -// It will handle setting the bottomBar. +// followLink should be used when the user "clicks" a link on a page, +// but not when a URL is opened on a new tab for the first time. +// +// It will handle updating the bottomBar. +// +// It should be called with the `go` keyword to spawn a new goroutine if +// it would otherwise block the UI loop, such as when called from an input +// handler. +// +// It blocks until navigation is finished, and we've completed any user +// interaction related to loading the URL (such as info, error modals) func followLink(t *tab, prev, next string) { if strings.HasPrefix(next, "about:") { - if final, ok := handleAbout(t, next); ok { - t.addToHistory(final) - } + goURL(t, next) return } @@ -29,7 +35,7 @@ func followLink(t *tab, prev, next string) { Error("URL Error", err.Error()) return } - go goURL(t, nextURL) + goURL(t, nextURL) return } // No content on current tab, so the "prev" URL is not valid. @@ -39,7 +45,7 @@ func followLink(t *tab, prev, next string) { Error("URL Error", "Link URL could not be parsed") return } - go goURL(t, next) + goURL(t, next) } // reformatPage will take the raw page content and reformat it according to the current terminal dimensions. @@ -112,7 +118,7 @@ func setPage(t *tab, p *structs.Page) { tabNum := tabNumber(t) browser.AddTab( strconv.Itoa(tabNum), - makeTabLabel(strconv.Itoa(tabNum+1)), + t.label(), makeContentLayout(t.view, leftMargin()), ) App.Draw() @@ -131,9 +137,15 @@ func setPage(t *tab, p *structs.Page) { // // It should be called in a goroutine. func goURL(t *tab, u string) { + // Update page cache in history for #122 + t.historyCachePage() + final, displayed := handleURL(t, u, 0) if displayed { t.addToHistory(final) + } else if t.page.URL == "" { + // The tab is showing interstitial or no content. Let's go to about:newtab. + handleAbout(t, "about:newtab") } if t == tabs[curTab] { // Display the bottomBar state that handleURL set diff --git a/display/subscriptions.go b/display/subscriptions.go index 30e76d8..bc00efa 100644 --- a/display/subscriptions.go +++ b/display/subscriptions.go @@ -97,9 +97,12 @@ func Subscriptions(t *tab, u string) string { } else { // Render page - rawPage += "You can use Ctrl-X to subscribe to a page, or to an Atom/RSS/JSON feed. See the online wiki for more.\n" + - "If you just opened Amfora then updates may appear incrementally. Reload the page to see them.\n\n" + - "=> about:manage-subscriptions Manage subscriptions\n\n" + if viper.GetBool("subscriptions.header") { + rawPage += "You can use Ctrl-X to subscribe to a page, or to an Atom/RSS/JSON feed." + + "See the online wiki for more.\n" + + "If you just opened Amfora then updates may appear incrementally. Reload the page to see them.\n\n" + } + rawPage += "=> about:manage-subscriptions Manage subscriptions\n\n" // curDay represents what day of posts the loop is on. // It only goes backwards in time. @@ -260,13 +263,13 @@ func openSubscriptionModal(validFeed, subscribed bool) bool { } } - panels.ShowPanel("yesno") - panels.SendToFront("yesno") + panels.ShowPanel(PanelYesNoModal) + panels.SendToFront(PanelYesNoModal) App.SetFocus(yesNoModal) App.Draw() resp := <-yesNoCh - panels.HidePanel("yesno") + panels.HidePanel(PanelYesNoModal) App.SetFocus(tabs[curTab].view) App.Draw() return resp diff --git a/display/tab.go b/display/tab.go index 72f0565..7c1e15f 100644 --- a/display/tab.go +++ b/display/tab.go @@ -3,6 +3,7 @@ package display import ( "fmt" "net/url" + "path" "strconv" "strings" @@ -21,19 +22,31 @@ const ( tabModeSearch ) +// tabHistoryPageCache is fields from the Page struct, cached here to solve #122 +// See structs/structs.go for an explanation of the fields. +type tabHistoryPageCache struct { + row int + column int + selected string + selectedID string + mode structs.PageMode +} + type tabHistory struct { - urls []string - pos int // Position: where in the list of URLs we are + urls []string + pos int // Position: where in the list of URLs we are + pageCache []*tabHistoryPageCache } // tab hold the information needed for each browser tab. type tab struct { - page *structs.Page - view *cview.TextView - history *tabHistory - mode tabMode - barLabel string // The bottomBar label for the tab - barText string // The bottomBar text for the tab + page *structs.Page + view *cview.TextView + history *tabHistory + mode tabMode + barLabel string // The bottomBar label for the tab + barText string // The bottomBar text for the tab + preferURLHandler bool // For #143, use URL handler over proxy } // makeNewTab initializes an tab struct with no content. @@ -87,7 +100,8 @@ 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]) + tabs[tab].preferURLHandler = false // Reset in case + go followLink(tabs[tab], tabs[tab].page.URL, tabs[tab].page.Links[linkN]) return } if len(currentSelection) == 0 && (key == tcell.KeyEnter || key == tcell.KeyTab) { @@ -95,7 +109,7 @@ func makeNewTab() *tab { tabs[tab].page.Mode = structs.ModeLinkSelect tabs[tab].view.Highlight("0") - tabs[tab].view.ScrollToHighlight() + tabs[tab].scrollToHighlight() // Display link URL in bottomBar bottomBar.SetLabel("[::b]Link: [::-]") bottomBar.SetText(tabs[tab].page.Links[0]) @@ -116,7 +130,7 @@ func makeNewTab() *tab { return } tabs[tab].view.Highlight(strconv.Itoa(index)) - tabs[tab].view.ScrollToHighlight() + tabs[tab].scrollToHighlight() // Display link URL in bottomBar bottomBar.SetLabel("[::b]Link: [::-]") bottomBar.SetText(tabs[tab].page.Links[index]) @@ -157,12 +171,12 @@ func makeNewTab() *tab { if t.hasContent() { savePath, err := downloadPage(t.page) if err != nil { - Error("Download Error", fmt.Sprintf("Error saving page content: %v", err)) + go Error("Download Error", fmt.Sprintf("Error saving page content: %v", err)) } else { - Info(fmt.Sprintf("Page content saved to %s. ", savePath)) + go Info(fmt.Sprintf("Page content saved to %s. ", savePath)) } } else { - Info("The current page has no content, so it couldn't be downloaded.") + go Info("The current page has no content, so it couldn't be downloaded.") } return nil case config.CmdBack: @@ -179,13 +193,13 @@ func makeNewTab() *tab { currentURL := tabs[curTab].page.URL err := clipboard.WriteAll(currentURL) if err != nil { - Error("Copy Error", err.Error()) + go Error("Copy Error", err.Error()) return nil } return nil case config.CmdCopyTargetURL: currentURL := t.page.URL - selectedURL := t.HighlightedURL() + selectedURL := t.highlightedURL() if selectedURL == "" { return nil } @@ -194,28 +208,42 @@ func makeNewTab() *tab { if err != nil { err := clipboard.WriteAll(selectedURL) if err != nil { - Error("Copy Error", err.Error()) + go Error("Copy Error", err.Error()) return nil } return nil } err = clipboard.WriteAll(copiedURL.String()) if err != nil { - Error("Copy Error", err.Error()) + go Error("Copy Error", err.Error()) return nil } return nil + case config.CmdURLHandlerOpen: + currentSelection := t.view.GetHighlights() + t.preferURLHandler = true + // Copied code from when enter key is pressed + if len(currentSelection) > 0 { + bottomBar.SetLabel("") + linkN, _ := strconv.Atoi(currentSelection[0]) + t.page.Selected = t.page.Links[linkN] + t.page.SelectedID = currentSelection[0] + go followLink(&t, t.page.URL, t.page.Links[linkN]) + } + return nil } // Number key: 1-9, 0, LINK1-LINK10 if cmd >= config.CmdLink1 && cmd <= config.CmdLink0 { if int(cmd) <= len(t.page.Links) { // It's a valid link number - followLink(&t, t.page.URL, t.page.Links[cmd-1]) + t.preferURLHandler = false // Reset in case + go followLink(&t, t.page.URL, t.page.Links[cmd-1]) return nil } } // Scrolling stuff + // Copied in scrollTo key := event.Key() mod := event.Modifiers() @@ -290,6 +318,21 @@ func makeNewTab() *tab { return &t } +// historyCachePage caches certain info about the current page in the tab's history, +// see #122 for details. +func (t *tab) historyCachePage() { + if t.page == nil || t.page.URL == "" || t.history.pageCache == nil || len(t.history.pageCache) == 0 { + return + } + t.history.pageCache[t.history.pos] = &tabHistoryPageCache{ + row: t.page.Row, + column: t.page.Column, + selected: t.page.Selected, + selectedID: t.page.SelectedID, + mode: t.page.Mode, + } +} + // addToHistory adds the given URL to history. // It assumes the URL is currently being loaded and displayed on the page. func (t *tab) addToHistory(u string) { @@ -297,14 +340,20 @@ func (t *tab) addToHistory(u string) { // We're somewhere in the middle of the history instead, with URLs ahead and behind. // The URLs ahead need to be removed so this new URL is the most recent item in the history t.history.urls = t.history.urls[:t.history.pos+1] + // Same for page cache + t.history.pageCache = t.history.pageCache[:t.history.pos+1] } t.history.urls = append(t.history.urls, u) t.history.pos++ + + // Cache page info for #122 + t.history.pageCache = append(t.history.pageCache, &tabHistoryPageCache{}) // Add new spot + t.historyCachePage() // Fill it with data } // pageUp scrolls up 75% of the height of the terminal, like Bombadillo. func (t *tab) pageUp() { - t.page.Row -= (termH / 4) * 3 + t.page.Row -= termH / 2 if t.page.Row < 0 { t.page.Row = 0 } @@ -315,7 +364,7 @@ func (t *tab) pageUp() { func (t *tab) pageDown() { height, _ := t.view.GetBufferSize() - t.page.Row += (termH / 4) * 3 + t.page.Row += termH / 2 if t.page.Row > height { t.page.Row = height } @@ -357,7 +406,7 @@ func (t *tab) applyHorizontalScroll() { // Scrolled to the right far enough that no left margin is needed browser.AddTab( strconv.Itoa(i), - makeTabLabel(strconv.Itoa(i+1)), + t.label(), makeContentLayout(t.view, 0), ) t.view.ScrollTo(t.page.Row, t.page.Column-leftMargin()) @@ -365,7 +414,7 @@ func (t *tab) applyHorizontalScroll() { // Left margin is still needed, but is not necessarily at the right size by default browser.AddTab( strconv.Itoa(i), - makeTabLabel(strconv.Itoa(i+1)), + t.label(), makeContentLayout(t.view, leftMargin()-t.page.Column), ) } @@ -378,6 +427,39 @@ func (t *tab) applyScroll() { t.applyHorizontalScroll() } +// scrollTo scrolls the current tab to specified position. Like +// cview.TextView.ScrollTo but using the custom scrolling logic required by #196. +func (t *tab) scrollTo(row, col int) { + height, width := t.view.GetBufferSize() + + // Keep row and col within limits + + if row < 0 { + row = 0 + } else if row > height { + row = height + } + if col < 0 { + col = 0 + } else if col > width { + col = width + } + + t.page.Row = row + t.page.Column = col + t.applyScroll() + App.Draw() +} + +// scrollToHighlight scrolls the current tab to specified position. Like +// cview.TextView.ScrollToHighlight but using the custom scrolling logic +// required by #196. +func (t *tab) scrollToHighlight() { + t.view.ScrollToHighlight() + App.Draw() + t.scrollTo(t.view.GetScrollOffset()) +} + // saveBottomBar saves the current bottomBar values in the tab. func (t *tab) saveBottomBar() { t.barLabel = bottomBar.GetLabel() @@ -432,8 +514,8 @@ func (t *tab) applyAll() { } } -// HighlightedURL returns the currently selected URL -func (t *tab) HighlightedURL() string { +// highlightedURL returns the currently selected URL +func (t *tab) highlightedURL() string { currentSelection := tabs[curTab].view.GetHighlights() if len(currentSelection) > 0 { @@ -443,3 +525,35 @@ func (t *tab) HighlightedURL() string { } return "" } + +// label returns the label to use for the tab name +func (t *tab) label() string { + tn := tabNumber(t) + if tn < 0 { + // Invalid tab, shouldn't happen + return "" + } + + // Increment so there's no tab 0 in the label + tn++ + + if t.page.URL == "" || t.page.URL == "about:newtab" { + // Just use tab number + // Spaces around to keep original Amfora look + return fmt.Sprintf(" %d ", tn) + } + if strings.HasPrefix(t.page.URL, "about:") { + // Don't look for domain, put the whole URL except query strings + return strings.SplitN(t.page.URL, "?", 2)[0] + } + if strings.HasPrefix(t.page.URL, "file://") { + // File URL, use file or folder as tab name + return cview.Escape(path.Base(t.page.URL[7:])) + } + // Otherwise, it's a Gemini URL + pu, err := url.Parse(t.page.URL) + if err != nil { + return fmt.Sprintf(" %d ", tn) + } + return pu.Host +} diff --git a/display/thanks.go b/display/thanks.go index e533c9b..9f550c6 100644 --- a/display/thanks.go +++ b/display/thanks.go @@ -25,4 +25,13 @@ Thank you to the following contributors, who have helped make Amfora great. FOSS * Himanshu (@singalhimanshu) * @regr4 * Anas Mohamed (@amohamed11) +* David Jimenez (@dvejmz) +* Michael McDonagh (@m-mcdonagh) +* mooff (@awfulcooking) +* Josias (@justjosias) +* mntn (@mntn-xyz) +* Maxime Bouillot (@Arkaeriit) +* Emily (@emily-is-my-username) +* Autumn! (@autumnull) +* William Rehwinkel (@FiskFan1999) `) diff --git a/display/util.go b/display/util.go index f91f2a8..550cdff 100644 --- a/display/util.go +++ b/display/util.go @@ -6,9 +6,7 @@ import ( "strings" "code.rocketnine.space/tslocum/cview" - "github.com/makeworld-the-better-one/go-gemini" "github.com/spf13/viper" - "golang.org/x/text/unicode/norm" ) // This file contains funcs that are small, self-contained utilities. @@ -34,12 +32,6 @@ func makeContentLayout(tv *cview.TextView, leftMargin int) *cview.Flex { return vert } -// makeTabLabel takes a string and adds spacing to it, making it -// suitable for display as a tab label. -func makeTabLabel(s string) string { - return " " + s + " " -} - // tabNumber gets the index of the tab in the tabs slice. It returns -1 // if the tab is not in that slice. func tabNumber(t *tab) int { @@ -63,7 +55,14 @@ func isValidTab(t *tab) bool { } func leftMargin() int { - return int(float64(termW) * viper.GetFloat64("a-general.left_margin")) + // Return the left margin size that centers the text, assuming it's the max width + // https://github.com/makeworld-the-better-one/amfora/issues/233 + + lm := (termW - viper.GetInt("a-general.max_width")) / 2 + if lm < 0 { + return 0 + } + return lm } func textWidth() int { @@ -73,13 +72,11 @@ func textWidth() int { return viper.GetInt("a-general.max_width") } - rightMargin := leftMargin() - if leftMargin() > 10 { - // 10 is the max right margin - rightMargin = 10 - } + // Subtract left and right margin from total width to get text width + // Left and right margin are equal because text is automatically centered, see: + // https://github.com/makeworld-the-better-one/amfora/issues/233 - max := termW - leftMargin() - rightMargin + max := termW - leftMargin()*2 if max < viper.GetInt("a-general.max_width") { return max } @@ -101,81 +98,3 @@ func resolveRelLink(t *tab, prev, next string) (string, error) { } return prevParsed.ResolveReference(nextParsed).String(), nil } - -// normalizeURL attempts to make URLs that are different strings -// but point to the same place all look the same. -// -// Example: gemini://gus.guru:1965/ and //gus.guru/. -// This function will take both output the same URL each time. -// -// It will also percent-encode invalid characters, and decode chars -// that don't need to be encoded. It will also apply Unicode NFC -// normalization. -// -// The string passed must already be confirmed to be a URL. -// Detection of a search string vs. a URL must happen elsewhere. -// -// It only works with absolute URLs. -func normalizeURL(u string) string { - u = norm.NFC.String(u) - - tmp, err := gemini.GetPunycodeURL(u) - if err != nil { - return u - } - u = tmp - parsed, _ := url.Parse(u) - - if parsed.Scheme == "" { - // Always add scheme - parsed.Scheme = "gemini" - } else if parsed.Scheme != "gemini" { - // Not a gemini URL, nothing to do - return u - } - - parsed.User = nil // No passwords in Gemini - parsed.Fragment = "" // No fragments either - if parsed.Port() == "1965" { - // Always remove default port - hostname := parsed.Hostname() - if strings.Contains(hostname, ":") { - parsed.Host = "[" + parsed.Hostname() + "]" - } else { - parsed.Host = parsed.Hostname() - } - } - - // Add slash to the end of a URL with just a domain - // gemini://example.com -> gemini://example.com/ - if parsed.Path == "" { - parsed.Path = "/" - } else { - // Decode and re-encode path - // This removes needless encoding, like that of ASCII chars - // And encodes anything that wasn't but should've been - parsed.RawPath = strings.ReplaceAll(url.PathEscape(parsed.Path), "%2F", "/") - } - - // Do the same to the query string - un, err := gemini.QueryUnescape(parsed.RawQuery) - if err == nil { - parsed.RawQuery = gemini.QueryEscape(un) - } - - return parsed.String() -} - -// fixUserURL will take a user-typed URL and add a gemini scheme to it if -// necessary. It is not the same as normalizeURL, and that func should still -// be used, afterward. -// -// For example "example.com" will become "gemini://example.com", but -// "//example.com" will be left untouched. -func fixUserURL(u string) string { - if !strings.HasPrefix(u, "//") && !strings.HasPrefix(u, "gemini://") && !strings.Contains(u, "://") { - // Assume it's a Gemini URL - u = "gemini://" + u - } - return u -} diff --git a/go.mod b/go.mod index 80a7f76..980420c 100644 --- a/go.mod +++ b/go.mod @@ -1,29 +1,58 @@ module github.com/makeworld-the-better-one/amfora -go 1.14 +go 1.19 require ( - code.rocketnine.space/tslocum/cview v1.5.6-0.20210525194531-92dca67ac283 + code.rocketnine.space/tslocum/cview v1.5.6-0.20210530175404-7e8817f20bdc + github.com/alecthomas/chroma v0.10.0 github.com/atotto/clipboard v0.1.4 - github.com/dustin/go-humanize v1.0.0 - github.com/fsnotify/fsnotify v1.4.9 // indirect - github.com/gdamore/tcell/v2 v2.3.3 - github.com/google/go-cmp v0.5.0 // indirect - github.com/makeworld-the-better-one/go-gemini v0.11.0 + github.com/dustin/go-humanize v1.0.1 + github.com/gdamore/tcell/v2 v2.6.0 + github.com/makeworld-the-better-one/go-gemini v0.13.1 + github.com/makeworld-the-better-one/go-gemini-socks5 v1.0.0 + github.com/makeworld-the-better-one/rr v1.0.0 github.com/mitchellh/go-homedir v1.1.0 - github.com/mitchellh/mapstructure v1.3.1 // indirect - github.com/mmcdole/gofeed v1.1.2 - github.com/pelletier/go-toml v1.8.0 // indirect - github.com/rkoesters/xdg v0.0.0-20181125232953-edd15b846f9b - github.com/schollz/progressbar/v3 v3.8.0 - github.com/spf13/afero v1.2.2 // indirect - github.com/spf13/cast v1.3.1 // indirect + github.com/mmcdole/gofeed v1.2.1 + github.com/muesli/termenv v0.15.2 + github.com/rkoesters/xdg v0.0.1 + github.com/schollz/progressbar/v3 v3.13.1 + github.com/spf13/viper v1.16.0 + github.com/stretchr/testify v1.8.4 + golang.org/x/text v0.13.0 +) + +require ( + code.rocketnine.space/tslocum/cbind v0.1.5 // indirect + github.com/PuerkitoBio/goquery v1.8.0 // indirect + github.com/andybalholm/cascadia v1.3.1 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dlclark/regexp2 v1.4.0 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/gdamore/encoding v1.0.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-isatty v0.0.18 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mmcdole/goxpp v1.1.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rivo/uniseg v0.4.3 // indirect + github.com/spf13/afero v1.9.5 // indirect + github.com/spf13/cast v1.5.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.7.1 - github.com/stretchr/testify v1.6.1 - golang.org/x/text v0.3.6 - gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect - gopkg.in/ini.v1 v1.62.0 // indirect - gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect + github.com/subosito/gotenv v1.4.2 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/term v0.13.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 8655c1f..c2c72f0 100644 --- a/go.sum +++ b/go.sum @@ -3,257 +3,269 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= code.rocketnine.space/tslocum/cbind v0.1.5 h1:i6NkeLLNPNMS4NWNi3302Ay3zSU6MrqOT+yJskiodxE= code.rocketnine.space/tslocum/cbind v0.1.5/go.mod h1:LtfqJTzM7qhg88nAvNhx+VnTjZ0SXBJtxBObbfBWo/M= -code.rocketnine.space/tslocum/cview v1.5.6-0.20210525194531-92dca67ac283 h1:5KBGXdQdfV09eYXOZuFTxqDujndqtRraXj+lyFcxlPk= -code.rocketnine.space/tslocum/cview v1.5.6-0.20210525194531-92dca67ac283/go.mod h1:KBRxzIsj8bfgFpnMpkGVoxsrPUvnQsRnX29XJ2yzB6M= +code.rocketnine.space/tslocum/cview v1.5.6-0.20210530175404-7e8817f20bdc h1:nAcBp7ZCWHpa8fHpynCbULDTAZgPQv28+Z+QnhnFG7E= +code.rocketnine.space/tslocum/cview v1.5.6-0.20210530175404-7e8817f20bdc/go.mod h1:KBRxzIsj8bfgFpnMpkGVoxsrPUvnQsRnX29XJ2yzB6M= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE= -github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo= -github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= +github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= +github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= +github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= +github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= +github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -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/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 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/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -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/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 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/v2 v2.2.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU= -github.com/gdamore/tcell/v2 v2.3.3 h1:RKoI6OcqYrr/Do8yHZklecdGzDTJH9ACKdfECbRdw3M= github.com/gdamore/tcell/v2 v2.3.3/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gdamore/tcell/v2 v2.6.0 h1:OKbluoP9VYmJwZwq/iLb4BxwKcwGthaa1YNBJIyCySg= +github.com/gdamore/tcell/v2 v2.6.0/go.mod h1:be9omFATkdr0D9qewWW3d+MEvl5dha+Etb5y65J2H8Y= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 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/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 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= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/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.11.0 h1:MNGiULJFvcqls9oCy40tE897hDeKvNmEK9i5kRucgQk= -github.com/makeworld-the-better-one/go-gemini v0.11.0/go.mod h1:F+3x+R1xeYK90jMtBq+U+8Sh64r2dHleDZ/en3YgSmg= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/makeworld-the-better-one/go-gemini v0.13.1 h1:qStBcQhgE29ViPCwCAyW65ibqeIEeyUV8TSp8hHJRkU= +github.com/makeworld-the-better-one/go-gemini v0.13.1/go.mod h1:SL62NFyZi6zcjtGwBc1euN1S3x/MHgcYdA/Ninrnwmo= +github.com/makeworld-the-better-one/go-gemini-socks5 v1.0.0 h1:D2o1rIfP/KOxcL3m3rzo4cfWNqfcGaMIhnU0keJc1+o= +github.com/makeworld-the-better-one/go-gemini-socks5 v1.0.0/go.mod h1:mfPK9BfBAAyLKuxPEbZi8mgrGmVlzMKVTGElVspuVR8= +github.com/makeworld-the-better-one/rr v1.0.0 h1:NclI3Z32Q/+kNzP8OOlpPFuYeN0BFGgKU0MLd9ZmfQQ= +github.com/makeworld-the-better-one/rr v1.0.0/go.mod h1:sd3i5WAdkx/7ALu3V6AbVUyDw8uqmDQv55LgHta0f7g= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -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/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= -github.com/mmcdole/gofeed v1.1.2 h1:7I5su6dO5/Rg2LEKS5ofPISVbi2vfxO2SNVSA/QN1y4= -github.com/mmcdole/gofeed v1.1.2/go.mod h1:QQO3maftbOu+hiVOGOZDRLymqGQCos4zxbA4j89gMrE= -github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf h1:sWGE2v+hO0Nd4yFU/S/mDBM5plIU8v/Qhfz41hkDIAI= -github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf/go.mod h1:pasqhqstspkosTneA62Nc+2p9SOBBYAPbnmRRWPQ0V8= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mmcdole/gofeed v1.2.1 h1:tPbFN+mfOLcM1kDF1x2c/N68ChbdBatkppdzf/vDe1s= +github.com/mmcdole/gofeed v1.2.1/go.mod h1:2wVInNpgmC85q16QTTuwbuKxtKkHLCDDtf0dCmnrNr4= +github.com/mmcdole/goxpp v1.1.0 h1:WwslZNF7KNAXTFuzRtn/OKZxFLJAAyOA9w82mDz2ZGI= +github.com/mmcdole/goxpp v1.1.0/go.mod h1:v+25+lT2ViuQ7mVxcncQ8ch1URund48oH+jhjiwEgS8= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -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/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= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rkoesters/xdg v0.0.0-20181125232953-edd15b846f9b h1:8NiY6v9/IlFU8osj1L7kqzRbrG6e3izRQQjGze1Q1R0= -github.com/rkoesters/xdg v0.0.0-20181125232953-edd15b846f9b/go.mod h1:T1HolqzmdHnJIH6p7A9LDuvYGQgEHx9ijX3vKgDKU60= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= +github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rkoesters/xdg v0.0.1 h1:RmfYxghVvIsb4d51u5LtNOcwqY5r3P44u6o86qqvBMA= +github.com/rkoesters/xdg v0.0.1/go.mod h1:5DcbjvJkY00fIOKkaBnylbC/rmc1NNJP5dmUcnlcm7U= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/schollz/progressbar/v3 v3.8.0 h1:BKyefEMgFBDbo+JaeqHcm/9QdSj8qG8sUY+6UppGpnw= -github.com/schollz/progressbar/v3 v3.8.0/go.mod h1:Y9mmL2knZj3LUaBDyBEzFdPrymIr08hnlFMZmfxwbx4= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= -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/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/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/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/schollz/progressbar/v3 v3.13.1 h1:o8rySDYiQ59Mwzy2FELeHY5ZARXZTVJC7iHD6PEFUiE= +github.com/schollz/progressbar/v3 v3.13.1/go.mod h1:xvrbki8kfT1fzWzBT/UZd9L6GA+jdL7HAgq2RFnO6fQ= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= +github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= 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/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.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= -github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= +github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= 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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= +github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= -golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -263,17 +275,23 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -282,24 +300,54 @@ golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201216054612-986b41b23924 h1:QsnDpLLOKwHBBDa8nDws4DYNc/ryVW2vCpxCs09d4PY= -golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -307,58 +355,145 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w 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/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210223095934-7937bea0104d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea h1:+WiDlPBBaO+h9vPNZi8uJ3k4BkKQB7Iow3aqwHVA5hI= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w= golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 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/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -368,30 +503,77 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 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= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= -gopkg.in/ini.v1 v1.62.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/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/logger/logger.go b/logger/logger.go index 6ef41b4..6160e48 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -3,18 +3,42 @@ package logger // For debugging import ( + "io" + "io/ioutil" "log" "os" ) -var Log *log.Logger +var Logger *log.Logger -func Init() error { - f, err := os.Create("debug.log") - if err != nil { - return err +func GetLogger() (*log.Logger, error) { + if Logger != nil { + return Logger, nil } - Log = log.New(f, "", log.LstdFlags) - Log.Println("Started Log") - return nil + + var writer io.Writer + var err error + + debugModeEnabled := os.Getenv("AMFORA_DEBUG") == "1" + if debugModeEnabled { + writer, err = os.Create("debug.log") + if err != nil { + return nil, err + } + } else { + // Suppress all logging output if debug mode is disabled + writer = ioutil.Discard + } + + Logger = log.New(writer, "", log.LstdFlags) + + if !debugModeEnabled { + // Clear all flags to skip log output formatting step to increase + // performance somewhat if we're not logging anything + Logger.SetFlags(0) + } + + Logger.Println("Started logger") + + return Logger, nil } diff --git a/renderer/renderer.go b/renderer/renderer.go index 911f36e..c1b4eb7 100644 --- a/renderer/renderer.go +++ b/renderer/renderer.go @@ -5,6 +5,7 @@ package renderer import ( + "bytes" "fmt" urlPkg "net/url" "regexp" @@ -12,13 +13,25 @@ import ( "strings" "code.rocketnine.space/tslocum/cview" + "github.com/alecthomas/chroma/formatters" + "github.com/alecthomas/chroma/lexers" + "github.com/alecthomas/chroma/styles" "github.com/makeworld-the-better-one/amfora/config" "github.com/spf13/viper" ) +// Terminal color information, set during display initialization by display/display.go +var TermColor string + // Regex for identifying ANSI color codes var ansiRegex = regexp.MustCompile(`\x1b\[[0-9;]*m`) +// Regex for identifying possible language string, based on RFC 6838 and lexers used by Chroma +var langRegex = regexp.MustCompile(`^([a-zA-Z0-9]+/)?[a-zA-Z0-9]+([a-zA-Z0-9!_\#\$\&\-\^\.\+]+)*`) + +// Regex for removing trailing newline (without disturbing ANSI codes) from code formatted with Chroma +var trailingNewline = regexp.MustCompile(`(\r?\n)(?:\x1b\[[0-9;]*m)*$`) + // RenderANSI renders plain text pages containing ANSI codes. // Practically, it is used for the text/x-ansi. func RenderANSI(s string) string { @@ -45,6 +58,10 @@ func RenderPlainText(s string) string { // // Set includeFirst to true if the prefix and suffix should be applied to the first wrapped line as well func wrapLine(line string, width int, prefix, suffix string, includeFirst bool) []string { + if width < 1 { + width = 1 + } + // Anonymous function to allow recovery from potential WordWrap panic var ret []string func() { @@ -159,6 +176,14 @@ func convertRegularGemini(s string, numLinks, width int, proxied bool) (string, spacing = " " } + // Underline non-gemini links if enabled + var linkTag string + if viper.GetBool("a-general.underline") { + linkTag = `[` + config.GetColorString("foreign_link") + `::u]` + } else { + linkTag = `[` + config.GetColorString("foreign_link") + `]` + } + // Wrap and add link text // Wrap the link text, but add some spaces to indent the wrapped lines past the link number // Set the style tags @@ -166,15 +191,16 @@ func convertRegularGemini(s string, numLinks, width int, proxied bool) (string, var wrappedLink []string - if viper.GetBool("a-general.color") { - pU, err := urlPkg.Parse(url) - if !proxied && err == nil && - (pU.Scheme == "" || pU.Scheme == "gemini" || pU.Scheme == "about") { - // A gemini link + pU, err := urlPkg.Parse(url) + if !proxied && err == nil && + (pU.Scheme == "" || pU.Scheme == "gemini" || pU.Scheme == "about") { + // A gemini link + + if viper.GetBool("a-general.color") { // Add the link text in blue (in a region), and a gray link number to the left of it // Those are the default colors, anyway - wrappedLink = wrapLine(linkText, width, + wrappedLink = wrapLine(linkText, width-indent, strings.Repeat(" ", indent)+ `["`+strconv.Itoa(num-1)+`"][`+config.GetColorString("amfora_link")+`]`, `[-][""]`, @@ -187,33 +213,50 @@ func convertRegularGemini(s string, numLinks, width int, proxied bool) (string, `["` + strconv.Itoa(num-1) + `"][` + config.GetColorString("amfora_link") + `]` + wrappedLink[0] + `[-][""]` } else { - // Not a gemini link + // No color - wrappedLink = wrapLine(linkText, width, + wrappedLink = wrapLine(linkText, width-indent, + strings.Repeat(" ", indent)+ // +4 for spaces and brackets + `["`+strconv.Itoa(num-1)+`"]`, + `[""]`, + false, // Don't indent the first line, it's the one with link number + ) + + wrappedLink[0] = `[::b][` + strconv.Itoa(num) + "[][::-] " + + `["` + strconv.Itoa(num-1) + `"]` + + wrappedLink[0] + `[""]` + } + } else { + // Not a gemini link + + if viper.GetBool("a-general.color") { + // Color + + wrappedLink = wrapLine(linkText, width-indent, strings.Repeat(" ", indent)+ - `["`+strconv.Itoa(num-1)+`"][`+config.GetColorString("foreign_link")+`]`, - `[-][""]`, + `["`+strconv.Itoa(num-1)+`"]`+linkTag, + `[-::-][""]`, false, // Don't indent the first line, it's the one with link number ) wrappedLink[0] = fmt.Sprintf(`[%s::b][`, config.GetColorString("link_number")) + - strconv.Itoa(num) + "[]" + "[-::-]" + spacing + - `["` + strconv.Itoa(num-1) + `"][` + config.GetColorString("foreign_link") + `]` + - wrappedLink[0] + `[-][""]` + strconv.Itoa(num) + "[][-::-]" + spacing + + `["` + strconv.Itoa(num-1) + `"]` + linkTag + + wrappedLink[0] + `[-::-][""]` + } else { + // No color + + wrappedLink = wrapLine(linkText, width-indent, + strings.Repeat(" ", indent)+ + `["`+strconv.Itoa(num-1)+`"]`, + `[::-][""]`, + false, // Don't indent the first line, it's the one with link number + ) + + wrappedLink[0] = `[::b][` + strconv.Itoa(num) + "[][::-]" + spacing + + `["` + strconv.Itoa(num-1) + `"]` + + wrappedLink[0] + `[::-][""]` } - } else { - // No colors allowed - - wrappedLink = wrapLine(linkText, width, - strings.Repeat(" ", len(strconv.Itoa(num))+4)+ // +4 for spaces and brackets - `["`+strconv.Itoa(num-1)+`"]`, - `[""]`, - false, // Don't indent the first line, it's the one with link number - ) - - wrappedLink[0] = `[::b][` + strconv.Itoa(num) + "[][::-] " + - `["` + strconv.Itoa(num-1) + `"]` + - wrappedLink[0] + `[""]` } wrappedLines = append(wrappedLines, wrappedLink...) @@ -222,7 +265,8 @@ func convertRegularGemini(s string, numLinks, width int, proxied bool) (string, } else if strings.HasPrefix(lines[i], "* ") { if viper.GetBool("a-general.bullets") { // Wrap list item, and indent wrapped lines past the bullet - wrappedItem := wrapLine(lines[i][1:], width, + wrappedItem := wrapLine(lines[i][1:], + width-4, // Subtract the 4 indent spaces fmt.Sprintf(" [%s]", config.GetColorString("list_text")), "[-]", false) // Add bullet @@ -230,7 +274,8 @@ func convertRegularGemini(s string, numLinks, width int, proxied bool) (string, wrappedItem[0] + "[-]" wrappedLines = append(wrappedLines, wrappedItem...) } else { - wrappedItem := wrapLine(lines[i][1:], width, + wrappedItem := wrapLine(lines[i][1:], + width-4, // Subtract the 4 indent spaces fmt.Sprintf(" [%s]", config.GetColorString("list_text")), "[-]", false) // Add "*" @@ -251,7 +296,9 @@ func convertRegularGemini(s string, numLinks, width int, proxied bool) (string, lines[i] = strings.TrimPrefix(lines[i], ">") lines[i] = strings.TrimPrefix(lines[i], " ") wrappedLines = append(wrappedLines, - wrapLine(lines[i], width, fmt.Sprintf("[%s::i]> ", config.GetColorString("quote_text")), + wrapLine(lines[i], + width-2, // Subtract 2 for width of prefix string + fmt.Sprintf("[%s::i]> ", config.GetColorString("quote_text")), "[-::-]", true)..., ) } @@ -289,11 +336,46 @@ func RenderGemini(s string, width int, proxied bool) (string, []string) { pre := false buf := "" // Block of regular or preformatted lines + // Language, formatter, and style for syntax highlighting + lang := "" + formatterName := TermColor + styleName := viper.GetString("a-general.highlight_style") + // processPre is for rendering preformatted blocks processPre := func() { + syntaxHighlighted := false + + // Perform syntax highlighting if language is set + if lang != "" { + style := styles.Get(styleName) + if style == nil { + style = styles.Fallback + } + formatter := formatters.Get(formatterName) + if formatter == nil { + formatter = formatters.Fallback + } + lexer := lexers.Get(lang) + if lexer == nil { + lexer = lexers.Fallback + } + + // Tokenize and format the text after stripping ANSI codes, replacing buffer if there are no errors + iterator, err := lexer.Tokenise(nil, ansiRegex.ReplaceAllString(buf, "")) + if err == nil { + formattedBuffer := new(bytes.Buffer) + if formatter.Format(formattedBuffer, style, iterator) == nil { + // Strip extra newline added by Chroma and replace buffer + buf = string(trailingNewline.ReplaceAll(formattedBuffer.Bytes(), []byte{})) + } + syntaxHighlighted = true + } + } + // Support ANSI color codes in preformatted blocks - see #59 - if viper.GetBool("a-general.color") && viper.GetBool("a-general.ansi") { + // This will also execute if code highlighting was successful for this block + if viper.GetBool("a-general.color") && (viper.GetBool("a-general.ansi") || syntaxHighlighted) { buf = cview.TranslateANSI(buf) // The TranslateANSI function will reset the colors when it encounters // an ANSI reset code, injecting a full reset tag: [-:-:-] @@ -315,8 +397,12 @@ func RenderGemini(s string, width int, proxied bool) (string, []string) { // Lines are modified below to always end with \r\n buf = strings.TrimSuffix(buf, "\r\n") - rendered += fmt.Sprintf("[%s]", config.GetColorString("preformatted_text")) + - buf + fmt.Sprintf("[%s:%s:-]\r\n", config.GetColorString("regular_text"), config.GetColorString("bg")) + if viper.GetBool("a-general.color") { + rendered += fmt.Sprintf("[%s]", config.GetColorString("preformatted_text")) + + buf + fmt.Sprintf("[%s:%s:-]\r\n", config.GetColorString("regular_text"), config.GetColorString("bg")) + } else { + rendered += buf + "\r\n" + } } // processRegular processes non-preformatted sections @@ -336,9 +422,21 @@ func RenderGemini(s string, width int, proxied bool) (string, []string) { // Don't add the current line with backticks processPre() + // Clear the language + lang = "" } else { // Not preformatted, regular text processRegular() + + if viper.GetBool("a-general.highlight_code") { + // Check for alt text indicating a language that Chroma can highlight + alt := strings.TrimSpace(strings.TrimPrefix(lines[i], "```")) + if matches := langRegex.FindStringSubmatch(alt); matches != nil { + if lexers.Get(matches[0]) != nil { + lang = matches[0] + } + } + } } buf = "" // Clear buffer for next block pre = !pre diff --git a/rr/README.md b/rr/README.md deleted file mode 100644 index a16cd29..0000000 --- a/rr/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# package `rr`, aka `RestartReader` - -This package exists just to hold the `RestartReader` type. It wraps `io.ReadCloser` and implements it. It holds the data from every `Read` in a `[]byte` buffer, and allows you to call `.Restart()`, causing subsequent `Read` calls to start from the beginning again. - -See [#140](https://github.com/makeworld-the-better-one/amfora/issues/140) for why this was needed. - -Other projects are encouraged to copy this code if it's useful to them, and this package may move out of Amfora if I end up using it in multiple projects. - -## License - -If you prefer, you can consider the code in this package, and this package only, to be licensed under the MIT license instead. So the code in this package is dual-licensed. You can use the LICENSE file in the root of this repo, or the license text below. - -
-Click to see MIT license terms - -``` -Copyright (c) 2020 makeworld - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -``` -
diff --git a/rr/rr.go b/rr/rr.go deleted file mode 100644 index ffd8a8a..0000000 --- a/rr/rr.go +++ /dev/null @@ -1,81 +0,0 @@ -package rr - -import ( - "errors" - "io" -) - -var ErrClosed = errors.New("RestartReader: closed") - -type RestartReader struct { - r io.ReadCloser - buf []byte - - // Where in the buffer we are. If it's equal to len(buf) then the reader - // should be used. - i int64 -} - -func (rr *RestartReader) Read(p []byte) (n int, err error) { - if rr.buf == nil { - return 0, ErrClosed - } - - if rr.i >= int64(len(rr.buf)) { - // Read new data - tmp := make([]byte, len(p)) - n, err = rr.r.Read(tmp) - if n > 0 { - rr.buf = append(rr.buf, tmp[:n]...) - copy(p, tmp[:n]) - } - rr.i = int64(len(rr.buf)) - return - } - - // Reading from buffer - - bufSize := len(rr.buf[rr.i:]) - - if len(p) > bufSize { - // It wants more data then what's in the buffer - tmp := make([]byte, len(p)-bufSize) - n, err = rr.r.Read(tmp) - if n > 0 { - rr.buf = append(rr.buf, tmp[:n]...) - } - copy(p, rr.buf[rr.i:]) - n += bufSize - rr.i = int64(len(rr.buf)) - return - } - // All the required data is in the buffer - end := rr.i + int64(len(p)) - copy(p, rr.buf[rr.i:end]) - rr.i = end - n = len(p) - err = nil - return -} - -// Restart causes subsequent Read calls to read from the beginning, instead -// of where they left off. -func (rr *RestartReader) Restart() { - rr.i = 0 -} - -// Close clears the buffer and closes the underlying io.ReadCloser, returning -// its error. -func (rr *RestartReader) Close() error { - rr.buf = nil - return rr.r.Close() -} - -// NewRestartReader creates and initializes a new RestartReader that reads from -// the provided io.ReadCloser. -func NewRestartReader(r io.ReadCloser) *RestartReader { - return &RestartReader{ - r: r, - buf: make([]byte, 0), - } -} diff --git a/rr/rr_test.go b/rr/rr_test.go deleted file mode 100644 index fb16f3c..0000000 --- a/rr/rr_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package rr - -import ( - "io/ioutil" - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -var r1 *RestartReader - -func reset() { - r1 = NewRestartReader(ioutil.NopCloser(strings.NewReader("1234567890"))) -} - -func TestRead(t *testing.T) { - reset() - p := make([]byte, 1) - n, err := r1.Read(p) - assert.Equal(t, 1, n, "should read one byte") - assert.Equal(t, nil, err, "should be no error") - assert.Equal(t, []byte{'1'}, p, "should have read one byte, '1'") -} - -//nolint -func TestRestart(t *testing.T) { - reset() - p := make([]byte, 4) - r1.Read(p) - - r1.Restart() - p = make([]byte, 5) - n, err := r1.Read(p) - assert.Equal(t, []byte("12345"), p, "should read the first 5 bytes again") - assert.Equal(t, 5, n, "should have read 4 bytes") - assert.Equal(t, nil, err, "err should be nil") - - r1.Restart() - p = make([]byte, 4) - n, err = r1.Read(p) - assert.Equal(t, []byte("1234"), p, "should read the first 4 bytes again") - assert.Equal(t, 4, n, "should have read 4 bytes") - assert.Equal(t, nil, err, "err should be nil") -} diff --git a/subscriptions/subscriptions.go b/subscriptions/subscriptions.go index ecee735..ba99ddf 100644 --- a/subscriptions/subscriptions.go +++ b/subscriptions/subscriptions.go @@ -251,7 +251,9 @@ func getResource(url string) (string, *gemini.Response, error) { return url, nil, err } - if res.Status == gemini.StatusSuccess { + status := gemini.CleanStatus(res.Status) + + if status == gemini.StatusSuccess { // No redirects return url, res, nil } @@ -266,8 +268,8 @@ func getResource(url string) (string, *gemini.Response, error) { urls := make([]*urlPkg.URL, 0) // Loop through redirects - for (res.Status == gemini.StatusRedirectPermanent || res.Status == gemini.StatusRedirectTemporary) && i < 5 { - redirs = append(redirs, res.Status) + for (status == gemini.StatusRedirectPermanent || status == gemini.StatusRedirectTemporary) && i < 5 { + redirs = append(redirs, status) urls = append(urls, parsed) tmp, err := parsed.Parse(res.Meta) @@ -302,7 +304,7 @@ func getResource(url string) (string, *gemini.Response, error) { if i < 5 { // The server stopped redirecting after <5 redirects - if res.Status == gemini.StatusSuccess { + if status == gemini.StatusSuccess { // It ended by succeeding for j := range redirs { diff --git a/sysopen/open_browser_darwin.go b/sysopen/open_browser_darwin.go index 49bd171..1355986 100644 --- a/sysopen/open_browser_darwin.go +++ b/sysopen/open_browser_darwin.go @@ -1,3 +1,4 @@ +//go:build darwin // +build darwin package sysopen @@ -6,9 +7,12 @@ import "os/exec" // Open opens `path` in default system viewer. func Open(path string) (string, error) { - err := exec.Command("open", path).Start() + proc := exec.Command("open", path) + err := proc.Start() if err != nil { return "", err } + //nolint:errcheck + go proc.Wait() // Prevent zombies, see #219 return "Opened in default system viewer", nil } diff --git a/sysopen/open_browser_other.go b/sysopen/open_browser_other.go index 7644a0e..ff4a709 100644 --- a/sysopen/open_browser_other.go +++ b/sysopen/open_browser_other.go @@ -1,3 +1,4 @@ +//go:build !linux && !darwin && !windows && !freebsd && !netbsd && !openbsd // +build !linux,!darwin,!windows,!freebsd,!netbsd,!openbsd package sysopen @@ -7,5 +8,5 @@ import "fmt" // Open opens `path` in default system viewer, but not on this OS. func Open(path string) (string, error) { return "", fmt.Errorf("unsupported OS for default system viewer. " + - "Set a catch-all [[mediatype-handlers]] command in the config") + "Set a catch-all command in the config") } diff --git a/sysopen/open_browser_unix.go b/sysopen/open_browser_unix.go index 88a215b..799709f 100644 --- a/sysopen/open_browser_unix.go +++ b/sysopen/open_browser_unix.go @@ -1,3 +1,4 @@ +//go:build linux || freebsd || netbsd || openbsd // +build linux freebsd netbsd openbsd //nolint:goerr113 @@ -20,16 +21,19 @@ func Open(path string) (string, error) { switch { case xorgDisplay == "" && waylandDisplay == "": return "", fmt.Errorf("no display server was found. " + - "You may set a default [[mediatype-handlers]] command in the config") + "You may set a default command in the config") case xdgOpenNotFoundErr == nil: // Use start rather than run or output in order // to make application run in background. - if err := exec.Command(xdgOpenPath, path).Start(); err != nil { + proc := exec.Command(xdgOpenPath, path) + if err := proc.Start(); err != nil { return "", err } + //nolint:errcheck + go proc.Wait() // Prevent zombies, see #219 return "Opened in default system viewer", nil default: return "", fmt.Errorf("could not determine default system viewer. " + - "Set a catch-all [[mediatype-handlers]] command in the config") + "Set a catch-all command in the config") } } diff --git a/sysopen/open_browser_windows.go b/sysopen/open_browser_windows.go index 4924ea8..815594d 100644 --- a/sysopen/open_browser_windows.go +++ b/sysopen/open_browser_windows.go @@ -1,3 +1,4 @@ +//go:build windows && (!linux || !darwin || !freebsd || !netbsd || !openbsd) // +build windows // +build !linux !darwin !freebsd !netbsd !openbsd @@ -7,9 +8,12 @@ import "os/exec" // Open opens `path` in default system vierwer. func Open(path string) (string, error) { - err := exec.Command("rundll32", "url.dll,FileProtocolHandler", path).Start() + proc := exec.Command("rundll32", "url.dll,FileProtocolHandler", path) + err := proc.Start() if err != nil { return "", err } + //nolint:errcheck + go proc.Wait() // Prevent zombies, see #219 return "Opened in default system viewer", nil } diff --git a/webbrowser/open_browser_darwin.go b/webbrowser/open_browser_darwin.go index 44c522e..38e00a7 100644 --- a/webbrowser/open_browser_darwin.go +++ b/webbrowser/open_browser_darwin.go @@ -1,3 +1,4 @@ +//go:build darwin // +build darwin package webbrowser @@ -6,9 +7,12 @@ import "os/exec" // Open opens `url` in default system browser. func Open(url string) (string, error) { - err := exec.Command("open", url).Start() + proc := exec.Command("open", url) + err := proc.Start() if err != nil { return "", err } + //nolint:errcheck + go proc.Wait() // Prevent zombies, see #219 return "Opened in system default web browser", nil } diff --git a/webbrowser/open_browser_other.go b/webbrowser/open_browser_other.go index 9501034..580e2de 100644 --- a/webbrowser/open_browser_other.go +++ b/webbrowser/open_browser_other.go @@ -1,3 +1,4 @@ +//go:build !linux && !darwin && !windows && !freebsd && !netbsd && !openbsd // +build !linux,!darwin,!windows,!freebsd,!netbsd,!openbsd package webbrowser diff --git a/webbrowser/open_browser_unix.go b/webbrowser/open_browser_unix.go index 4d265be..00d5af1 100644 --- a/webbrowser/open_browser_unix.go +++ b/webbrowser/open_browser_unix.go @@ -1,3 +1,4 @@ +//go:build linux || freebsd || netbsd || openbsd // +build linux freebsd netbsd openbsd //nolint:goerr113 @@ -33,14 +34,20 @@ func Open(url string) (string, error) { case xdgOpenNotFoundErr == nil: // Prefer xdg-open over $BROWSER // Use start rather than run or output in order // to make browser running in background. - if err := exec.Command(xdgOpenPath, url).Start(); err != nil { + proc := exec.Command(xdgOpenPath, url) + if err := proc.Start(); err != nil { return "", err } + //nolint:errcheck + go proc.Wait() // Prevent zombies, see #219 return "Opened in system default web browser", nil case envBrowser != "": - if err := exec.Command(envBrowser, url).Start(); err != nil { + proc := exec.Command(envBrowser, url) + if err := proc.Start(); err != nil { return "", err } + //nolint:errcheck + go proc.Wait() // Prevent zombies, see #219 return "Opened in system default web browser", nil default: return "", fmt.Errorf("could not determine system browser") diff --git a/webbrowser/open_browser_windows.go b/webbrowser/open_browser_windows.go index 66b8c46..3e5d5f8 100644 --- a/webbrowser/open_browser_windows.go +++ b/webbrowser/open_browser_windows.go @@ -1,3 +1,4 @@ +//go:build windows && (!linux || !darwin || !freebsd || !netbsd || !openbsd) // +build windows // +build !linux !darwin !freebsd !netbsd !openbsd @@ -7,9 +8,12 @@ import "os/exec" // Open opens `url` in default system browser. func Open(url string) (string, error) { - err := exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() + proc := exec.Command("rundll32", "url.dll,FileProtocolHandler", url) + err := proc.Start() if err != nil { return "", err } + //nolint:errcheck + go proc.Wait() // Prevent zombies, see #219 return "Opened in system default web browser", nil }