mirror of
https://github.com/makew0rld/amfora.git
synced 2024-06-01 18:31:08 +00:00
Merge branch 'master' into commands
This commit is contained in:
commit
3b3d77447e
|
@ -7,10 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
## [Unreleased]
|
||||
### Added
|
||||
- Syntax highlighting for preformatted text blocks with alt text (#252, #263, [wiki page](https://github.com/makeworld-the-better-one/amfora/wiki/Source-Code-Highlighting))
|
||||
- [Client certificates](https://github.com/makeworld-the-better-one/amfora/wiki/Client-Certificates) can be restricted to certain paths of a host (#115)
|
||||
- `header` config option in `[subscriptions]` to allow disabling the header text on the subscriptions page (#191)
|
||||
- Selected link and scroll position stays for non-cached pages (#122)
|
||||
- Keybinding to open URL with URL handler instead of configured proxy (#143)
|
||||
- `include` theme key to import themes from an external file (#154)
|
||||
- Support SOCKS5 proxying by setting `AMFORA_SOCKS5` environment variable (#155)
|
||||
|
||||
### Changed
|
||||
- Center text automatically, removing `left_margin` from the config (#233)
|
||||
- `max_width` defaults to 80 columns instead of 100 (#233)
|
||||
- Tabs have the domain of the current page instead of numbers (#202)
|
||||
|
||||
### Fixed
|
||||
- Modal can't be closed when opening non-gemini text URLs from the commandline (#283, #284)
|
||||
|
|
|
@ -54,7 +54,12 @@ func main() {
|
|||
fmt.Fprintf(os.Stderr, "Config error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
client.Init()
|
||||
|
||||
err = client.Init()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Client error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = subscriptions.Init()
|
||||
if err != nil {
|
||||
|
|
111
client/client.go
111
client/client.go
|
@ -2,51 +2,128 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/makeworld-the-better-one/go-gemini"
|
||||
gemsocks5 "github.com/makeworld-the-better-one/go-gemini-socks5"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Simple key for certCache map and others, instead of a full URL
|
||||
// Only uses the part of the URL relevant to matching certs to a URL
|
||||
type certMapKey struct {
|
||||
host string
|
||||
path string
|
||||
}
|
||||
|
||||
var (
|
||||
certCache = make(map[string][][]byte)
|
||||
// [auth] section of config put into maps
|
||||
confCerts = make(map[certMapKey]string)
|
||||
confKeys = make(map[certMapKey]string)
|
||||
|
||||
// Cache the cert and key assigned to different URLs
|
||||
certCache = make(map[certMapKey][][]byte)
|
||||
certCacheMu = &sync.RWMutex{}
|
||||
|
||||
fetchClient *gemini.Client
|
||||
)
|
||||
|
||||
func Init() {
|
||||
func Init() error {
|
||||
fetchClient = &gemini.Client{
|
||||
ConnectTimeout: 10 * time.Second, // Default is 15
|
||||
ReadTimeout: time.Duration(viper.GetInt("a-general.page_max_time")) * time.Second,
|
||||
}
|
||||
|
||||
if socksHost := os.Getenv("AMFORA_SOCKS5"); socksHost != "" {
|
||||
fetchClient.Proxy = gemsocks5.ProxyFunc(socksHost, nil)
|
||||
}
|
||||
|
||||
// Populate config maps
|
||||
|
||||
certsViper := viper.Sub("auth.certs")
|
||||
for _, certURL := range certsViper.AllKeys() {
|
||||
// Normalize URL so that it can be matched no matter how it was written
|
||||
// in the config
|
||||
pu, _ := normalizeURL(FixUserURL(certURL))
|
||||
if pu == nil {
|
||||
//nolint:goerr113
|
||||
return errors.New("[auth.certs]: couldn't normalize URL: " + certURL)
|
||||
}
|
||||
confCerts[certMapKey{pu.Host, pu.Path}] = certsViper.GetString(certURL)
|
||||
}
|
||||
|
||||
keysViper := viper.Sub("auth.keys")
|
||||
for _, keyURL := range keysViper.AllKeys() {
|
||||
pu, _ := normalizeURL(FixUserURL(keyURL))
|
||||
if pu == nil {
|
||||
//nolint:goerr113
|
||||
return errors.New("[auth.keys]: couldn't normalize URL: " + keyURL)
|
||||
}
|
||||
confKeys[certMapKey{pu.Host, pu.Path}] = keysViper.GetString(keyURL)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func clientCert(host string) ([]byte, []byte) {
|
||||
// getCertPath returns the path of the cert from the config.
|
||||
// It returns "" if no config value exists.
|
||||
func getCertPath(host string, path string) string {
|
||||
for k, v := range confCerts {
|
||||
if k.host == host && (k.path == path || strings.HasPrefix(path, k.path)) {
|
||||
// Either exact match to what's in config, or a subpath
|
||||
return v
|
||||
}
|
||||
}
|
||||
// No matches
|
||||
return ""
|
||||
}
|
||||
|
||||
// getKeyPath returns the path of the key from the config.
|
||||
// It returns "" if no config value exists.
|
||||
func getKeyPath(host string, path string) string {
|
||||
for k, v := range confKeys {
|
||||
if k.host == host && (k.path == path || strings.HasPrefix(path, k.path)) {
|
||||
// Either exact match to what's in config, or a subpath
|
||||
return v
|
||||
}
|
||||
}
|
||||
// No matches
|
||||
return ""
|
||||
}
|
||||
|
||||
func clientCert(host string, path string) ([]byte, []byte) {
|
||||
mkey := certMapKey{host, path}
|
||||
|
||||
certCacheMu.RLock()
|
||||
pair, ok := certCache[host]
|
||||
pair, ok := certCache[mkey]
|
||||
certCacheMu.RUnlock()
|
||||
if ok {
|
||||
return pair[0], pair[1]
|
||||
}
|
||||
|
||||
ogCertPath := getCertPath(host, path)
|
||||
// Expand paths starting with ~/
|
||||
certPath, err := homedir.Expand(viper.GetString("auth.certs." + host))
|
||||
certPath, err := homedir.Expand(ogCertPath)
|
||||
if err != nil {
|
||||
certPath = viper.GetString("auth.certs." + host)
|
||||
certPath = ogCertPath
|
||||
}
|
||||
keyPath, err := homedir.Expand(viper.GetString("auth.keys." + host))
|
||||
ogKeyPath := getKeyPath(host, path)
|
||||
keyPath, err := homedir.Expand(ogKeyPath)
|
||||
if err != nil {
|
||||
keyPath = viper.GetString("auth.keys." + host)
|
||||
keyPath = ogKeyPath
|
||||
}
|
||||
|
||||
if certPath == "" && keyPath == "" {
|
||||
certCacheMu.Lock()
|
||||
certCache[host] = [][]byte{nil, nil}
|
||||
certCache[mkey] = [][]byte{nil, nil}
|
||||
certCacheMu.Unlock()
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -54,33 +131,33 @@ func clientCert(host string) ([]byte, []byte) {
|
|||
cert, err := ioutil.ReadFile(certPath)
|
||||
if err != nil {
|
||||
certCacheMu.Lock()
|
||||
certCache[host] = [][]byte{nil, nil}
|
||||
certCache[mkey] = [][]byte{nil, nil}
|
||||
certCacheMu.Unlock()
|
||||
return nil, nil
|
||||
}
|
||||
key, err := ioutil.ReadFile(keyPath)
|
||||
if err != nil {
|
||||
certCacheMu.Lock()
|
||||
certCache[host] = [][]byte{nil, nil}
|
||||
certCache[mkey] = [][]byte{nil, nil}
|
||||
certCacheMu.Unlock()
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
certCacheMu.Lock()
|
||||
certCache[host] = [][]byte{cert, key}
|
||||
certCache[mkey] = [][]byte{cert, key}
|
||||
certCacheMu.Unlock()
|
||||
return cert, key
|
||||
}
|
||||
|
||||
// HasClientCert returns whether or not a client certificate exists for a host.
|
||||
func HasClientCert(host string) bool {
|
||||
cert, _ := clientCert(host)
|
||||
// HasClientCert returns whether or not a client certificate exists for a host and path.
|
||||
func HasClientCert(host string, path string) bool {
|
||||
cert, _ := clientCert(host, path)
|
||||
return cert != nil
|
||||
}
|
||||
|
||||
func fetch(u string, c *gemini.Client) (*gemini.Response, error) {
|
||||
parsed, _ := url.Parse(u)
|
||||
cert, key := clientCert(parsed.Host)
|
||||
cert, key := clientCert(parsed.Host, parsed.Path)
|
||||
|
||||
var res *gemini.Response
|
||||
var err error
|
||||
|
@ -109,7 +186,7 @@ func Fetch(u string) (*gemini.Response, error) {
|
|||
|
||||
func fetchWithProxy(proxyHostname, proxyPort, u string, c *gemini.Client) (*gemini.Response, error) {
|
||||
parsed, _ := url.Parse(u)
|
||||
cert, key := clientCert(parsed.Host)
|
||||
cert, key := clientCert(parsed.Host, parsed.Path)
|
||||
|
||||
var res *gemini.Response
|
||||
var err error
|
||||
|
|
102
client/url.go
Normal file
102
client/url.go
Normal file
|
@ -0,0 +1,102 @@
|
|||
package client
|
||||
|
||||
// Functions that transform and normalize URLs
|
||||
// Originally used to be in display/util.go
|
||||
// Moved here for #115, so URLs in the [auth] config section could be normalized
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/makeworld-the-better-one/go-gemini"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
|
||||
// See doc for NormalizeURL
|
||||
func normalizeURL(u string) (*url.URL, string) {
|
||||
u = norm.NFC.String(u)
|
||||
|
||||
tmp, err := gemini.GetPunycodeURL(u)
|
||||
if err != nil {
|
||||
return nil, u
|
||||
}
|
||||
u = tmp
|
||||
parsed, _ := url.Parse(u)
|
||||
|
||||
if parsed.Scheme == "" {
|
||||
// Always add scheme
|
||||
parsed.Scheme = "gemini"
|
||||
} else if parsed.Scheme != "gemini" {
|
||||
// Not a gemini URL, nothing to do
|
||||
return nil, u
|
||||
}
|
||||
|
||||
parsed.User = nil // No passwords in Gemini
|
||||
parsed.Fragment = "" // No fragments either
|
||||
if parsed.Port() == "1965" {
|
||||
// Always remove default port
|
||||
hostname := parsed.Hostname()
|
||||
if strings.Contains(hostname, ":") {
|
||||
parsed.Host = "[" + parsed.Hostname() + "]"
|
||||
} else {
|
||||
parsed.Host = parsed.Hostname()
|
||||
}
|
||||
}
|
||||
|
||||
// Add slash to the end of a URL with just a domain
|
||||
// gemini://example.com -> gemini://example.com/
|
||||
if parsed.Path == "" {
|
||||
parsed.Path = "/"
|
||||
} else {
|
||||
// Decode and re-encode path
|
||||
// This removes needless encoding, like that of ASCII chars
|
||||
// And encodes anything that wasn't but should've been
|
||||
parsed.RawPath = strings.ReplaceAll(url.PathEscape(parsed.Path), "%2F", "/")
|
||||
}
|
||||
|
||||
// Do the same to the query string
|
||||
un, err := gemini.QueryUnescape(parsed.RawQuery)
|
||||
if err == nil {
|
||||
parsed.RawQuery = gemini.QueryEscape(un)
|
||||
}
|
||||
|
||||
return parsed, ""
|
||||
}
|
||||
|
||||
// NormalizeURL attempts to make URLs that are different strings
|
||||
// but point to the same place all look the same.
|
||||
//
|
||||
// Example: gemini://gus.guru:1965/ and //gus.guru/.
|
||||
// This function will take both output the same URL each time.
|
||||
//
|
||||
// It will also percent-encode invalid characters, and decode chars
|
||||
// that don't need to be encoded. It will also apply Unicode NFC
|
||||
// normalization.
|
||||
//
|
||||
// The string passed must already be confirmed to be a URL.
|
||||
// Detection of a search string vs. a URL must happen elsewhere.
|
||||
//
|
||||
// It only works with absolute URLs.
|
||||
func NormalizeURL(u string) string {
|
||||
pu, s := normalizeURL(u)
|
||||
if pu != nil {
|
||||
// Could be normalized, return it
|
||||
return pu.String()
|
||||
}
|
||||
// Return the best URL available up to that point
|
||||
return s
|
||||
}
|
||||
|
||||
// FixUserURL will take a user-typed URL and add a gemini scheme to it if
|
||||
// necessary. It is not the same as normalizeURL, and that func should still
|
||||
// be used, afterward.
|
||||
//
|
||||
// For example "example.com" will become "gemini://example.com", but
|
||||
// "//example.com" will be left untouched.
|
||||
func FixUserURL(u string) string {
|
||||
if !strings.HasPrefix(u, "//") && !strings.HasPrefix(u, "gemini://") && !strings.Contains(u, "://") {
|
||||
// Assume it's a Gemini URL
|
||||
u = "gemini://" + u
|
||||
}
|
||||
return u
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
//nolint: lll
|
||||
package display
|
||||
package client
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
@ -36,7 +36,7 @@ var normalizeURLTests = []struct {
|
|||
|
||||
func TestNormalizeURL(t *testing.T) {
|
||||
for _, tt := range normalizeURLTests {
|
||||
actual := normalizeURL(tt.u)
|
||||
actual := NormalizeURL(tt.u)
|
||||
if actual != tt.expected {
|
||||
t.Errorf("normalizeURL(%s): expected %s, actual %s", tt.u, tt.expected, actual)
|
||||
}
|
|
@ -285,6 +285,7 @@ func Init() error {
|
|||
viper.SetDefault("keybindings.bind_beginning", []string{"Home", "g"})
|
||||
viper.SetDefault("keybindings.bind_end", []string{"End", "G"})
|
||||
viper.SetDefault("keybindings.shift_numbers", "")
|
||||
viper.SetDefault("keybindings.bind_url_handler_open", "Ctrl-U")
|
||||
viper.SetDefault("url-handlers.other", "default")
|
||||
viper.SetDefault("cache.max_size", 0)
|
||||
viper.SetDefault("cache.max_pages", 20)
|
||||
|
@ -293,6 +294,7 @@ func Init() error {
|
|||
viper.SetDefault("subscriptions.update_interval", 1800)
|
||||
viper.SetDefault("subscriptions.workers", 3)
|
||||
viper.SetDefault("subscriptions.entries_per_page", 20)
|
||||
viper.SetDefault("subscriptions.header", true)
|
||||
|
||||
viper.SetConfigFile(configPath)
|
||||
viper.SetConfigType("toml")
|
||||
|
@ -375,29 +377,56 @@ func Init() error {
|
|||
cache.SetMaxPages(viper.GetInt("cache.max_pages"))
|
||||
cache.SetTimeout(viper.GetInt("cache.timeout"))
|
||||
|
||||
setColor := func(k string, colorStr string) error {
|
||||
colorStr = strings.ToLower(colorStr)
|
||||
var color tcell.Color
|
||||
if colorStr == "default" {
|
||||
if strings.HasSuffix(k, "bg") {
|
||||
color = tcell.ColorDefault
|
||||
} else {
|
||||
return fmt.Errorf(`"default" is only valid for a background color (color ending in "bg"), not "%s"`, k)
|
||||
}
|
||||
} else {
|
||||
color = tcell.GetColor(colorStr)
|
||||
if color == tcell.ColorDefault {
|
||||
return fmt.Errorf(`invalid color format for "%s": %s`, k, colorStr)
|
||||
}
|
||||
}
|
||||
SetColor(k, color)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Setup theme
|
||||
configTheme := viper.Sub("theme")
|
||||
if configTheme != nil {
|
||||
// Include key comes first
|
||||
if incPath := configTheme.GetString("include"); incPath != "" {
|
||||
incViper := viper.New()
|
||||
incViper.SetConfigFile(incPath)
|
||||
incViper.SetConfigType("toml")
|
||||
err = incViper.ReadInConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for k2, v2 := range incViper.AllSettings() {
|
||||
colorStr, ok := v2.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf(`include: value for "%s" is not a string: %v`, k2, v2)
|
||||
}
|
||||
if err := setColor(k2, colorStr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
for k, v := range configTheme.AllSettings() {
|
||||
colorStr, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf(`value for "%s" is not a string: %v`, k, v)
|
||||
}
|
||||
colorStr = strings.ToLower(colorStr)
|
||||
var color tcell.Color
|
||||
if colorStr == "default" {
|
||||
if strings.HasSuffix(k, "bg") {
|
||||
color = tcell.ColorDefault
|
||||
} else {
|
||||
return fmt.Errorf(`"default" is only valid for a background color (color ending in "bg"), not "%s"`, k)
|
||||
}
|
||||
} else {
|
||||
color = tcell.GetColor(colorStr)
|
||||
if color == tcell.ColorDefault {
|
||||
return fmt.Errorf(`invalid color format for "%s": %s`, k, colorStr)
|
||||
}
|
||||
if err := setColor(k, colorStr); err != nil {
|
||||
return err
|
||||
}
|
||||
SetColor(k, color)
|
||||
}
|
||||
}
|
||||
if viper.GetBool("a-general.color") {
|
||||
|
|
|
@ -99,13 +99,17 @@ underline = true
|
|||
|
||||
[auth.certs]
|
||||
# Client certificates
|
||||
# Set domain name equal to path to client cert
|
||||
# "example.com" = 'mycert.crt'
|
||||
# Set URL equal to path to client cert file
|
||||
#
|
||||
# "example.com" = 'mycert.crt' # Cert is used for all paths on this domain
|
||||
# "example.com/dir/"= 'mycert.crt' # Cert is used for /dir/ and everything below only
|
||||
#
|
||||
# See the comment at the beginning of this file for examples of all valid types of
|
||||
# URLs, ports and schemes can be used too
|
||||
|
||||
[auth.keys]
|
||||
# Client certificate keys
|
||||
# Set domain name equal to path to key for the client cert above
|
||||
# "example.com" = 'mycert.key'
|
||||
# Same as [auth.certs] but the path is to the client key file.
|
||||
|
||||
|
||||
[commands]
|
||||
|
@ -230,6 +234,7 @@ underline = true
|
|||
# bind_copy_target_url
|
||||
# bind_beginning: moving to beginning of page (top left)
|
||||
# bind_end: same but the for the end (bottom left)
|
||||
# bind_url_handler_open: Open highlighted URL with URL handler (#143)
|
||||
|
||||
[url-handlers]
|
||||
# Allows setting the commands to run for various URL schemes.
|
||||
|
@ -375,6 +380,9 @@ workers = 3
|
|||
# The number of subscription updates displayed per page.
|
||||
entries_per_page = 20
|
||||
|
||||
# Set to false to remove the explanatory text from the top of the subscription page
|
||||
header = true
|
||||
|
||||
|
||||
[theme]
|
||||
# This section is for changing the COLORS used in Amfora.
|
||||
|
@ -414,6 +422,15 @@ entries_per_page = 20
|
|||
# bottombar_bg
|
||||
# scrollbar: The scrollbar that appears on the right for long pages
|
||||
|
||||
# You can also set an 'include' key to process another TOML file that contains theme keys.
|
||||
# Example:
|
||||
# include = "my/path/to/special-theme.toml"
|
||||
#
|
||||
# Any other theme keys will override this external file.
|
||||
# You can use this special key to switch between themes easily.
|
||||
# Download other themes here: https://github.com/makeworld-the-better-one/amfora/tree/master/contrib/themes
|
||||
|
||||
|
||||
# hdg_1
|
||||
# hdg_2
|
||||
# hdg_3
|
||||
|
|
|
@ -83,6 +83,7 @@ const (
|
|||
CmdCopyTargetURL
|
||||
CmdBeginning
|
||||
CmdEnd
|
||||
CmdURLHandlerOpen // See #143
|
||||
)
|
||||
|
||||
type keyBinding struct {
|
||||
|
@ -220,6 +221,7 @@ func KeyInit() {
|
|||
CmdCopyTargetURL: "keybindings.bind_copy_target_url",
|
||||
CmdBeginning: "keybindings.bind_beginning",
|
||||
CmdEnd: "keybindings.bind_end",
|
||||
CmdURLHandlerOpen: "keybindings.bind_url_handler_open",
|
||||
CmdCommand1: "keybindings.bind_command1",
|
||||
CmdCommand2: "keybindings.bind_command2",
|
||||
CmdCommand3: "keybindings.bind_command3",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
[theme]
|
||||
#[theme]
|
||||
|
||||
# Only the 256 xterm colors are used, so truecolor support is not needed
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
[theme]
|
||||
#[theme]
|
||||
|
||||
# atelier forest light
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
[theme]
|
||||
#[theme]
|
||||
|
||||
# atelier forest
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
[theme]
|
||||
#[theme]
|
||||
bg = "#282a36"
|
||||
tab_num = "#bd93f9"
|
||||
tab_divider = "#f8f8f2"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
[theme]
|
||||
#[theme]
|
||||
# This section is for changing the COLORS used in Amfora.
|
||||
# These colors only apply if 'color' is enabled above.
|
||||
# Colors can be set using a W3C color name, or a hex value such as "#ffffff".
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
[theme]
|
||||
#[theme]
|
||||
bg = "#ffffff"
|
||||
tab_num = "#000000"
|
||||
tab_divider = "#000000"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
[theme]
|
||||
#[theme]
|
||||
# This section is for changing the COLORS used in Amfora.
|
||||
# These colors only apply if 'color' is enabled above.
|
||||
# Colors can be set using a W3C color name, or a hex value such as "#ffffff".
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
[theme]
|
||||
#[theme]
|
||||
|
||||
# Gruvbox Dark theme
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
[theme]
|
||||
#[theme]
|
||||
# This section is for changing the COLORS used in Amfora.
|
||||
# These colors only apply if 'color' is enabled above.
|
||||
# Colors can be set using a W3C color name, or a hex value such as "#ffffff".
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
[theme]
|
||||
#[theme]
|
||||
# This section is for changing the COLORS used in Amfora.
|
||||
# These colors only apply if 'color' is enabled above.
|
||||
# Colors can be set using a W3C color name, or a hex value such as "#ffffff".
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Atom One Dark theme ported to Amfora
|
||||
# by Serge Tymoshenko <serge@tymo.name>
|
||||
|
||||
[theme]
|
||||
#[theme]
|
||||
# This section is for changing the COLORS used in Amfora.
|
||||
# These colors only apply if 'color' is enabled above.
|
||||
# Colors can be set using a W3C color name, or a hex value such as "#ffffff".
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
[theme]
|
||||
#[theme]
|
||||
# This section is for changing the COLORS used in Amfora.
|
||||
# These colors only apply if 'color' is enabled above.
|
||||
# Colors can be set using a W3C color name, or a hex value such as "#ffffff".
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
[theme]
|
||||
#[theme]
|
||||
# This section is for changing the COLORS used in Amfora.
|
||||
# These colors only apply if 'color' is enabled above.
|
||||
# Colors can be set using a W3C color name, or a hex value such as "#ffffff".
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
[theme]
|
||||
#[theme]
|
||||
# This section is for changing the COLORS used in Amfora.
|
||||
# These colors only apply if 'color' is enabled above.
|
||||
# Colors can be set using a W3C color name, or a hex value such as "#ffffff".
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
[theme]
|
||||
#[theme]
|
||||
|
||||
# Tokyo Night
|
||||
|
||||
|
|
|
@ -96,13 +96,17 @@ underline = true
|
|||
|
||||
[auth.certs]
|
||||
# Client certificates
|
||||
# Set domain name equal to path to client cert
|
||||
# "example.com" = 'mycert.crt'
|
||||
# Set URL equal to path to client cert file
|
||||
#
|
||||
# "example.com" = 'mycert.crt' # Cert is used for all paths on this domain
|
||||
# "example.com/dir/"= 'mycert.crt' # Cert is used for /dir/ and everything below only
|
||||
#
|
||||
# See the comment at the beginning of this file for examples of all valid types of
|
||||
# URLs, ports and schemes can be used too
|
||||
|
||||
[auth.keys]
|
||||
# Client certificate keys
|
||||
# Set domain name equal to path to key for the client cert above
|
||||
# "example.com" = 'mycert.key'
|
||||
# Same as [auth.certs] but the path is to the client key file.
|
||||
|
||||
|
||||
[commands]
|
||||
|
@ -227,6 +231,7 @@ underline = true
|
|||
# bind_copy_target_url
|
||||
# bind_beginning: moving to beginning of page (top left)
|
||||
# bind_end: same but the for the end (bottom left)
|
||||
# bind_url_handler_open: Open highlighted URL with URL handler (#143)
|
||||
|
||||
[url-handlers]
|
||||
# Allows setting the commands to run for various URL schemes.
|
||||
|
@ -372,6 +377,9 @@ workers = 3
|
|||
# The number of subscription updates displayed per page.
|
||||
entries_per_page = 20
|
||||
|
||||
# Set to false to remove the explanatory text from the top of the subscription page
|
||||
header = true
|
||||
|
||||
|
||||
[theme]
|
||||
# This section is for changing the COLORS used in Amfora.
|
||||
|
@ -411,6 +419,15 @@ entries_per_page = 20
|
|||
# bottombar_bg
|
||||
# scrollbar: The scrollbar that appears on the right for long pages
|
||||
|
||||
# You can also set an 'include' key to process another TOML file that contains theme keys.
|
||||
# Example:
|
||||
# include = "my/path/to/special-theme.toml"
|
||||
#
|
||||
# Any other theme keys will override this external file.
|
||||
# You can use this special key to switch between themes easily.
|
||||
# Download other themes here: https://github.com/makeworld-the-better-one/amfora/tree/master/contrib/themes
|
||||
|
||||
|
||||
# hdg_1
|
||||
# hdg_2
|
||||
# hdg_3
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"code.rocketnine.space/tslocum/cview"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/makeworld-the-better-one/amfora/cache"
|
||||
"github.com/makeworld-the-better-one/amfora/client"
|
||||
"github.com/makeworld-the-better-one/amfora/config"
|
||||
"github.com/makeworld-the-better-one/amfora/renderer"
|
||||
"github.com/makeworld-the-better-one/amfora/structs"
|
||||
|
@ -87,7 +88,7 @@ func Init(version, commit, builtBy string) {
|
|||
// Overwrite all tabs with a new, differently sized, left margin
|
||||
browser.AddTab(
|
||||
strconv.Itoa(i),
|
||||
makeTabLabel(strconv.Itoa(i+1)),
|
||||
tabs[i].label(),
|
||||
makeContentLayout(tabs[i].view, leftMargin()),
|
||||
)
|
||||
if tabs[i] == t {
|
||||
|
@ -228,12 +229,12 @@ func Init(version, commit, builtBy string) {
|
|||
|
||||
u := viper.GetString("a-general.search") + "?" + gemini.QueryEscape(query)
|
||||
// Don't use the cached version of the search
|
||||
cache.RemovePage(normalizeURL(u))
|
||||
cache.RemovePage(client.NormalizeURL(u))
|
||||
URL(u)
|
||||
} else {
|
||||
// Full URL
|
||||
// Don't use cached version for manually entered URL
|
||||
cache.RemovePage(normalizeURL(fixUserURL(query)))
|
||||
cache.RemovePage(client.NormalizeURL(client.FixUserURL(query)))
|
||||
URL(query)
|
||||
}
|
||||
return
|
||||
|
@ -437,7 +438,7 @@ func NewTabWithURL(url string) {
|
|||
|
||||
browser.AddTab(
|
||||
strconv.Itoa(curTab),
|
||||
makeTabLabel(strconv.Itoa(curTab+1)),
|
||||
tabs[curTab].label(),
|
||||
makeContentLayout(tabs[curTab].view, leftMargin()),
|
||||
)
|
||||
browser.SetCurrentTab(strconv.Itoa(curTab))
|
||||
|
@ -555,7 +556,7 @@ func URL(u string) {
|
|||
if strings.HasPrefix(u, "about:") {
|
||||
go goURL(t, u)
|
||||
} else {
|
||||
go goURL(t, fixUserURL(u))
|
||||
go goURL(t, client.FixUserURL(u))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -216,6 +216,8 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
|
|||
}
|
||||
t.mode = tabModeDone
|
||||
|
||||
t.preferURLHandler = false
|
||||
|
||||
go func(p *structs.Page) {
|
||||
if b && t.hasContent() && !t.isAnAboutPage() && viper.GetBool("subscriptions.popup") {
|
||||
// The current page might be an untracked feed, and the user wants
|
||||
|
@ -241,7 +243,7 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
|
|||
return ret(handleAbout(t, u))
|
||||
}
|
||||
|
||||
u = normalizeURL(u)
|
||||
u = client.NormalizeURL(u)
|
||||
u = cache.Redirect(u)
|
||||
|
||||
parsed, err := url.Parse(u)
|
||||
|
@ -261,7 +263,7 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
|
|||
}
|
||||
|
||||
if strings.HasPrefix(u, "http") {
|
||||
if proxy == "" || proxy == "off" {
|
||||
if proxy == "" || proxy == "off" || t.preferURLHandler {
|
||||
// No proxy available
|
||||
handleHTTP(u, true)
|
||||
return ret("", false)
|
||||
|
@ -280,7 +282,7 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
|
|||
|
||||
if !strings.HasPrefix(u, "http") && !strings.HasPrefix(u, "gemini") && !strings.HasPrefix(u, "file") {
|
||||
// Not a Gemini URL
|
||||
if proxy == "" || proxy == "off" {
|
||||
if proxy == "" || proxy == "off" || t.preferURLHandler {
|
||||
// No proxy available
|
||||
handleOther(u)
|
||||
return ret("", false)
|
||||
|
@ -376,7 +378,7 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
|
|||
|
||||
page.TermWidth = termW
|
||||
|
||||
if !client.HasClientCert(parsed.Host) {
|
||||
if !client.HasClientCert(parsed.Host, parsed.Path) {
|
||||
// Don't cache pages with client certs
|
||||
go cache.AddPage(page)
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ var helpCells = strings.TrimSpace(
|
|||
"Enter, Tab\tOn a page this will start link highlighting.\n" +
|
||||
"\tPress Tab and Shift-Tab to pick different links.\n" +
|
||||
"\tPress Enter again to go to one, or Esc to stop.\n" +
|
||||
"%s\tOpen the highlighted URL with a URL handler instead of the configured proxy\n" +
|
||||
"%s\tGo to a specific tab. (Default: Shift-NUMBER)\n" +
|
||||
"%s\tGo to the last tab.\n" +
|
||||
"%s\tPrevious tab\n" +
|
||||
|
@ -103,6 +104,7 @@ func helpInit() {
|
|||
config.GetKeyBinding(config.CmdEdit),
|
||||
config.GetKeyBinding(config.CmdCopyPageURL),
|
||||
config.GetKeyBinding(config.CmdCopyTargetURL),
|
||||
config.GetKeyBinding(config.CmdURLHandlerOpen),
|
||||
tabKeys,
|
||||
config.GetKeyBinding(config.CmdTab0),
|
||||
config.GetKeyBinding(config.CmdPrevTab),
|
||||
|
|
|
@ -3,6 +3,18 @@ package display
|
|||
// applyHist is a history.go internal function, to load a URL in the history.
|
||||
func applyHist(t *tab) {
|
||||
handleURL(t, t.history.urls[t.history.pos], 0) // Load that position in history
|
||||
|
||||
// Set page's scroll and link info from history cache, in case it didn't have it in the page already
|
||||
// Like for non-cached pages like about: pages
|
||||
// This fixes #122
|
||||
pg := t.history.pageCache[t.history.pos]
|
||||
p := t.page
|
||||
p.Row = pg.row
|
||||
p.Column = pg.column
|
||||
p.Selected = pg.selected
|
||||
p.SelectedID = pg.selectedID
|
||||
p.Mode = pg.mode
|
||||
|
||||
t.applyAll()
|
||||
}
|
||||
|
||||
|
@ -11,6 +23,10 @@ func histForward(t *tab) {
|
|||
// Already on the most recent URL in the history
|
||||
return
|
||||
}
|
||||
|
||||
// Update page cache in history for #122
|
||||
t.historyCachePage()
|
||||
|
||||
t.history.pos++
|
||||
go applyHist(t)
|
||||
}
|
||||
|
@ -20,6 +36,10 @@ func histBack(t *tab) {
|
|||
// First tab in history
|
||||
return
|
||||
}
|
||||
|
||||
// Update page cache in history for #122
|
||||
t.historyCachePage()
|
||||
|
||||
t.history.pos--
|
||||
go applyHist(t)
|
||||
}
|
||||
|
|
|
@ -118,7 +118,7 @@ func setPage(t *tab, p *structs.Page) {
|
|||
tabNum := tabNumber(t)
|
||||
browser.AddTab(
|
||||
strconv.Itoa(tabNum),
|
||||
makeTabLabel(strconv.Itoa(tabNum+1)),
|
||||
t.label(),
|
||||
makeContentLayout(t.view, leftMargin()),
|
||||
)
|
||||
App.Draw()
|
||||
|
@ -137,6 +137,9 @@ func setPage(t *tab, p *structs.Page) {
|
|||
//
|
||||
// It should be called in a goroutine.
|
||||
func goURL(t *tab, u string) {
|
||||
// Update page cache in history for #122
|
||||
t.historyCachePage()
|
||||
|
||||
final, displayed := handleURL(t, u, 0)
|
||||
if displayed {
|
||||
t.addToHistory(final)
|
||||
|
|
|
@ -97,9 +97,12 @@ func Subscriptions(t *tab, u string) string {
|
|||
} else {
|
||||
// Render page
|
||||
|
||||
rawPage += "You can use Ctrl-X to subscribe to a page, or to an Atom/RSS/JSON feed. See the online wiki for more.\n" +
|
||||
"If you just opened Amfora then updates may appear incrementally. Reload the page to see them.\n\n" +
|
||||
"=> about:manage-subscriptions Manage subscriptions\n\n"
|
||||
if viper.GetBool("subscriptions.header") {
|
||||
rawPage += "You can use Ctrl-X to subscribe to a page, or to an Atom/RSS/JSON feed." +
|
||||
"See the online wiki for more.\n" +
|
||||
"If you just opened Amfora then updates may appear incrementally. Reload the page to see them.\n\n"
|
||||
}
|
||||
rawPage += "=> about:manage-subscriptions Manage subscriptions\n\n"
|
||||
|
||||
// curDay represents what day of posts the loop is on.
|
||||
// It only goes backwards in time.
|
||||
|
|
100
display/tab.go
100
display/tab.go
|
@ -3,6 +3,7 @@ package display
|
|||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
@ -20,19 +21,31 @@ const (
|
|||
tabModeLoading
|
||||
)
|
||||
|
||||
// tabHistoryPageCache is fields from the Page struct, cached here to solve #122
|
||||
// See structs/structs.go for an explanation of the fields.
|
||||
type tabHistoryPageCache struct {
|
||||
row int
|
||||
column int
|
||||
selected string
|
||||
selectedID string
|
||||
mode structs.PageMode
|
||||
}
|
||||
|
||||
type tabHistory struct {
|
||||
urls []string
|
||||
pos int // Position: where in the list of URLs we are
|
||||
urls []string
|
||||
pos int // Position: where in the list of URLs we are
|
||||
pageCache []*tabHistoryPageCache
|
||||
}
|
||||
|
||||
// tab hold the information needed for each browser tab.
|
||||
type tab struct {
|
||||
page *structs.Page
|
||||
view *cview.TextView
|
||||
history *tabHistory
|
||||
mode tabMode
|
||||
barLabel string // The bottomBar label for the tab
|
||||
barText string // The bottomBar text for the tab
|
||||
page *structs.Page
|
||||
view *cview.TextView
|
||||
history *tabHistory
|
||||
mode tabMode
|
||||
barLabel string // The bottomBar label for the tab
|
||||
barText string // The bottomBar text for the tab
|
||||
preferURLHandler bool // For #143, use URL handler over proxy
|
||||
}
|
||||
|
||||
// makeNewTab initializes an tab struct with no content.
|
||||
|
@ -86,6 +99,7 @@ func makeNewTab() *tab {
|
|||
linkN, _ := strconv.Atoi(currentSelection[0])
|
||||
tabs[tab].page.Selected = tabs[tab].page.Links[linkN]
|
||||
tabs[tab].page.SelectedID = currentSelection[0]
|
||||
tabs[tab].preferURLHandler = false // Reset in case
|
||||
go followLink(tabs[tab], tabs[tab].page.URL, tabs[tab].page.Links[linkN])
|
||||
return
|
||||
}
|
||||
|
@ -204,11 +218,24 @@ func makeNewTab() *tab {
|
|||
return nil
|
||||
}
|
||||
return nil
|
||||
case config.CmdURLHandlerOpen:
|
||||
currentSelection := t.view.GetHighlights()
|
||||
t.preferURLHandler = true
|
||||
// Copied code from when enter key is pressed
|
||||
if len(currentSelection) > 0 {
|
||||
bottomBar.SetLabel("")
|
||||
linkN, _ := strconv.Atoi(currentSelection[0])
|
||||
t.page.Selected = t.page.Links[linkN]
|
||||
t.page.SelectedID = currentSelection[0]
|
||||
go followLink(&t, t.page.URL, t.page.Links[linkN])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// Number key: 1-9, 0, LINK1-LINK10
|
||||
if cmd >= config.CmdLink1 && cmd <= config.CmdLink0 {
|
||||
if int(cmd) <= len(t.page.Links) {
|
||||
// It's a valid link number
|
||||
t.preferURLHandler = false // Reset in case
|
||||
go followLink(&t, t.page.URL, t.page.Links[cmd-1])
|
||||
return nil
|
||||
}
|
||||
|
@ -313,6 +340,21 @@ func makeNewTab() *tab {
|
|||
return &t
|
||||
}
|
||||
|
||||
// historyCachePage caches certain info about the current page in the tab's history,
|
||||
// see #122 for details.
|
||||
func (t *tab) historyCachePage() {
|
||||
if t.page == nil || t.page.URL == "" || t.history.pageCache == nil || len(t.history.pageCache) == 0 {
|
||||
return
|
||||
}
|
||||
t.history.pageCache[t.history.pos] = &tabHistoryPageCache{
|
||||
row: t.page.Row,
|
||||
column: t.page.Column,
|
||||
selected: t.page.Selected,
|
||||
selectedID: t.page.SelectedID,
|
||||
mode: t.page.Mode,
|
||||
}
|
||||
}
|
||||
|
||||
// addToHistory adds the given URL to history.
|
||||
// It assumes the URL is currently being loaded and displayed on the page.
|
||||
func (t *tab) addToHistory(u string) {
|
||||
|
@ -320,9 +362,15 @@ func (t *tab) addToHistory(u string) {
|
|||
// We're somewhere in the middle of the history instead, with URLs ahead and behind.
|
||||
// The URLs ahead need to be removed so this new URL is the most recent item in the history
|
||||
t.history.urls = t.history.urls[:t.history.pos+1]
|
||||
// Same for page cache
|
||||
t.history.pageCache = t.history.pageCache[:t.history.pos+1]
|
||||
}
|
||||
t.history.urls = append(t.history.urls, u)
|
||||
t.history.pos++
|
||||
|
||||
// Cache page info for #122
|
||||
t.history.pageCache = append(t.history.pageCache, &tabHistoryPageCache{}) // Add new spot
|
||||
t.historyCachePage() // Fill it with data
|
||||
}
|
||||
|
||||
// pageUp scrolls up 75% of the height of the terminal, like Bombadillo.
|
||||
|
@ -380,7 +428,7 @@ func (t *tab) applyHorizontalScroll() {
|
|||
// Scrolled to the right far enough that no left margin is needed
|
||||
browser.AddTab(
|
||||
strconv.Itoa(i),
|
||||
makeTabLabel(strconv.Itoa(i+1)),
|
||||
t.label(),
|
||||
makeContentLayout(t.view, 0),
|
||||
)
|
||||
t.view.ScrollTo(t.page.Row, t.page.Column-leftMargin())
|
||||
|
@ -388,7 +436,7 @@ func (t *tab) applyHorizontalScroll() {
|
|||
// Left margin is still needed, but is not necessarily at the right size by default
|
||||
browser.AddTab(
|
||||
strconv.Itoa(i),
|
||||
makeTabLabel(strconv.Itoa(i+1)),
|
||||
t.label(),
|
||||
makeContentLayout(t.view, leftMargin()-t.page.Column),
|
||||
)
|
||||
}
|
||||
|
@ -499,3 +547,35 @@ func (t *tab) highlightedURL() string {
|
|||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// label returns the label to use for the tab name
|
||||
func (t *tab) label() string {
|
||||
tn := tabNumber(t)
|
||||
if tn < 0 {
|
||||
// Invalid tab, shouldn't happen
|
||||
return ""
|
||||
}
|
||||
|
||||
// Increment so there's no tab 0 in the label
|
||||
tn++
|
||||
|
||||
if t.page.URL == "" || t.page.URL == "about:newtab" {
|
||||
// Just use tab number
|
||||
// Spaces around to keep original Amfora look
|
||||
return fmt.Sprintf(" %d ", tn)
|
||||
}
|
||||
if strings.HasPrefix(t.page.URL, "about:") {
|
||||
// Don't look for domain, put the whole URL except query strings
|
||||
return strings.SplitN(t.page.URL, "?", 2)[0]
|
||||
}
|
||||
if strings.HasPrefix(t.page.URL, "file://") {
|
||||
// File URL, use file or folder as tab name
|
||||
return path.Base(t.page.URL[7:])
|
||||
}
|
||||
// Otherwise, it's a Gemini URL
|
||||
pu, err := url.Parse(t.page.URL)
|
||||
if err != nil {
|
||||
return fmt.Sprintf(" %d ", tn)
|
||||
}
|
||||
return pu.Host
|
||||
}
|
||||
|
|
|
@ -6,9 +6,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"code.rocketnine.space/tslocum/cview"
|
||||
"github.com/makeworld-the-better-one/go-gemini"
|
||||
"github.com/spf13/viper"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
|
||||
// This file contains funcs that are small, self-contained utilities.
|
||||
|
@ -34,12 +32,6 @@ func makeContentLayout(tv *cview.TextView, leftMargin int) *cview.Flex {
|
|||
return vert
|
||||
}
|
||||
|
||||
// makeTabLabel takes a string and adds spacing to it, making it
|
||||
// suitable for display as a tab label.
|
||||
func makeTabLabel(s string) string {
|
||||
return " " + s + " "
|
||||
}
|
||||
|
||||
// tabNumber gets the index of the tab in the tabs slice. It returns -1
|
||||
// if the tab is not in that slice.
|
||||
func tabNumber(t *tab) int {
|
||||
|
@ -106,81 +98,3 @@ func resolveRelLink(t *tab, prev, next string) (string, error) {
|
|||
}
|
||||
return prevParsed.ResolveReference(nextParsed).String(), nil
|
||||
}
|
||||
|
||||
// normalizeURL attempts to make URLs that are different strings
|
||||
// but point to the same place all look the same.
|
||||
//
|
||||
// Example: gemini://gus.guru:1965/ and //gus.guru/.
|
||||
// This function will take both output the same URL each time.
|
||||
//
|
||||
// It will also percent-encode invalid characters, and decode chars
|
||||
// that don't need to be encoded. It will also apply Unicode NFC
|
||||
// normalization.
|
||||
//
|
||||
// The string passed must already be confirmed to be a URL.
|
||||
// Detection of a search string vs. a URL must happen elsewhere.
|
||||
//
|
||||
// It only works with absolute URLs.
|
||||
func normalizeURL(u string) string {
|
||||
u = norm.NFC.String(u)
|
||||
|
||||
tmp, err := gemini.GetPunycodeURL(u)
|
||||
if err != nil {
|
||||
return u
|
||||
}
|
||||
u = tmp
|
||||
parsed, _ := url.Parse(u)
|
||||
|
||||
if parsed.Scheme == "" {
|
||||
// Always add scheme
|
||||
parsed.Scheme = "gemini"
|
||||
} else if parsed.Scheme != "gemini" {
|
||||
// Not a gemini URL, nothing to do
|
||||
return u
|
||||
}
|
||||
|
||||
parsed.User = nil // No passwords in Gemini
|
||||
parsed.Fragment = "" // No fragments either
|
||||
if parsed.Port() == "1965" {
|
||||
// Always remove default port
|
||||
hostname := parsed.Hostname()
|
||||
if strings.Contains(hostname, ":") {
|
||||
parsed.Host = "[" + parsed.Hostname() + "]"
|
||||
} else {
|
||||
parsed.Host = parsed.Hostname()
|
||||
}
|
||||
}
|
||||
|
||||
// Add slash to the end of a URL with just a domain
|
||||
// gemini://example.com -> gemini://example.com/
|
||||
if parsed.Path == "" {
|
||||
parsed.Path = "/"
|
||||
} else {
|
||||
// Decode and re-encode path
|
||||
// This removes needless encoding, like that of ASCII chars
|
||||
// And encodes anything that wasn't but should've been
|
||||
parsed.RawPath = strings.ReplaceAll(url.PathEscape(parsed.Path), "%2F", "/")
|
||||
}
|
||||
|
||||
// Do the same to the query string
|
||||
un, err := gemini.QueryUnescape(parsed.RawQuery)
|
||||
if err == nil {
|
||||
parsed.RawQuery = gemini.QueryEscape(un)
|
||||
}
|
||||
|
||||
return parsed.String()
|
||||
}
|
||||
|
||||
// fixUserURL will take a user-typed URL and add a gemini scheme to it if
|
||||
// necessary. It is not the same as normalizeURL, and that func should still
|
||||
// be used, afterward.
|
||||
//
|
||||
// For example "example.com" will become "gemini://example.com", but
|
||||
// "//example.com" will be left untouched.
|
||||
func fixUserURL(u string) string {
|
||||
if !strings.HasPrefix(u, "//") && !strings.HasPrefix(u, "gemini://") && !strings.Contains(u, "://") {
|
||||
// Assume it's a Gemini URL
|
||||
u = "gemini://" + u
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
|
3
go.mod
3
go.mod
|
@ -10,7 +10,8 @@ require (
|
|||
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
||||
github.com/gdamore/tcell/v2 v2.3.3
|
||||
github.com/google/go-cmp v0.5.0 // indirect
|
||||
github.com/makeworld-the-better-one/go-gemini v0.12.1
|
||||
github.com/makeworld-the-better-one/go-gemini v0.13.0
|
||||
github.com/makeworld-the-better-one/go-gemini-socks5 v1.0.0
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/mitchellh/mapstructure v1.3.1 // indirect
|
||||
github.com/mmcdole/gofeed v1.1.2
|
||||
|
|
11
go.sum
11
go.sum
|
@ -151,12 +151,13 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69
|
|||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.12.1 h1:cWHvCHL31Caq3Rm9elCFFoQeyrn92Kv7KummsVxCOFg=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.12.1/go.mod h1:F+3x+R1xeYK90jMtBq+U+8Sh64r2dHleDZ/en3YgSmg=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.13.0 h1:CwUKLldUlZwWolG1/xKkYVgerCzlxwi0OFOh216GTHk=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.13.0/go.mod h1:F+3x+R1xeYK90jMtBq+U+8Sh64r2dHleDZ/en3YgSmg=
|
||||
github.com/makeworld-the-better-one/go-gemini-socks5 v1.0.0 h1:D2o1rIfP/KOxcL3m3rzo4cfWNqfcGaMIhnU0keJc1+o=
|
||||
github.com/makeworld-the-better-one/go-gemini-socks5 v1.0.0/go.mod h1:mfPK9BfBAAyLKuxPEbZi8mgrGmVlzMKVTGElVspuVR8=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA=
|
||||
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
|
@ -306,8 +307,9 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
|
|||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201216054612-986b41b23924 h1:QsnDpLLOKwHBBDa8nDws4DYNc/ryVW2vCpxCs09d4PY=
|
||||
golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM=
|
||||
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
@ -337,6 +339,7 @@ golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210223095934-7937bea0104d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea h1:+WiDlPBBaO+h9vPNZi8uJ3k4BkKQB7Iow3aqwHVA5hI=
|
||||
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
|
|
|
@ -9,11 +9,11 @@ import (
|
|||
"os"
|
||||
)
|
||||
|
||||
var logger *log.Logger
|
||||
var Logger *log.Logger
|
||||
|
||||
func GetLogger() (*log.Logger, error) {
|
||||
if logger != nil {
|
||||
return logger, nil
|
||||
if Logger != nil {
|
||||
return Logger, nil
|
||||
}
|
||||
|
||||
var writer io.Writer
|
||||
|
@ -30,15 +30,15 @@ func GetLogger() (*log.Logger, error) {
|
|||
writer = ioutil.Discard
|
||||
}
|
||||
|
||||
logger = log.New(writer, "", log.LstdFlags)
|
||||
Logger = log.New(writer, "", log.LstdFlags)
|
||||
|
||||
if !debugModeEnabled {
|
||||
// Clear all flags to skip log output formatting step to increase
|
||||
// performance somewhat if we're not logging anything
|
||||
logger.SetFlags(0)
|
||||
Logger.SetFlags(0)
|
||||
}
|
||||
|
||||
logger.Println("Started logger")
|
||||
Logger.Println("Started logger")
|
||||
|
||||
return logger, nil
|
||||
return Logger, nil
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user