1
0
Fork 0

Merge branch 'master' into feeds

This commit is contained in:
makeworld 2020-11-17 10:45:28 -05:00
commit bc4c72bba9
20 changed files with 461 additions and 140 deletions

30
.github/workflows/test.yml vendored Normal file
View File

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

View File

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

View File

@ -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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [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 ### Added
- **Support client certificates** through config (#112)
- `ansi` config setting, to disable ANSI colors in pages (#79, #86) - `ansi` config setting, to disable ANSI colors in pages (#79, #86)
- Edit current URL with <kbd>e</kbd> (#87) - Edit current URL with <kbd>e</kbd> (#87)
- If `emoji_favicons` is enabled, new bookmarks will have the domain's favicon prepended (#69, #90) - 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) - 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 ### Changed
- Disabling the `color` config setting also disables ANSI colors in pages (#79, #86) - 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 - 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) - The web browser code doesn't check for Xorg anymore, just display variables (#93)
- Bookmarks can be made to non-gemini URLs (#94) - 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 ### Fixed
- XDG user dir file is parsed instead of looking for XDG env vars (#97, #100) - 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 ### Added
- **Proxy support** - see the `[proxies]` section in the config (#66, #80) - **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) - **Emoji favicons** can now be seen if `emoji_favicons` is enabled in the config (#62)

View File

@ -5,7 +5,7 @@
<h6>Image modified from: amphora by Alvaro Cabrera from the Noun Project</h6> <h6>Image modified from: amphora by Alvaro Cabrera from the Noun Project</h6>
</center> </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) [![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) [![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) [![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
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 ### 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 - Disabled by default, enable in config
- [x] Proxying - [x] Proxying
- Schemes like Gopher or HTTP can be proxied through a Gemini server - 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* - [x] *Subscribe to RSS and Atom feeds and display them*
- Subscribing to page changes, similar to how Spacewalk works, will also be supported - Subscribing to page changes, similar to how Spacewalk works, will also be supported
- [ ] Stream support - [ ] 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 - [ ] Table of contents for pages
- [ ] History browser - [ ] 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\<username>\AppData\Roaming\amfora\config.toml`. On Windows, the file is in `%APPDATA%\amfora\config.toml`, which usually expands to `C:\Users\<username>\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 ## 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)) - 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). 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).

13
THANKS.md Normal file
View File

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

View File

@ -10,7 +10,7 @@ import (
) )
var ( var (
version = "1.5.0" version = "v1.6.0"
commit = "unknown" commit = "unknown"
builtBy = "unknown" builtBy = "unknown"
) )

View File

@ -2,23 +2,74 @@
package client package client
import ( import (
"io/ioutil"
"net" "net"
"net/url" "net/url"
"github.com/makeworld-the-better-one/go-gemini" "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. // Fetch returns response data and an error.
// The error text is human friendly and should be displayed. // The error text is human friendly and should be displayed.
func Fetch(u string) (*gemini.Response, error) { 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 { if err != nil {
return nil, err return nil, err
} }
parsed, _ := url.Parse(u)
ok := handleTofu(parsed.Hostname(), parsed.Port(), res.Cert) ok := handleTofu(parsed.Hostname(), parsed.Port(), res.Cert)
if !ok { if !ok {
return res, ErrTofu return res, ErrTofu
@ -29,7 +80,16 @@ func Fetch(u string) (*gemini.Response, error) {
// FetchWithProxy is the same as Fetch, but uses a proxy. // FetchWithProxy is the same as Fetch, but uses a proxy.
func FetchWithProxy(proxyHostname, proxyPort, u string) (*gemini.Response, error) { 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 { if err != nil {
return nil, err return nil, err
} }

View File

@ -1,6 +1,7 @@
// Package config initializes all files required for Amfora, even those used by // 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 // other packages. It also reads in the config file and initializes a Viper and
// the theme // the theme
//nolint:golint,goerr113
package config package config
import ( import (
@ -40,12 +41,13 @@ var bkmkPath string
var DownloadsDir string var DownloadsDir string
// Feeds // Feeds
var FeedJSON io.ReadCloser var FeedJSON io.ReadCloser
var feedDir string var feedDir string
var FeedPath 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 { func Init() error {
// *** Set paths *** // *** Set paths ***
@ -69,12 +71,7 @@ func Init() error {
configDir = amforaAppData configDir = amforaAppData
} else { } else {
// Unix / POSIX system // Unix / POSIX system
if basedir.ConfigHome == "" { configDir = filepath.Join(basedir.ConfigHome, "amfora")
// Default to ~/.config/amfora
configDir = filepath.Join(home, ".config", "amfora")
} else {
configDir = filepath.Join(basedir.ConfigHome, "amfora")
}
} }
configPath = filepath.Join(configDir, "config.toml") configPath = filepath.Join(configDir, "config.toml")
@ -91,12 +88,7 @@ func Init() error {
tofuDBDir = amforaAppData tofuDBDir = amforaAppData
} else { } else {
// XDG cache dir on POSIX systems // XDG cache dir on POSIX systems
if basedir.CacheHome == "" { tofuDBDir = filepath.Join(basedir.CacheHome, "amfora")
// Default to ~/.cache/amfora
tofuDBDir = filepath.Join(home, ".cache", "amfora")
} else {
tofuDBDir = filepath.Join(basedir.CacheHome, "amfora")
}
} }
tofuDBPath = filepath.Join(tofuDBDir, "tofu.toml") tofuDBPath = filepath.Join(tofuDBDir, "tofu.toml")
@ -106,12 +98,7 @@ func Init() error {
bkmkDir = amforaAppData bkmkDir = amforaAppData
} else { } else {
// XDG data dir on POSIX systems // XDG data dir on POSIX systems
if basedir.DataHome == "" { bkmkDir = filepath.Join(basedir.DataHome, "amfora")
// Default to ~/.local/share/amfora
bkmkDir = filepath.Join(home, ".local", "share", "amfora")
} else {
bkmkDir = filepath.Join(basedir.DataHome, "amfora")
}
} }
bkmkPath = filepath.Join(bkmkDir, "bookmarks.toml") bkmkPath = filepath.Join(bkmkDir, "bookmarks.toml")
@ -282,5 +269,14 @@ func Init() error {
cview.Styles.PrimitiveBackgroundColor = GetColor("bg") cview.Styles.PrimitiveBackgroundColor = GetColor("bg")
} // Otherwise it's black by default } // 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 return nil
} }

View File

@ -21,10 +21,20 @@ home = "gemini://gemini.circumlunar.space"
# If set to false, a prompt will be shown before following redirects. # If set to false, a prompt will be shown before following redirects.
auto_redirect = false auto_redirect = false
# What command to run to open a HTTP(S) URL. Set to "default" to try to guess the browser, # What command to run to open a HTTP(S) URL.
# or set to "off" to not open HTTP(S) URLs. # 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. # 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" http = "default"
# Any URL that will accept a query string can be put here # 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 # Whether colors will be used in the terminal
color = true 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 ansi = true
# Whether to replace list asterisks with unicode bullets # Whether to replace list asterisks with unicode bullets
@ -59,6 +69,20 @@ page_max_time = 10
emoji_favicons = false 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] [keybindings]
# In the future there will be more settings here. # In the future there will be more settings here.

View File

@ -58,7 +58,7 @@ var theme = map[string]tcell.Color{
"link_number": tcell.ColorSilver, "link_number": tcell.ColorSilver,
"regular_text": tcell.ColorWhite, "regular_text": tcell.ColorWhite,
"quote_text": tcell.ColorWhite, "quote_text": tcell.ColorWhite,
"preformatted_text": tcell.ColorWhite, "preformatted_text": tcell.ColorGrey,
"list_text": tcell.ColorWhite, "list_text": tcell.ColorWhite,
} }

111
contrib/themes/nord.toml Normal file
View File

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

View File

@ -18,10 +18,20 @@ home = "gemini://gemini.circumlunar.space"
# If set to false, a prompt will be shown before following redirects. # If set to false, a prompt will be shown before following redirects.
auto_redirect = false auto_redirect = false
# What command to run to open a HTTP(S) URL. Set to "default" to try to guess the browser, # What command to run to open a HTTP(S) URL.
# or set to "off" to not open HTTP(S) URLs. # 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. # 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" http = "default"
# Any URL that will accept a query string can be put here # 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 # Whether colors will be used in the terminal
color = true 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 ansi = true
# Whether to replace list asterisks with unicode bullets # Whether to replace list asterisks with unicode bullets
@ -56,6 +66,20 @@ page_max_time = 10
emoji_favicons = false 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] [keybindings]
# In the future there will be more settings here. # In the future there will be more settings here.

View File

@ -88,7 +88,7 @@ func openBkmkModal(name string, exists bool, favicon string) (string, int) {
if favicon != "" && !exists { if favicon != "" && !exists {
name = favicon + " " + name name = favicon + " " + name
} }
bkmkModalText = "" bkmkModalText = name
bkmkModal.GetForm().AddInputField("Name: ", name, 0, nil, bkmkModal.GetForm().AddInputField("Name: ", name, 0, nil,
func(text string) { func(text string) {
// Store for use later // Store for use later

View File

@ -133,6 +133,13 @@ func Init() {
// This shouldn't occur // This shouldn't occur
return 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) target, err := current.Parse(query)
if err != nil { if err != nil {
// Invalid relative url // Invalid relative url

View File

@ -117,10 +117,12 @@ func dlChoice(text, u string, resp *gemini.Response) {
portalURL = parsed.String() + "%3F" + query portalURL = parsed.String() + "%3F" + query
} }
portalURL = strings.TrimPrefix(portalURL, "gemini://") + "?raw=1" portalURL = strings.TrimPrefix(portalURL, "gemini://") + "?raw=1"
handleHTTP("https://portal.mozz.us/gemini/"+portalURL, false) ok := handleHTTP("https://portal.mozz.us/gemini/"+portalURL, false)
tabPages.SwitchToPage(strconv.Itoa(curTab)) if ok {
App.SetFocus(tabs[curTab].view) tabPages.SwitchToPage(strconv.Itoa(curTab))
App.Draw() App.SetFocus(tabs[curTab].view)
App.Draw()
}
return return
} }
tabPages.SwitchToPage(strconv.Itoa(curTab)) tabPages.SwitchToPage(strconv.Itoa(curTab))

View File

@ -20,7 +20,6 @@ import (
"github.com/makeworld-the-better-one/go-gemini" "github.com/makeworld-the-better-one/go-gemini"
"github.com/makeworld-the-better-one/go-isemoji" "github.com/makeworld-the-better-one/go-isemoji"
"github.com/spf13/viper" "github.com/spf13/viper"
"gitlab.com/tslocum/cview"
) )
// This file contains the functions that aren't part of the public API. // 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. // handleHTTP is used by handleURL.
// It opens HTTP links and displays Info and Error modals. // It opens HTTP links and displays Info and Error modals.
func handleHTTP(u string, showInfo bool) { // Returns false if there was an error.
switch strings.TrimSpace(viper.GetString("a-general.http")) { func handleHTTP(u string, showInfo bool) bool {
case "", "off": if len(config.HTTPCommand) == 1 {
Error("HTTP Error", "Opening HTTP URLs is turned off.") // Possibly a non-command
case "default":
s, err := webbrowser.Open(u) switch strings.TrimSpace(config.HTTPCommand[0]) {
if err != nil { case "", "off":
Error("Webbrowser Error", err.Error()) Error("HTTP Error", "Opening HTTP URLs is turned off.")
} else if showInfo { return false
Info(s) case "default":
} s, err := webbrowser.Open(u)
default: if err != nil {
// The config has a custom command to execute for HTTP URLs Error("Webbrowser Error", err.Error())
fields := strings.Fields(viper.GetString("a-general.http")) return false
err := exec.Command(fields[0], append(fields[1:], u)...).Start() }
if err != nil { if showInfo {
Error("HTTP Error", "Error executing custom browser command: "+err.Error()) 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() App.Draw()
return true
} }
// handleOther is used by handleURL. // 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 // Gemini URL, or one with a Gemini proxy available
// Load page from cache if possible // Load page from cache if it exists,
page, ok := cache.GetPage(u) // and this isn't a page that was redirected to by the server (indicates dynamic content)
if ok { if numRedirects == 0 {
setPage(t, page) page, ok := cache.GetPage(u)
return ret(u, true) if ok {
setPage(t, page)
return ret(u, true)
}
} }
// Otherwise download it // Otherwise download it
bottomBar.SetText("Loading...") bottomBar.SetText("Loading...")
@ -449,7 +467,12 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
} }
page.Width = termW 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) setPage(t, page)
return ret(u, true) 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 // Could be a non 20 (or 21) status code, or a different kind of document
// Handle each status code // Handle each status code
switch gemini.SimplifyStatus(res.Status) { switch res.Status {
case 10: case 10, 11:
userInput, ok := Input(res.Meta) userInput, ok := Input(res.Meta)
if ok { if ok {
// Make another request with the query string added // 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(handleURL(t, parsed.String(), 0))
} }
return ret("", false) return ret("", false)
case 30: case 30, 31:
parsedMeta, err := url.Parse(res.Meta) parsedMeta, err := url.Parse(res.Meta)
if err != nil { if err != nil {
Error("Redirect Error", "Invalid URL: "+err.Error()) Error("Redirect Error", "Invalid URL: "+err.Error())
@ -497,15 +520,46 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
} }
return ret("", false) return ret("", false)
case 40: 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) return ret("", false)
case 50: 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) return ret("", false)
case 60: 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) return ret("", false)
} }
// Status code 20, but not a document that can be displayed // 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) go dlChoice("That file could not be displayed. What would you like to do?", u, res)
return ret("", false) return ret("", false)

View File

@ -3,12 +3,19 @@ package display
import ( import (
"errors" "errors"
"net/url" "net/url"
"strings"
"github.com/spf13/viper" "github.com/spf13/viper"
"gitlab.com/tslocum/cview"
) )
// This file contains funcs that are small, self-contained utilities. // 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. // isValidTab indicates whether the passed tab is still being used, even if it's not currently displayed.
func isValidTab(t *tab) bool { func isValidTab(t *tab) bool {
tempTabs := tabs tempTabs := tabs

2
go.mod
View File

@ -7,7 +7,7 @@ require (
github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/gdamore/tcell v1.3.1-0.20200608133353-cb1e5d6fa606 github.com/gdamore/tcell v1.3.1-0.20200608133353-cb1e5d6fa606
github.com/google/go-cmp v0.5.0 // indirect 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/go-isemoji v1.1.0
github.com/makeworld-the-better-one/progressbar/v3 v3.3.5-0.20200710151429-125743e22b4f github.com/makeworld-the-better-one/progressbar/v3 v3.3.5-0.20200710151429-125743e22b4f
github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-homedir v1.1.0

4
go.sum
View File

@ -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/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 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 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.9.1 h1:/Vc6Y4Y1aOi4lZIBA1wDe+4N2xAI8EQ0CIjip2NUQkk=
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/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 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/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= github.com/makeworld-the-better-one/progressbar/v3 v3.3.5-0.20200710151429-125743e22b4f h1:YEUlTs5gb35UlBLTgqrub9axWTYB3d7/8TxrkJDZpRI=

View File

@ -80,17 +80,6 @@ func wrapLine(line string, width int, prefix, suffix string, includeFirst bool)
return ret 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 // convertRegularGemini converts non-preformatted blocks of text/gemini
// into a cview-compatible format. // into a cview-compatible format.
// Since this only works on non-preformatted blocks, RenderGemini // 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. // If it's not a gemini:// page, set this to true.
func RenderGemini(s string, width, leftMargin int, proxied bool) (string, []string) { func RenderGemini(s string, width, leftMargin int, proxied bool) (string, []string) {
s = cview.Escape(s) 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") lines := strings.Split(s, "\n")
@ -302,13 +286,22 @@ func RenderGemini(s string, width, leftMargin int, proxied bool) (string, []stri
if pre { if pre {
// In a preformatted block, so add the text as is // In a preformatted block, so add the text as is
// Don't add the current line with backticks // Don't add the current line with backticks
rendered += tagLines(
buf, // Support ANSI color codes in preformatted blocks - see #59
fmt.Sprintf("[%s]", config.GetColorString("preformatted_text")), 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 { } else {
// Not preformatted, regular text // Not preformatted, regular text
// ANSI not allowed in regular text - see #59
buf = ansiRegex.ReplaceAllString(buf, "")
ren, lks := convertRegularGemini(buf, len(links), width, proxied) ren, lks := convertRegularGemini(buf, len(links), width, proxied)
links = append(links, lks...) links = append(links, lks...)
rendered += ren 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 // Gone through all the lines, but there still is likely a block in the buffer
if pre { if pre {
// File ended without closing the preformatted block // 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 { } else {
// Not preformatted, regular text // Not preformatted, regular text
// Same code as in the loop above // Same code as in the loop above
buf = ansiRegex.ReplaceAllString(buf, "")
ren, lks := convertRegularGemini(buf, len(links), width, proxied) ren, lks := convertRegularGemini(buf, len(links), width, proxied)
links = append(links, lks...) links = append(links, lks...)
rendered += ren rendered += ren