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