diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..1b02251
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,30 @@
+on: [push, pull_request]
+name: Test
+
+jobs:
+ test:
+ strategy:
+ matrix:
+ go-version: ['1.13', '1.14', '1.15']
+ os: [ubuntu-latest, macos-latest, windows-latest]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - name: Install Go
+ uses: actions/setup-go@v2
+ with:
+ go-version: ${{ matrix.go-version }}
+ - name: Install make on Windows
+ if: matrix.os == 'windows-latest'
+ run: choco install make
+ - name: Checkout code
+ uses: actions/checkout@v2
+ - uses: actions/cache@v2
+ with:
+ path: ~/go/pkg/mod
+ key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+ restore-keys: |
+ ${{ runner.os }}-go-
+ - name: Test
+ run: |
+ go test -race ./...
+ make
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 6af09e9..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,35 +0,0 @@
-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"
-
-os:
- - linux
- - osx
- - windows
-
-before_install:
- - if [ "$TRAVIS_OS_NAME" = "windows" ]; then choco install make; fi
-
-script:
- - go test -race ./...
- - make
-
-env:
- GO111MODULE=on
-
-cache:
- directories:
- - $GOCACHE
- - $GOPATH/pkg/mod
-
-# TODO: GitHub Releases deploy
-
-notifications:
- email:
- on_success: never
- on_failure: always
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8d4592c..4547b95 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,23 +5,38 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
+### Changed
+- Updated [go-gemini](https://github.com/makeworld-the-better-one/go-gemini) to v0.9.1 to support CN-only wildcard certs
+- Preformatted text is now grey by default
+
+
+## [1.6.0] - 2020-11-04
### Added
+- **Support client certificates** through config (#112)
- `ansi` config setting, to disable ANSI colors in pages (#79, #86)
- Edit current URL with e (#87)
- If `emoji_favicons` is enabled, new bookmarks will have the domain's favicon prepended (#69, #90)
- The `BROWSER` env var is now also checked when opening web links on Unix (#93)
+- More accurate error messages based on server response code
### Changed
- Disabling the `color` config setting also disables ANSI colors in pages (#79, #86)
- Updated [go-isemoji](https://github.com/makeworld-the-better-one/go-isemoji) to v1.1.0 to support Emoji 13.1 for favicons
- The web browser code doesn't check for Xorg anymore, just display variables (#93)
- Bookmarks can be made to non-gemini URLs (#94)
+- Remove pointless directory fallbacks (#101)
+- Don't load page from cache when redirected to it (#114)
### Fixed
- XDG user dir file is parsed instead of looking for XDG env vars (#97, #100)
+- Support paths with spaces in HTTP browser config setting (#77)
+- Clicking "Change" on an existing bookmark without changing the text no longer removes it (#91)
+- Display HTTP Error if "Open In Portal" fails (#81)
+- Support ANSI color codes again, but only in preformatted blocks (#59)
+- Make the `..` command work lke it used to in v1.4.0
-## [v1.5.0] - 2020-09-01
+## [1.5.0] - 2020-09-01
### Added
- **Proxy support** - see the `[proxies]` section in the config (#66, #80)
- **Emoji favicons** can now be seen if `emoji_favicons` is enabled in the config (#62)
diff --git a/README.md b/README.md
index 5c27aff..844ed66 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
Image modified from: amphora by Alvaro Cabrera from the Noun Project
-[![travis build status](https://img.shields.io/travis/com/makeworld-the-better-one/amfora)](https://https://travis-ci.com/github/makeworld-the-better-one/amfora)
+[![travis build status](https://img.shields.io/travis/com/makeworld-the-better-one/amfora/master?label=master)](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)
@@ -42,10 +42,10 @@ Make sure to click "Watch" > "Releases only" in the top right to get notified ab
### Arch Linux
-Arch Linux users can install Amfora from AUR. It has the package name `amfora`, and is maintained by @pboyd.
+Arch Linux users can install Amfora using pacman.
```
-yay -S amfora
+sudo pacman -S amfora
```
### Homebrew
@@ -121,15 +121,15 @@ Features in *italics* are in the master branch, but not in the latest release.
- Disabled by default, enable in config
- [x] Proxying
- Schemes like Gopher or HTTP can be proxied through a Gemini server
+- [x] Client certificate support
+ - [ ] 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
- [x] *Subscribe to RSS and Atom feeds and display them*
- Subscribing to page changes, similar to how Spacewalk works, will also be supported
- [ ] Stream support
-- [ ] 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
- [ ] History browser
@@ -138,10 +138,19 @@ The config file is written in the intuitive [TOML](https://github.com/toml-lang/
On Windows, the file is in `%APPDATA%\amfora\config.toml`, which usually expands to `C:\Users\\AppData\Roaming\amfora\config.toml`.
+## Client Certificates
+
+Amfora has early support for client certs. Eventually Amfora will be able to generate them itself, but for you can do it by using OpenSSL:
+
+```shell
+openssl req -new -subj "/CN=username" -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -days 1825 -nodes -out cert.pem -keyout key.pem
+```
+
+This will create a certificate and key file, that can be renamed and moved as you like. See the configuration section above for how to edit your config file to tell Amfora about them.
+
## Known Bugs
- Pasting on Windows is truncated, the full paste content won't be added. ([#43](https://github.com/makeworld-the-better-one/amfora/issues/43))
-- ANSI codes aren't displaying properly ([#59](https://github.com/makeworld-the-better-one/amfora/issues/59))
You can also check out [all the issues with the bug label](https://github.com/makeworld-the-better-one/amfora/issues?q=is%3Aopen+is%3Aissue+label%3Abug).
diff --git a/THANKS.md b/THANKS.md
new file mode 100644
index 0000000..0e093f4
--- /dev/null
+++ b/THANKS.md
@@ -0,0 +1,13 @@
+# THANKS
+
+Thank you to the following contributors, who have helped make Amfora great. FOSS projects are a community effort, and we would be worse off without you.
+
+- Sotiris Papatheodorou (@sotpapathe)
+- Chloe Kudryavtsev (@CosmicToast)
+- Adrian Hesketh (@a-h)
+- Jansen Price (@sumpygump)
+- Alex Wennerberg (@alexwennerberg)
+- Timur Ismagilov (@bouncepaw)
+- Matt Caroll (@ohiolab)
+- Patryk Niedźwiedziński (@pniedzwiedzinski)
+- Trevor Slocum (@tsclocum)
\ No newline at end of file
diff --git a/amfora.go b/amfora.go
index 25e6887..c63eeca 100644
--- a/amfora.go
+++ b/amfora.go
@@ -10,7 +10,7 @@ import (
)
var (
- version = "1.5.0"
+ version = "v1.6.0"
commit = "unknown"
builtBy = "unknown"
)
diff --git a/client/client.go b/client/client.go
index 4bc1557..2ec65d5 100644
--- a/client/client.go
+++ b/client/client.go
@@ -2,23 +2,74 @@
package client
import (
+ "io/ioutil"
"net"
"net/url"
"github.com/makeworld-the-better-one/go-gemini"
+ "github.com/mitchellh/go-homedir"
+ "github.com/spf13/viper"
)
+var certCache = make(map[string][][]byte)
+
+func clientCert(host string) ([]byte, []byte) {
+ if cert := certCache[host]; cert != nil {
+ return cert[0], cert[1]
+ }
+
+ // Expand paths starting with ~/
+ certPath, err := homedir.Expand(viper.GetString("auth.certs." + host))
+ if err != nil {
+ certPath = viper.GetString("auth.certs." + host)
+ }
+ keyPath, err := homedir.Expand(viper.GetString("auth.keys." + host))
+ if err != nil {
+ keyPath = viper.GetString("auth.keys." + host)
+ }
+ if certPath == "" && keyPath == "" {
+ certCache[host] = [][]byte{nil, nil}
+ return nil, nil
+ }
+
+ cert, err := ioutil.ReadFile(certPath)
+ if err != nil {
+ certCache[host] = [][]byte{nil, nil}
+ return nil, nil
+ }
+ key, err := ioutil.ReadFile(keyPath)
+ if err != nil {
+ certCache[host] = [][]byte{nil, nil}
+ return nil, nil
+ }
+
+ certCache[host] = [][]byte{cert, key}
+ return cert, key
+}
+
+// HasClientCert returns whether or not a client certificate exists for a host.
+func HasClientCert(host string) bool {
+ cert, _ := clientCert(host)
+ return cert != nil
+}
+
// Fetch returns response data and an error.
// The error text is human friendly and should be displayed.
func Fetch(u string) (*gemini.Response, error) {
+ parsed, _ := url.Parse(u)
+ cert, key := clientCert(parsed.Host)
- res, err := gemini.Fetch(u)
+ var res *gemini.Response
+ var err error
+ if cert != nil {
+ res, err = gemini.FetchWithCert(u, cert, key)
+ } else {
+ res, err = gemini.Fetch(u)
+ }
if err != nil {
return nil, err
}
- parsed, _ := url.Parse(u)
-
ok := handleTofu(parsed.Hostname(), parsed.Port(), res.Cert)
if !ok {
return res, ErrTofu
@@ -29,7 +80,16 @@ func Fetch(u string) (*gemini.Response, error) {
// FetchWithProxy is the same as Fetch, but uses a proxy.
func FetchWithProxy(proxyHostname, proxyPort, u string) (*gemini.Response, error) {
- res, err := gemini.FetchWithHost(net.JoinHostPort(proxyHostname, proxyPort), u)
+ parsed, _ := url.Parse(u)
+ cert, key := clientCert(parsed.Host)
+
+ var res *gemini.Response
+ var err error
+ if cert != nil {
+ res, err = gemini.FetchWithHostAndCert(net.JoinHostPort(proxyHostname, proxyPort), u, cert, key)
+ } else {
+ res, err = gemini.FetchWithHost(net.JoinHostPort(proxyHostname, proxyPort), u)
+ }
if err != nil {
return nil, err
}
diff --git a/config/config.go b/config/config.go
index 01746c7..c68a62b 100644
--- a/config/config.go
+++ b/config/config.go
@@ -1,6 +1,7 @@
// Package config initializes all files required for Amfora, even those used by
// other packages. It also reads in the config file and initializes a Viper and
// the theme
+//nolint:golint,goerr113
package config
import (
@@ -40,12 +41,13 @@ var bkmkPath string
var DownloadsDir string
// Feeds
-
var FeedJSON io.ReadCloser
var feedDir string
var FeedPath string
-//nolint:golint,goerr113
+// Command for opening HTTP(S) URLs in the browser, from "a-general.http" in config.
+var HTTPCommand []string
+
func Init() error {
// *** Set paths ***
@@ -69,12 +71,7 @@ func Init() error {
configDir = amforaAppData
} else {
// Unix / POSIX system
- if basedir.ConfigHome == "" {
- // Default to ~/.config/amfora
- configDir = filepath.Join(home, ".config", "amfora")
- } else {
- configDir = filepath.Join(basedir.ConfigHome, "amfora")
- }
+ configDir = filepath.Join(basedir.ConfigHome, "amfora")
}
configPath = filepath.Join(configDir, "config.toml")
@@ -91,12 +88,7 @@ func Init() error {
tofuDBDir = amforaAppData
} else {
// XDG cache dir on POSIX systems
- if basedir.CacheHome == "" {
- // Default to ~/.cache/amfora
- tofuDBDir = filepath.Join(home, ".cache", "amfora")
- } else {
- tofuDBDir = filepath.Join(basedir.CacheHome, "amfora")
- }
+ tofuDBDir = filepath.Join(basedir.CacheHome, "amfora")
}
tofuDBPath = filepath.Join(tofuDBDir, "tofu.toml")
@@ -106,12 +98,7 @@ func Init() error {
bkmkDir = amforaAppData
} else {
// XDG data dir on POSIX systems
- if basedir.DataHome == "" {
- // Default to ~/.local/share/amfora
- bkmkDir = filepath.Join(home, ".local", "share", "amfora")
- } else {
- bkmkDir = filepath.Join(basedir.DataHome, "amfora")
- }
+ bkmkDir = filepath.Join(basedir.DataHome, "amfora")
}
bkmkPath = filepath.Join(bkmkDir, "bookmarks.toml")
@@ -282,5 +269,14 @@ func Init() error {
cview.Styles.PrimitiveBackgroundColor = GetColor("bg")
} // Otherwise it's black by default
+ // Parse HTTP command
+ HTTPCommand = viper.GetStringSlice("a-general.http")
+ if len(HTTPCommand) == 0 {
+ // Not a string array, interpret as a string instead
+ // Split on spaces to maintain compatibility with old versions
+ // The new better way to is to just define a string array in config
+ HTTPCommand = strings.Fields(viper.GetString("a-general.http"))
+ }
+
return nil
}
diff --git a/config/default.go b/config/default.go
index 11cc1ef..6329539 100644
--- a/config/default.go
+++ b/config/default.go
@@ -21,10 +21,20 @@ home = "gemini://gemini.circumlunar.space"
# If set to false, a prompt will be shown before following redirects.
auto_redirect = false
-# What command to run to open a HTTP(S) URL. Set to "default" to try to guess the browser,
-# or set to "off" to not open HTTP(S) URLs.
+# What command to run to open a HTTP(S) URL.
+# Set to "default" to try to guess the browser, or set to "off" to not open HTTP(S) URLs.
# If a command is set, than the URL will be added (in quotes) to the end of the command.
-# A space will be prepended if necessary.
+# A space will be prepended to the URL.
+#
+# The best to define a command is using a string array.
+# Examples:
+# http = ["firefox"]
+# http = ["custom-browser", "--flag", "--option=2"]
+# http = ["/path/with spaces/in it/firefox"]
+#
+# Using just a string will also work, but it is deprecated,
+# and will degrade if you use paths with spaces.
+
http = "default"
# Any URL that will accept a query string can be put here
@@ -33,7 +43,7 @@ search = "gemini://gus.guru/search"
# Whether colors will be used in the terminal
color = true
-# Whether ANSI codes from the page content should be rendered
+# Whether ANSI color codes from the page content should be rendered
ansi = true
# Whether to replace list asterisks with unicode bullets
@@ -59,6 +69,20 @@ page_max_time = 10
emoji_favicons = false
+[auth]
+# Authentication settings
+
+[auth.certs]
+# Client certificates
+# Set domain name equal to path to client cert
+# "example.com" = "mycert.crt"
+
+[auth.keys]
+# Client certificate keys
+# Set domain name equal to path to key for the client cert above
+# "example.com" = "mycert.key"
+
+
[keybindings]
# In the future there will be more settings here.
diff --git a/config/theme.go b/config/theme.go
index 7de75f9..6559864 100644
--- a/config/theme.go
+++ b/config/theme.go
@@ -58,7 +58,7 @@ var theme = map[string]tcell.Color{
"link_number": tcell.ColorSilver,
"regular_text": tcell.ColorWhite,
"quote_text": tcell.ColorWhite,
- "preformatted_text": tcell.ColorWhite,
+ "preformatted_text": tcell.ColorGrey,
"list_text": tcell.ColorWhite,
}
diff --git a/contrib/themes/nord.toml b/contrib/themes/nord.toml
new file mode 100644
index 0000000..ec2bf7b
--- /dev/null
+++ b/contrib/themes/nord.toml
@@ -0,0 +1,111 @@
+[theme]
+# This section is for changing the COLORS used in Amfora.
+# These colors only apply if 'color' is enabled above.
+# Colors can be set using a W3C color name, or a hex value such as "#ffffff".
+
+# Note that not all colors will work on terminals that do not have truecolor support.
+# If you want to stick to the standard 16 or 256 colors, you can get
+# a list of those here: https://jonasjacek.github.io/colors/
+# DO NOT use the names from that site, just the hex codes.
+
+# Definitions:
+# bg = background
+# fg = foreground
+# dl = download
+# btn = button
+# hdg = heading
+# bkmk = bookmark
+# modal = a popup window/box in the middle of the screen
+
+# EXAMPLES:
+# hdg_1 = "green"
+# hdg_2 = "#5f0000"
+
+# Available keys to set:
+
+# bg: background for pages, tab row, app in general
+# tab_num: The number/highlight of the tabs at the top
+# tab_divider: The color of the divider character between tab numbers: |
+# bottombar_label: The color of the prompt that appears when you press space
+# bottombar_text: The color of the text you type
+# bottombar_bg
+bg = "#2e3440"
+fg = "#eceff4"
+tab_num = "#88c0d0"
+tab_divider = "#eceff4"
+bottombar_bg = "#3b4252"
+bottombar_text = "#eceff4"
+bottombar_label = "#88c0d0"
+
+# hdg_1
+# hdg_2
+# hdg_3
+# amfora_link: A link that Amfora supports viewing. For now this is only gemini://
+# foreign_link: HTTP(S), Gopher, etc
+# link_number: The silver number that appears to the left of a link
+# regular_text: Normal gemini text, and plaintext documents
+# quote_text
+# preformatted_text
+# list_text
+hdg_1 = "#5e81ac"
+hdg_2 = "#81a1c1"
+hdg_3 = "#8fbcbb"
+amfora_link = "#88c0d0"
+foreign_link = "#b48ead"
+link_number = "#a3be8c"
+regular_text = "#eceff4"
+quote_text = "#8fbcbb"
+preformatted_text = "#eceff4"
+list_text = "#eceff4"
+
+# btn_bg: The bg color for all modal buttons
+# btn_text: The text color for all modal buttons
+btn_bg = "#4c566a"
+btn_text = "#eceff4"
+
+# dl_choice_modal_bg
+# dl_choice_modal_text
+# dl_modal_bg
+# dl_modal_text
+# info_modal_bg
+# info_modal_text
+# error_modal_bg
+# error_modal_text
+# yesno_modal_bg
+# yesno_modal_text
+# tofu_modal_bg
+# tofu_modal_text
+
+dl_choice_modal_bg = "#3b4252"
+dl_choice_modal_text = "#eceff4"
+dl_modal_bg = "#3b4252"
+dl_modal_text = "#eceff4"
+info_modal_bg = "#3b4252"
+info_modal_text = "#eceff4"
+error_modal_bg = "#bf616a"
+error_modal_text = "#2e3440"
+yesno_modal_bg = "#3b4252"
+yesno_modal_text = "#eceff4"
+tofu_modal_bg = "#3b4252"
+tofu_modal_text = "#eceff4"
+
+# input_modal_bg
+# input_modal_text
+# input_modal_field_bg: The bg of the input field, where you type the text
+# input_modal_field_text: The color of the text you type
+input_modal_bg = "#3b4252"
+input_modal_text = "#eceff4"
+input_modal_field_bg = "#4c566a"
+input_modal_field_text ="#eceff4"
+
+# bkmk_modal_bg
+# bkmk_modal_text
+# bkmk_modal_label
+# bkmk_modal_field_bg
+# bkmk_modal_field_text
+
+bkmk_modal_bg = "#3b4252"
+bkmk_modal_text = "#eceff4"
+bkmk_modal_label = "#88c0d0"
+bkmk_modal_field_bg = "#4c566a"
+bkmk_modal_field_text = "#eceff4"
diff --git a/default-config.toml b/default-config.toml
index 4242bb1..9ac0d21 100644
--- a/default-config.toml
+++ b/default-config.toml
@@ -18,10 +18,20 @@ home = "gemini://gemini.circumlunar.space"
# If set to false, a prompt will be shown before following redirects.
auto_redirect = false
-# What command to run to open a HTTP(S) URL. Set to "default" to try to guess the browser,
-# or set to "off" to not open HTTP(S) URLs.
+# What command to run to open a HTTP(S) URL.
+# Set to "default" to try to guess the browser, or set to "off" to not open HTTP(S) URLs.
# If a command is set, than the URL will be added (in quotes) to the end of the command.
-# A space will be prepended if necessary.
+# A space will be prepended to the URL.
+#
+# The best to define a command is using a string array.
+# Examples:
+# http = ["firefox"]
+# http = ["custom-browser", "--flag", "--option=2"]
+# http = ["/path/with spaces/in it/firefox"]
+#
+# Using just a string will also work, but it is deprecated,
+# and will degrade if you use paths with spaces.
+
http = "default"
# Any URL that will accept a query string can be put here
@@ -30,7 +40,7 @@ search = "gemini://gus.guru/search"
# Whether colors will be used in the terminal
color = true
-# Whether ANSI codes from the page content should be rendered
+# Whether ANSI color codes from the page content should be rendered
ansi = true
# Whether to replace list asterisks with unicode bullets
@@ -56,6 +66,20 @@ page_max_time = 10
emoji_favicons = false
+[auth]
+# Authentication settings
+
+[auth.certs]
+# Client certificates
+# Set domain name equal to path to client cert
+# "example.com" = "mycert.crt"
+
+[auth.keys]
+# Client certificate keys
+# Set domain name equal to path to key for the client cert above
+# "example.com" = "mycert.key"
+
+
[keybindings]
# In the future there will be more settings here.
diff --git a/display/bookmarks.go b/display/bookmarks.go
index 017c19d..8b0fe9a 100644
--- a/display/bookmarks.go
+++ b/display/bookmarks.go
@@ -88,7 +88,7 @@ func openBkmkModal(name string, exists bool, favicon string) (string, int) {
if favicon != "" && !exists {
name = favicon + " " + name
}
- bkmkModalText = ""
+ bkmkModalText = name
bkmkModal.GetForm().AddInputField("Name: ", name, 0, nil,
func(text string) {
// Store for use later
diff --git a/display/display.go b/display/display.go
index 0f08676..d2b6921 100644
--- a/display/display.go
+++ b/display/display.go
@@ -133,6 +133,13 @@ func Init() {
// This shouldn't occur
return
}
+
+ if query == ".." && tabs[tab].page.URL[len(tabs[tab].page.URL)-1] != '/' {
+ // Support what ".." used to work like
+ // If on /dir/doc.gmi, got to /dir/
+ query = "./"
+ }
+
target, err := current.Parse(query)
if err != nil {
// Invalid relative url
diff --git a/display/download.go b/display/download.go
index a150166..6a30b4e 100644
--- a/display/download.go
+++ b/display/download.go
@@ -117,10 +117,12 @@ func dlChoice(text, u string, resp *gemini.Response) {
portalURL = parsed.String() + "%3F" + query
}
portalURL = strings.TrimPrefix(portalURL, "gemini://") + "?raw=1"
- handleHTTP("https://portal.mozz.us/gemini/"+portalURL, false)
- tabPages.SwitchToPage(strconv.Itoa(curTab))
- App.SetFocus(tabs[curTab].view)
- App.Draw()
+ ok := handleHTTP("https://portal.mozz.us/gemini/"+portalURL, false)
+ if ok {
+ tabPages.SwitchToPage(strconv.Itoa(curTab))
+ App.SetFocus(tabs[curTab].view)
+ App.Draw()
+ }
return
}
tabPages.SwitchToPage(strconv.Itoa(curTab))
diff --git a/display/private.go b/display/private.go
index d2a341b..8686fd7 100644
--- a/display/private.go
+++ b/display/private.go
@@ -20,7 +20,6 @@ import (
"github.com/makeworld-the-better-one/go-gemini"
"github.com/makeworld-the-better-one/go-isemoji"
"github.com/spf13/viper"
- "gitlab.com/tslocum/cview"
)
// This file contains the functions that aren't part of the public API.
@@ -145,26 +144,42 @@ func setPage(t *tab, p *structs.Page) {
// handleHTTP is used by handleURL.
// It opens HTTP links and displays Info and Error modals.
-func handleHTTP(u string, showInfo bool) {
- switch strings.TrimSpace(viper.GetString("a-general.http")) {
- case "", "off":
- Error("HTTP Error", "Opening HTTP URLs is turned off.")
- case "default":
- s, err := webbrowser.Open(u)
- if err != nil {
- Error("Webbrowser Error", err.Error())
- } else if showInfo {
- Info(s)
- }
- default:
- // The config has a custom command to execute for HTTP URLs
- fields := strings.Fields(viper.GetString("a-general.http"))
- err := exec.Command(fields[0], append(fields[1:], u)...).Start()
- if err != nil {
- Error("HTTP Error", "Error executing custom browser command: "+err.Error())
+// Returns false if there was an error.
+func handleHTTP(u string, showInfo bool) bool {
+ if len(config.HTTPCommand) == 1 {
+ // Possibly a non-command
+
+ switch strings.TrimSpace(config.HTTPCommand[0]) {
+ case "", "off":
+ Error("HTTP Error", "Opening HTTP URLs is turned off.")
+ return false
+ case "default":
+ s, err := webbrowser.Open(u)
+ if err != nil {
+ Error("Webbrowser Error", err.Error())
+ return false
+ }
+ if showInfo {
+ Info(s)
+ }
+ return true
}
}
+
+ // Custom command
+ var err error = nil
+ if len(config.HTTPCommand) > 1 {
+ err = exec.Command(config.HTTPCommand[0], append(config.HTTPCommand[1:], u)...).Start()
+ } else {
+ err = exec.Command(config.HTTPCommand[0], u).Start()
+ }
+ if err != nil {
+ Error("HTTP Error", "Error executing custom browser command: "+err.Error())
+ return false
+ }
+
App.Draw()
+ return true
}
// handleOther is used by handleURL.
@@ -367,11 +382,14 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
// Gemini URL, or one with a Gemini proxy available
- // Load page from cache if possible
- page, ok := cache.GetPage(u)
- if ok {
- setPage(t, page)
- return ret(u, true)
+ // Load page from cache if it exists,
+ // and this isn't a page that was redirected to by the server (indicates dynamic content)
+ if numRedirects == 0 {
+ page, ok := cache.GetPage(u)
+ if ok {
+ setPage(t, page)
+ return ret(u, true)
+ }
}
// Otherwise download it
bottomBar.SetText("Loading...")
@@ -449,7 +467,12 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
}
page.Width = termW
- go cache.AddPage(page)
+
+ if !client.HasClientCert(parsed.Host) {
+ // Don't cache pages with client certs
+ go cache.AddPage(page)
+ }
+
setPage(t, page)
return ret(u, true)
}
@@ -457,8 +480,8 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
// Could be a non 20 (or 21) status code, or a different kind of document
// Handle each status code
- switch gemini.SimplifyStatus(res.Status) {
- case 10:
+ switch res.Status {
+ case 10, 11:
userInput, ok := Input(res.Meta)
if ok {
// Make another request with the query string added
@@ -471,7 +494,7 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
return ret(handleURL(t, parsed.String(), 0))
}
return ret("", false)
- case 30:
+ case 30, 31:
parsedMeta, err := url.Parse(res.Meta)
if err != nil {
Error("Redirect Error", "Invalid URL: "+err.Error())
@@ -497,15 +520,46 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
}
return ret("", false)
case 40:
- Error("Temporary Failure", cview.Escape(res.Meta))
+ Error("Temporary Failure", escapeMeta(res.Meta))
+ return ret("", false)
+ case 41:
+ Error("Server Unavailable", escapeMeta(res.Meta))
+ return ret("", false)
+ case 42:
+ Error("CGI Error", escapeMeta(res.Meta))
+ return ret("", false)
+ case 43:
+ Error("Proxy Failure", escapeMeta(res.Meta))
+ return ret("", false)
+ case 44:
+ Error("Slow Down", "You should wait "+escapeMeta(res.Meta)+" seconds before making another request.")
return ret("", false)
case 50:
- Error("Permanent Failure", cview.Escape(res.Meta))
+ Error("Permanent Failure", escapeMeta(res.Meta))
+ return ret("", false)
+ case 51:
+ Error("Not Found", escapeMeta(res.Meta))
+ return ret("", false)
+ case 52:
+ Error("Gone", escapeMeta(res.Meta))
+ return ret("", false)
+ case 53:
+ Error("Proxy Request Refused", escapeMeta(res.Meta))
+ return ret("", false)
+ case 59:
+ Error("Bad Request", escapeMeta(res.Meta))
return ret("", false)
case 60:
- Info("The server requested a certificate. Cert handling is coming to Amfora soon!")
+ Error("Client Certificate Required", escapeMeta(res.Meta))
+ return ret("", false)
+ case 61:
+ Error("Certificate Not Authorised", escapeMeta(res.Meta))
+ return ret("", false)
+ case 62:
+ Error("Certificate Not Valid", escapeMeta(res.Meta))
return ret("", false)
}
+
// Status code 20, but not a document that can be displayed
go dlChoice("That file could not be displayed. What would you like to do?", u, res)
return ret("", false)
diff --git a/display/util.go b/display/util.go
index e03fdd6..4ea8e91 100644
--- a/display/util.go
+++ b/display/util.go
@@ -3,12 +3,19 @@ package display
import (
"errors"
"net/url"
+ "strings"
"github.com/spf13/viper"
+ "gitlab.com/tslocum/cview"
)
// This file contains funcs that are small, self-contained utilities.
+// escapeMeta santizes a META string for use within a cview modal.
+func escapeMeta(meta string) string {
+ return cview.Escape(strings.ReplaceAll(meta, "\n", ""))
+}
+
// isValidTab indicates whether the passed tab is still being used, even if it's not currently displayed.
func isValidTab(t *tab) bool {
tempTabs := tabs
diff --git a/go.mod b/go.mod
index bc129f0..4925424 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,7 @@ require (
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/gdamore/tcell v1.3.1-0.20200608133353-cb1e5d6fa606
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-gemini v0.9.1
github.com/makeworld-the-better-one/go-isemoji v1.1.0
github.com/makeworld-the-better-one/progressbar/v3 v3.3.5-0.20200710151429-125743e22b4f
github.com/mitchellh/go-homedir v1.1.0
diff --git a/go.sum b/go.sum
index d8f5d5d..4affab9 100644
--- a/go.sum
+++ b/go.sum
@@ -130,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.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-gemini v0.9.1 h1:/Vc6Y4Y1aOi4lZIBA1wDe+4N2xAI8EQ0CIjip2NUQkk=
+github.com/makeworld-the-better-one/go-gemini v0.9.1/go.mod h1:P7/FbZ+IEIbA/d+A0Y3w2GNgD8SA2AcNv7aDGJbaWG4=
github.com/makeworld-the-better-one/go-isemoji v1.1.0 h1:wZBHOKB5zAIgaU2vaWnXFDDhatebB8TySrNVxjVV84g=
github.com/makeworld-the-better-one/go-isemoji v1.1.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=
diff --git a/renderer/renderer.go b/renderer/renderer.go
index 3b1e6eb..8f41fdd 100644
--- a/renderer/renderer.go
+++ b/renderer/renderer.go
@@ -80,17 +80,6 @@ func wrapLine(line string, width int, prefix, suffix string, includeFirst bool)
return ret
}
-// tagLines splits a string into lines and adds a the given
-// string to the start and another to the end.
-// It is used for adding cview color tags.
-func tagLines(s, start, end string) string {
- lines := strings.Split(s, "\n")
- for i := range lines {
- lines[i] = start + lines[i] + end
- }
- return strings.Join(lines, "\n")
-}
-
// convertRegularGemini converts non-preformatted blocks of text/gemini
// into a cview-compatible format.
// Since this only works on non-preformatted blocks, RenderGemini
@@ -283,11 +272,6 @@ func convertRegularGemini(s string, numLinks, width int, proxied bool) (string,
// If it's not a gemini:// page, set this to true.
func RenderGemini(s string, width, leftMargin int, proxied bool) (string, []string) {
s = cview.Escape(s)
- if viper.GetBool("a-general.color") && viper.GetBool("a-general.ansi") {
- s = cview.TranslateANSI(s)
- } else {
- s = ansiRegex.ReplaceAllString(s, "")
- }
lines := strings.Split(s, "\n")
@@ -302,13 +286,22 @@ func RenderGemini(s string, width, leftMargin int, proxied bool) (string, []stri
if pre {
// In a preformatted block, so add the text as is
// Don't add the current line with backticks
- rendered += tagLines(
- buf,
- fmt.Sprintf("[%s]", config.GetColorString("preformatted_text")),
- "[-]",
- )
+
+ // Support ANSI color codes in preformatted blocks - see #59
+ if viper.GetBool("a-general.color") && viper.GetBool("a-general.ansi") {
+ buf = cview.TranslateANSI(buf)
+ } else {
+ buf = ansiRegex.ReplaceAllString(buf, "")
+ }
+
+ rendered += fmt.Sprintf("[%s]", config.GetColorString("preformatted_text")) +
+ buf + "[-]"
} else {
// Not preformatted, regular text
+
+ // ANSI not allowed in regular text - see #59
+ buf = ansiRegex.ReplaceAllString(buf, "")
+
ren, lks := convertRegularGemini(buf, len(links), width, proxied)
links = append(links, lks...)
rendered += ren
@@ -323,10 +316,21 @@ func RenderGemini(s string, width, leftMargin int, proxied bool) (string, []stri
// Gone through all the lines, but there still is likely a block in the buffer
if pre {
// File ended without closing the preformatted block
- rendered += buf
+ // Same code as in the loop above
+
+ if viper.GetBool("a-general.color") && viper.GetBool("a-general.ansi") {
+ buf = cview.TranslateANSI(buf)
+ } else {
+ buf = ansiRegex.ReplaceAllString(buf, "")
+ }
+ rendered += fmt.Sprintf("[%s]", config.GetColorString("preformatted_text")) +
+ buf + "[-]"
} else {
// Not preformatted, regular text
// Same code as in the loop above
+
+ buf = ansiRegex.ReplaceAllString(buf, "")
+
ren, lks := convertRegularGemini(buf, len(links), width, proxied)
links = append(links, lks...)
rendered += ren