1
0
mirror of https://github.com/makew0rld/amfora.git synced 2024-06-15 19:15:24 +00:00

🔀 Merge branch 'master' into feeds

This commit is contained in:
makeworld 2020-08-28 12:07:08 -04:00
commit 3a63b73300
31 changed files with 385 additions and 157 deletions

15
.github/workflows/golangci-lint.yml vendored Normal file
View 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
View 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
View 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

View File

@ -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

View File

@ -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

View File

@ -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
View File

@ -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
View File

@ -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")
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)

View File

@ -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
`)

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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,
}

View File

@ -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()
}

View File

@ -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),

View File

@ -1,3 +1,4 @@
//nolint
package display
var newTabContent = `# New Tab

View File

@ -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:

View File

@ -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 == "" {

View File

@ -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
View 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
View File

@ -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
View File

@ -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=

View File

@ -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
}

View File

@ -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"))

View File

@ -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])
}

View File

@ -1,5 +1,6 @@
// +build linux freebsd netbsd openbsd
//nolint:goerr113
package webbrowser
import (