mirror of
https://github.com/makeworld-the-better-one/amfora.git
synced 2024-09-11 22:57:37 +03:00
Merge branch 'master' into feeds
This commit is contained in:
commit
ba28b2b5f9
13
CHANGELOG.md
13
CHANGELOG.md
@ -5,9 +5,18 @@ 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]
|
||||||
|
### Added
|
||||||
|
- Opening local files with `file://` URIs (#103, #117)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Updated [go-gemini](https://github.com/makeworld-the-better-one/go-gemini) to v0.9.1 to support CN-only wildcard certs
|
- Updated [go-gemini](https://github.com/makeworld-the-better-one/go-gemini) to v0.9.3
|
||||||
- Preformatted text is now grey by default
|
- Supports CN-only wildcard certs
|
||||||
|
- Time out when header takes too long
|
||||||
|
- Preformatted text is now light yellow by default
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Single quotes are used in the default config for commands and paths so that Windows paths with backslashes will be parsed correctly
|
||||||
|
- Downloading now uses proxies when appropriate
|
||||||
|
|
||||||
|
|
||||||
## [1.6.0] - 2020-11-04
|
## [1.6.0] - 2020-11-04
|
||||||
|
2
NOTES.md
2
NOTES.md
@ -1,8 +1,6 @@
|
|||||||
# Notes
|
# Notes
|
||||||
|
|
||||||
## Feeds (temp)
|
## Feeds (temp)
|
||||||
- Support multiple links in Atom feeds
|
|
||||||
- Display gemini one if it exist, then HTTP(S), then whatever is first
|
|
||||||
- TODO: remove all logger lines
|
- TODO: remove all logger lines
|
||||||
|
|
||||||
## Issues
|
## Issues
|
||||||
|
@ -116,6 +116,7 @@ Features in *italics* are in the master branch, but not in the latest release.
|
|||||||
- [x] Bookmarks
|
- [x] Bookmarks
|
||||||
- [x] Download pages and arbitrary data
|
- [x] Download pages and arbitrary data
|
||||||
- [x] Theming
|
- [x] Theming
|
||||||
|
- Check out the [user contributed themes](https://github.com/makeworld-the-better-one/amfora/tree/master/contrib/themes)!
|
||||||
- [x] Emoji favicons
|
- [x] Emoji favicons
|
||||||
- See `gemini://mozz.us/files/rfc_gemini_favicon.gmi` for details
|
- See `gemini://mozz.us/files/rfc_gemini_favicon.gmi` for details
|
||||||
- Disabled by default, enable in config
|
- Disabled by default, enable in config
|
||||||
|
@ -10,4 +10,5 @@ Thank you to the following contributors, who have helped make Amfora great. FOSS
|
|||||||
- Timur Ismagilov (@bouncepaw)
|
- Timur Ismagilov (@bouncepaw)
|
||||||
- Matt Caroll (@ohiolab)
|
- Matt Caroll (@ohiolab)
|
||||||
- Patryk Niedźwiedziński (@pniedzwiedzinski)
|
- Patryk Niedźwiedziński (@pniedzwiedzinski)
|
||||||
- Trevor Slocum (@tsclocum)
|
- Trevor Slocum (@tsclocum)
|
||||||
|
- Mattias Jadelius (@jedthehumanoid)
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/makeworld-the-better-one/amfora/client"
|
||||||
"github.com/makeworld-the-better-one/amfora/config"
|
"github.com/makeworld-the-better-one/amfora/config"
|
||||||
"github.com/makeworld-the-better-one/amfora/display"
|
"github.com/makeworld-the-better-one/amfora/display"
|
||||||
"github.com/makeworld-the-better-one/amfora/feeds"
|
"github.com/makeworld-the-better-one/amfora/feeds"
|
||||||
@ -50,6 +51,8 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
client.Init()
|
||||||
|
|
||||||
display.Init()
|
display.Init()
|
||||||
display.NewTab()
|
display.NewTab()
|
||||||
display.NewTab() // Open extra tab and close it to fully initialize the app and wrapping
|
display.NewTab() // Open extra tab and close it to fully initialize the app and wrapping
|
||||||
|
@ -6,14 +6,31 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/makeworld-the-better-one/go-gemini"
|
"github.com/makeworld-the-better-one/go-gemini"
|
||||||
"github.com/mitchellh/go-homedir"
|
"github.com/mitchellh/go-homedir"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
var certCache = make(map[string][][]byte)
|
var (
|
||||||
var certCacheMu = &sync.RWMutex{}
|
certCache = make(map[string][][]byte)
|
||||||
|
certCacheMu = &sync.RWMutex{}
|
||||||
|
|
||||||
|
fetchClient *gemini.Client
|
||||||
|
dlClient *gemini.Client // For downloading
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
fetchClient = &gemini.Client{
|
||||||
|
ConnectTimeout: 10 * time.Second, // Default is 15
|
||||||
|
ReadTimeout: time.Duration(viper.GetInt("a-general.page_max_time")) * time.Second,
|
||||||
|
}
|
||||||
|
dlClient = &gemini.Client{
|
||||||
|
ConnectTimeout: 10 * time.Second, // Default is 15
|
||||||
|
// No read timeout, download can take as long as it needs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func clientCert(host string) ([]byte, []byte) {
|
func clientCert(host string) ([]byte, []byte) {
|
||||||
certCacheMu.RLock()
|
certCacheMu.RLock()
|
||||||
@ -66,18 +83,16 @@ func HasClientCert(host string) bool {
|
|||||||
return cert != nil
|
return cert != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch returns response data and an error.
|
func fetch(u string, c *gemini.Client) (*gemini.Response, error) {
|
||||||
// The error text is human friendly and should be displayed.
|
|
||||||
func Fetch(u string) (*gemini.Response, error) {
|
|
||||||
parsed, _ := url.Parse(u)
|
parsed, _ := url.Parse(u)
|
||||||
cert, key := clientCert(parsed.Host)
|
cert, key := clientCert(parsed.Host)
|
||||||
|
|
||||||
var res *gemini.Response
|
var res *gemini.Response
|
||||||
var err error
|
var err error
|
||||||
if cert != nil {
|
if cert != nil {
|
||||||
res, err = gemini.FetchWithCert(u, cert, key)
|
res, err = c.FetchWithCert(u, cert, key)
|
||||||
} else {
|
} else {
|
||||||
res, err = gemini.Fetch(u)
|
res, err = c.Fetch(u)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -91,17 +106,27 @@ func Fetch(u string) (*gemini.Response, error) {
|
|||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchWithProxy is the same as Fetch, but uses a proxy.
|
// Fetch returns response data and an error.
|
||||||
func FetchWithProxy(proxyHostname, proxyPort, u string) (*gemini.Response, error) {
|
// The error text is human friendly and should be displayed.
|
||||||
|
func Fetch(u string) (*gemini.Response, error) {
|
||||||
|
return fetch(u, fetchClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download is the same as Fetch but with no read timeout.
|
||||||
|
func Download(u string) (*gemini.Response, error) {
|
||||||
|
return fetch(u, dlClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchWithProxy(proxyHostname, proxyPort, u string, c *gemini.Client) (*gemini.Response, error) {
|
||||||
parsed, _ := url.Parse(u)
|
parsed, _ := url.Parse(u)
|
||||||
cert, key := clientCert(parsed.Host)
|
cert, key := clientCert(parsed.Host)
|
||||||
|
|
||||||
var res *gemini.Response
|
var res *gemini.Response
|
||||||
var err error
|
var err error
|
||||||
if cert != nil {
|
if cert != nil {
|
||||||
res, err = gemini.FetchWithHostAndCert(net.JoinHostPort(proxyHostname, proxyPort), u, cert, key)
|
res, err = c.FetchWithHostAndCert(net.JoinHostPort(proxyHostname, proxyPort), u, cert, key)
|
||||||
} else {
|
} else {
|
||||||
res, err = gemini.FetchWithHost(net.JoinHostPort(proxyHostname, proxyPort), u)
|
res, err = c.FetchWithHost(net.JoinHostPort(proxyHostname, proxyPort), u)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -115,3 +140,13 @@ func FetchWithProxy(proxyHostname, proxyPort, u string) (*gemini.Response, error
|
|||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FetchWithProxy is the same as Fetch, but uses a proxy.
|
||||||
|
func FetchWithProxy(proxyHostname, proxyPort, u string) (*gemini.Response, error) {
|
||||||
|
return fetchWithProxy(proxyHostname, proxyPort, u, fetchClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DownloadWithProxy is the same as FetchWithProxy but with no read timeout.
|
||||||
|
func DownloadWithProxy(proxyHostname, proxyPort, u string) (*gemini.Response, error) {
|
||||||
|
return fetchWithProxy(proxyHostname, proxyPort, u, dlClient)
|
||||||
|
}
|
||||||
|
@ -28,14 +28,15 @@ auto_redirect = false
|
|||||||
#
|
#
|
||||||
# The best to define a command is using a string array.
|
# The best to define a command is using a string array.
|
||||||
# Examples:
|
# Examples:
|
||||||
# http = ["firefox"]
|
# http = ['firefox']
|
||||||
# http = ["custom-browser", "--flag", "--option=2"]
|
# http = ['custom-browser', '--flag', '--option=2']
|
||||||
# http = ["/path/with spaces/in it/firefox"]
|
# http = ['/path/with spaces/in it/firefox']
|
||||||
#
|
#
|
||||||
# Using just a string will also work, but it is deprecated,
|
# Note the use of single quotes, so that backslashes will not be escaped.
|
||||||
# and will degrade if you use paths with spaces.
|
# 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
|
||||||
search = "gemini://gus.guru/search"
|
search = "gemini://gus.guru/search"
|
||||||
@ -58,7 +59,8 @@ max_width = 100
|
|||||||
# 'downloads' is the path to a downloads folder.
|
# 'downloads' is the path to a downloads folder.
|
||||||
# An empty value means the code will find the default downloads folder for your system.
|
# An empty value means the code will find the default downloads folder for your system.
|
||||||
# If the path does not exist it will be created.
|
# If the path does not exist it will be created.
|
||||||
downloads = ""
|
# Note the use of single quotes, so that backslashes will not be escaped.
|
||||||
|
downloads = ''
|
||||||
|
|
||||||
# Max size for displayable content in bytes - after that size a download window pops up
|
# Max size for displayable content in bytes - after that size a download window pops up
|
||||||
page_max_size = 2097152 # 2 MiB
|
page_max_size = 2097152 # 2 MiB
|
||||||
@ -71,16 +73,17 @@ emoji_favicons = false
|
|||||||
|
|
||||||
[auth]
|
[auth]
|
||||||
# Authentication settings
|
# Authentication settings
|
||||||
|
# Note the use of single quotes for values, so that backslashes will not be escaped.
|
||||||
|
|
||||||
[auth.certs]
|
[auth.certs]
|
||||||
# Client certificates
|
# Client certificates
|
||||||
# Set domain name equal to path to client cert
|
# Set domain name equal to path to client cert
|
||||||
# "example.com" = "mycert.crt"
|
# "example.com" = 'mycert.crt'
|
||||||
|
|
||||||
[auth.keys]
|
[auth.keys]
|
||||||
# Client certificate keys
|
# Client certificate keys
|
||||||
# Set domain name equal to path to key for the client cert above
|
# Set domain name equal to path to key for the client cert above
|
||||||
# "example.com" = "mycert.key"
|
# "example.com" = 'mycert.key'
|
||||||
|
|
||||||
|
|
||||||
[keybindings]
|
[keybindings]
|
||||||
@ -94,18 +97,19 @@ shift_numbers = "!@#$%^&*()"
|
|||||||
[url-handlers]
|
[url-handlers]
|
||||||
# Allows setting the commands to run for various URL schemes.
|
# Allows setting the commands to run for various URL schemes.
|
||||||
# E.g. to open FTP URLs with FileZilla set the following key:
|
# E.g. to open FTP URLs with FileZilla set the following key:
|
||||||
# ftp = "filezilla"
|
# ftp = 'filezilla'
|
||||||
# You can set any scheme to "off" or "" to disable handling it, or
|
# You can set any scheme to "off" or "" to disable handling it, or
|
||||||
# just leave the key unset.
|
# just leave the key unset.
|
||||||
#
|
#
|
||||||
# DO NOT use this for setting the HTTP command.
|
# DO NOT use this for setting the HTTP command.
|
||||||
# Use the http setting in the "a-general" section above.
|
# Use the http setting in the "a-general" section above.
|
||||||
#
|
#
|
||||||
# NOTE: These settings are override by the ones in the proxies section.
|
# NOTE: These settings are overrided by the ones in the proxies section.
|
||||||
|
# Note the use of single quotes, so that backslashes will not be escaped.
|
||||||
|
|
||||||
# This is a special key that defines the handler for all URL schemes for which
|
# This is a special key that defines the handler for all URL schemes for which
|
||||||
# no handler is defined.
|
# no handler is defined.
|
||||||
other = "off"
|
other = 'off'
|
||||||
|
|
||||||
|
|
||||||
[cache]
|
[cache]
|
||||||
|
@ -60,7 +60,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.ColorGrey,
|
"preformatted_text": tcell.Color229, // xterm:Wheat1, #ffffaf
|
||||||
"list_text": tcell.ColorWhite,
|
"list_text": tcell.ColorWhite,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
26
contrib/themes/README.md
Normal file
26
contrib/themes/README.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# User Contributed Themes
|
||||||
|
|
||||||
|
You can use these themes by replacing the `[theme]` section of your config with their contents. Some themes won't display properly on terminals that do not have truecolor support.
|
||||||
|
|
||||||
|
## Nord
|
||||||
|
|
||||||
|
Contributed by **[@lokesh-krishna](https://github.com/lokesh-krishna)**.
|
||||||
|
|
||||||
|
![screenshot of the nord theme](https://user-images.githubusercontent.com/20235646/99020443-a93a1980-2584-11eb-8028-0b95cfcf0fc6.png)
|
||||||
|
|
||||||
|
## Dracula
|
||||||
|
|
||||||
|
Contributed by **[@crdpa](https://github.com/crdpa)**.
|
||||||
|
|
||||||
|
![screenshot of dracula theme](https://user-images.githubusercontent.com/61637474/99983229-5b928d80-2d8a-11eb-8e5c-e5681bb274c5.png)
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>More screenshots</summary>
|
||||||
|
|
||||||
|
![screenshot of dracula theme](https://user-images.githubusercontent.com/61637474/99983237-5e8d7e00-2d8a-11eb-8e22-3a3459ae560a.png)
|
||||||
|
![screenshot of dracula theme](https://user-images.githubusercontent.com/61637474/99983210-53d2e900-2d8a-11eb-9ab7-12dc10c2933a.png)
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## Yours?
|
||||||
|
|
||||||
|
Contribute your own theme by opening a PR.
|
103
contrib/themes/dracula.toml
Normal file
103
contrib/themes/dracula.toml
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
[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 = "#282a36"
|
||||||
|
fg = "#f8f8f2"
|
||||||
|
tab_num = "#50fa7b"
|
||||||
|
tab_divider = "#f8f8f2"
|
||||||
|
bottombar_bg = "#282a36"
|
||||||
|
bottombar_text = "#f8f8f2"
|
||||||
|
bottombar_label = "#9aedfe"
|
||||||
|
|
||||||
|
# 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 = "#5af78e"
|
||||||
|
hdg_2 = "#9aedfe"
|
||||||
|
hdg_3 = "#caa9fa"
|
||||||
|
amfora_link = "#f4f99d"
|
||||||
|
foreign_link = "#d4d989"
|
||||||
|
link_number = "#ff5555"
|
||||||
|
regular_text = "#f8f8f2"
|
||||||
|
quote_text = "#E6E6E6"
|
||||||
|
preformatted_text = "#f8f8f2"
|
||||||
|
list_text = "#f8f8f2"
|
||||||
|
|
||||||
|
# btn_bg: The bg color for all modal buttons
|
||||||
|
# btn_text: The text color for all modal buttons
|
||||||
|
|
||||||
|
btn_bg = "#bfbfbf"
|
||||||
|
btn_text = "#4d4d4d"
|
||||||
|
|
||||||
|
dl_choice_modal_bg = "#282a36"
|
||||||
|
dl_choice_modal_text = "#f8f8f2"
|
||||||
|
dl_modal_bg = "#282a36"
|
||||||
|
dl_modal_text = "#f8f8f2"
|
||||||
|
info_modal_bg = "#282a36"
|
||||||
|
info_modal_text = "#f8f8f2"
|
||||||
|
error_modal_bg = "#282a36"
|
||||||
|
error_modal_text = "#ff5555"
|
||||||
|
yesno_modal_bg = "#282a36"
|
||||||
|
yesno_modal_text = "#f1fa8c"
|
||||||
|
tofu_modal_bg = "#282a36"
|
||||||
|
tofu_modal_text = "#f8f8f2"
|
||||||
|
|
||||||
|
# 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 = "#282a36"
|
||||||
|
input_modal_text = "#f8f8f2"
|
||||||
|
input_modal_field_bg = "#4d4d4d"
|
||||||
|
input_modal_field_text ="#f8f8f2"
|
||||||
|
|
||||||
|
# bkmk_modal_bg
|
||||||
|
# bkmk_modal_text
|
||||||
|
# bkmk_modal_label
|
||||||
|
# bkmk_modal_field_bg
|
||||||
|
# bkmk_modal_field_text
|
||||||
|
|
||||||
|
bkmk_modal_bg = "#282a36"
|
||||||
|
bkmk_modal_text = "#f8f8f2"
|
||||||
|
bkmk_modal_label = "#f8f8f2"
|
||||||
|
bkmk_modal_field_bg = "#000000"
|
||||||
|
bkmk_modal_field_text = "#f8f8f2"
|
||||||
|
|
@ -25,14 +25,15 @@ auto_redirect = false
|
|||||||
#
|
#
|
||||||
# The best to define a command is using a string array.
|
# The best to define a command is using a string array.
|
||||||
# Examples:
|
# Examples:
|
||||||
# http = ["firefox"]
|
# http = ['firefox']
|
||||||
# http = ["custom-browser", "--flag", "--option=2"]
|
# http = ['custom-browser', '--flag', '--option=2']
|
||||||
# http = ["/path/with spaces/in it/firefox"]
|
# http = ['/path/with spaces/in it/firefox']
|
||||||
#
|
#
|
||||||
# Using just a string will also work, but it is deprecated,
|
# Note the use of single quotes, so that backslashes will not be escaped.
|
||||||
# and will degrade if you use paths with spaces.
|
# 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
|
||||||
search = "gemini://gus.guru/search"
|
search = "gemini://gus.guru/search"
|
||||||
@ -55,7 +56,8 @@ max_width = 100
|
|||||||
# 'downloads' is the path to a downloads folder.
|
# 'downloads' is the path to a downloads folder.
|
||||||
# An empty value means the code will find the default downloads folder for your system.
|
# An empty value means the code will find the default downloads folder for your system.
|
||||||
# If the path does not exist it will be created.
|
# If the path does not exist it will be created.
|
||||||
downloads = ""
|
# Note the use of single quotes, so that backslashes will not be escaped.
|
||||||
|
downloads = ''
|
||||||
|
|
||||||
# Max size for displayable content in bytes - after that size a download window pops up
|
# Max size for displayable content in bytes - after that size a download window pops up
|
||||||
page_max_size = 2097152 # 2 MiB
|
page_max_size = 2097152 # 2 MiB
|
||||||
@ -68,16 +70,17 @@ emoji_favicons = false
|
|||||||
|
|
||||||
[auth]
|
[auth]
|
||||||
# Authentication settings
|
# Authentication settings
|
||||||
|
# Note the use of single quotes for values, so that backslashes will not be escaped.
|
||||||
|
|
||||||
[auth.certs]
|
[auth.certs]
|
||||||
# Client certificates
|
# Client certificates
|
||||||
# Set domain name equal to path to client cert
|
# Set domain name equal to path to client cert
|
||||||
# "example.com" = "mycert.crt"
|
# "example.com" = 'mycert.crt'
|
||||||
|
|
||||||
[auth.keys]
|
[auth.keys]
|
||||||
# Client certificate keys
|
# Client certificate keys
|
||||||
# Set domain name equal to path to key for the client cert above
|
# Set domain name equal to path to key for the client cert above
|
||||||
# "example.com" = "mycert.key"
|
# "example.com" = 'mycert.key'
|
||||||
|
|
||||||
|
|
||||||
[keybindings]
|
[keybindings]
|
||||||
@ -91,18 +94,19 @@ shift_numbers = "!@#$%^&*()"
|
|||||||
[url-handlers]
|
[url-handlers]
|
||||||
# Allows setting the commands to run for various URL schemes.
|
# Allows setting the commands to run for various URL schemes.
|
||||||
# E.g. to open FTP URLs with FileZilla set the following key:
|
# E.g. to open FTP URLs with FileZilla set the following key:
|
||||||
# ftp = "filezilla"
|
# ftp = 'filezilla'
|
||||||
# You can set any scheme to "off" or "" to disable handling it, or
|
# You can set any scheme to "off" or "" to disable handling it, or
|
||||||
# just leave the key unset.
|
# just leave the key unset.
|
||||||
#
|
#
|
||||||
# DO NOT use this for setting the HTTP command.
|
# DO NOT use this for setting the HTTP command.
|
||||||
# Use the http setting in the "a-general" section above.
|
# Use the http setting in the "a-general" section above.
|
||||||
#
|
#
|
||||||
# NOTE: These settings are override by the ones in the proxies section.
|
# NOTE: These settings are overrided by the ones in the proxies section.
|
||||||
|
# Note the use of single quotes, so that backslashes will not be escaped.
|
||||||
|
|
||||||
# This is a special key that defines the handler for all URL schemes for which
|
# This is a special key that defines the handler for all URL schemes for which
|
||||||
# no handler is defined.
|
# no handler is defined.
|
||||||
other = "off"
|
other = 'off'
|
||||||
|
|
||||||
|
|
||||||
[cache]
|
[cache]
|
||||||
|
116
display/file.go
Normal file
116
display/file.go
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
package display
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"mime"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/makeworld-the-better-one/amfora/renderer"
|
||||||
|
"github.com/makeworld-the-better-one/amfora/structs"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// handleFile handles urls using file:// protocol
|
||||||
|
func handleFile(u string) (*structs.Page, bool) {
|
||||||
|
page := &structs.Page{}
|
||||||
|
|
||||||
|
uri, err := url.ParseRequestURI(u)
|
||||||
|
if err != nil {
|
||||||
|
Error("File Error", "Cannot parse URI: "+err.Error())
|
||||||
|
return page, false
|
||||||
|
}
|
||||||
|
fi, err := os.Stat(uri.Path)
|
||||||
|
if err != nil {
|
||||||
|
Error("File Error", "Cannot open local file: "+err.Error())
|
||||||
|
return page, false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch mode := fi.Mode(); {
|
||||||
|
case mode.IsDir():
|
||||||
|
return createDirectoryListing(u)
|
||||||
|
case mode.IsRegular():
|
||||||
|
if fi.Size() > viper.GetInt64("a-general.page_max_size") {
|
||||||
|
Error("File Error", "Cannot open local file, exceeds page max size")
|
||||||
|
return page, false
|
||||||
|
}
|
||||||
|
|
||||||
|
mimetype := mime.TypeByExtension(filepath.Ext(uri.Path))
|
||||||
|
if strings.HasSuffix(u, ".gmi") || strings.HasSuffix(u, ".gemini") {
|
||||||
|
mimetype = "text/gemini"
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(mimetype, "text/") {
|
||||||
|
Error("File Error", "Cannot open file, not recognized as text.")
|
||||||
|
return page, false
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := ioutil.ReadFile(uri.Path)
|
||||||
|
if err != nil {
|
||||||
|
Error("File Error", "Cannot open local file: "+err.Error())
|
||||||
|
return page, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if mimetype == "text/gemini" {
|
||||||
|
rendered, links := renderer.RenderGemini(string(content), textWidth(), leftMargin(), false)
|
||||||
|
page = &structs.Page{
|
||||||
|
Mediatype: structs.TextGemini,
|
||||||
|
URL: u,
|
||||||
|
Raw: string(content),
|
||||||
|
Content: rendered,
|
||||||
|
Links: links,
|
||||||
|
Width: termW,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
page = &structs.Page{
|
||||||
|
Mediatype: structs.TextPlain,
|
||||||
|
URL: u,
|
||||||
|
Raw: string(content),
|
||||||
|
Content: renderer.RenderPlainText(string(content), leftMargin()),
|
||||||
|
Links: []string{},
|
||||||
|
Width: termW,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return page, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// createDirectoryListing creates a text/gemini page for a directory
|
||||||
|
// that lists all the files as links.
|
||||||
|
func createDirectoryListing(u string) (*structs.Page, bool) {
|
||||||
|
page := &structs.Page{}
|
||||||
|
|
||||||
|
uri, err := url.ParseRequestURI(u)
|
||||||
|
if err != nil {
|
||||||
|
Error("Directory Error", "Cannot parse URI: "+err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
files, err := ioutil.ReadDir(uri.Path)
|
||||||
|
if err != nil {
|
||||||
|
Error("Directory error", "Cannot open local directory: "+err.Error())
|
||||||
|
return page, false
|
||||||
|
}
|
||||||
|
content := "Index of " + uri.Path + "\n"
|
||||||
|
content += "=> ../ ../\n"
|
||||||
|
for _, f := range files {
|
||||||
|
separator := ""
|
||||||
|
if f.IsDir() {
|
||||||
|
separator = "/"
|
||||||
|
}
|
||||||
|
content += fmt.Sprintf("=> %s%s %s%s\n", f.Name(), separator, f.Name(), separator)
|
||||||
|
}
|
||||||
|
|
||||||
|
rendered, links := renderer.RenderGemini(content, textWidth(), leftMargin(), false)
|
||||||
|
page = &structs.Page{
|
||||||
|
Mediatype: structs.TextGemini,
|
||||||
|
URL: u,
|
||||||
|
Raw: content,
|
||||||
|
Content: rendered,
|
||||||
|
Links: links,
|
||||||
|
Width: termW,
|
||||||
|
}
|
||||||
|
return page, true
|
||||||
|
}
|
@ -86,7 +86,9 @@ func reformatPage(p *structs.Page) {
|
|||||||
case structs.TextGemini:
|
case structs.TextGemini:
|
||||||
// Links are not recorded because they won't change
|
// Links are not recorded because they won't change
|
||||||
proxied := true
|
proxied := true
|
||||||
if strings.HasPrefix(p.URL, "gemini") || strings.HasPrefix(p.URL, "about") {
|
if strings.HasPrefix(p.URL, "gemini") ||
|
||||||
|
strings.HasPrefix(p.URL, "about") ||
|
||||||
|
strings.HasPrefix(p.URL, "file") {
|
||||||
proxied = false
|
proxied = false
|
||||||
}
|
}
|
||||||
rendered, _ = renderer.RenderGemini(p.Raw, textWidth(), leftMargin(), proxied)
|
rendered, _ = renderer.RenderGemini(p.Raw, textWidth(), leftMargin(), proxied)
|
||||||
@ -396,7 +398,16 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
|
|||||||
usingProxy = true
|
usingProxy = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.HasPrefix(u, "http") && !strings.HasPrefix(u, "gemini") {
|
if strings.HasPrefix(u, "file") {
|
||||||
|
page, ok := handleFile(u)
|
||||||
|
if !ok {
|
||||||
|
return ret("", false)
|
||||||
|
}
|
||||||
|
setPage(t, page)
|
||||||
|
return ret(u, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(u, "http") && !strings.HasPrefix(u, "gemini") && !strings.HasPrefix(u, "file") {
|
||||||
// Not a Gemini URL
|
// Not a Gemini URL
|
||||||
if proxy == "" || proxy == "off" {
|
if proxy == "" || proxy == "off" {
|
||||||
// No proxy available
|
// No proxy available
|
||||||
@ -467,24 +478,35 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
|
|||||||
return ret("", false)
|
return ret("", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var res2 *gemini.Response
|
||||||
|
var dlErr error
|
||||||
|
|
||||||
if errors.Is(err, renderer.ErrTooLarge) {
|
if errors.Is(err, renderer.ErrTooLarge) {
|
||||||
// Make new request for downloading purposes
|
// Make new request for downloading purposes
|
||||||
res, clientErr := client.Fetch(u)
|
if usingProxy {
|
||||||
if clientErr != nil && !errors.Is(clientErr, client.ErrTofu) {
|
res2, dlErr = client.DownloadWithProxy(proxyHostname, proxyPort, u)
|
||||||
|
} else {
|
||||||
|
res2, dlErr = client.Download(u)
|
||||||
|
}
|
||||||
|
if dlErr != nil && !errors.Is(dlErr, client.ErrTofu) {
|
||||||
Error("URL Fetch Error", err.Error())
|
Error("URL Fetch Error", err.Error())
|
||||||
return ret("", false)
|
return ret("", false)
|
||||||
}
|
}
|
||||||
go dlChoice("That page is too large. What would you like to do?", u, res)
|
go dlChoice("That page is too large. What would you like to do?", u, res2)
|
||||||
return ret("", false)
|
return ret("", false)
|
||||||
}
|
}
|
||||||
if errors.Is(err, renderer.ErrTimedOut) {
|
if errors.Is(err, renderer.ErrTimedOut) {
|
||||||
// Make new request for downloading purposes
|
// Make new request for downloading purposes
|
||||||
res, clientErr := client.Fetch(u)
|
if usingProxy {
|
||||||
if clientErr != nil && !errors.Is(clientErr, client.ErrTofu) {
|
res2, dlErr = client.DownloadWithProxy(proxyHostname, proxyPort, u)
|
||||||
|
} else {
|
||||||
|
res2, dlErr = client.Download(u)
|
||||||
|
}
|
||||||
|
if dlErr != nil && !errors.Is(dlErr, client.ErrTofu) {
|
||||||
Error("URL Fetch Error", err.Error())
|
Error("URL Fetch Error", err.Error())
|
||||||
return ret("", false)
|
return ret("", false)
|
||||||
}
|
}
|
||||||
go dlChoice("Loading that page timed out. What would you like to do?", u, res)
|
go dlChoice("Loading that page timed out. What would you like to do?", u, res2)
|
||||||
return ret("", false)
|
return ret("", false)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
2
go.mod
2
go.mod
@ -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.9.1
|
github.com/makeworld-the-better-one/go-gemini v0.9.3
|
||||||
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
4
go.sum
@ -133,8 +133,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.9.1 h1:/Vc6Y4Y1aOi4lZIBA1wDe+4N2xAI8EQ0CIjip2NUQkk=
|
github.com/makeworld-the-better-one/go-gemini v0.9.3 h1:vpJc1u4LYpEI5h7GcOE2zSfOmpE9gQzt0vEayp/ilWc=
|
||||||
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-gemini v0.9.3/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/gofeed v1.1.1-0.20201123002655-c0c6354134fe h1:i3b9Qy5z23DcXRnrsMYcM5s9Ng5VIidM1xZd+szuTsY=
|
github.com/makeworld-the-better-one/gofeed v1.1.1-0.20201123002655-c0c6354134fe h1:i3b9Qy5z23DcXRnrsMYcM5s9Ng5VIidM1xZd+szuTsY=
|
||||||
|
@ -5,8 +5,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/makeworld-the-better-one/amfora/structs"
|
"github.com/makeworld-the-better-one/amfora/structs"
|
||||||
"github.com/makeworld-the-better-one/go-gemini"
|
"github.com/makeworld-the-better-one/go-gemini"
|
||||||
@ -63,18 +63,13 @@ func MakePage(url string, res *gemini.Response, width, leftMargin int, proxied b
|
|||||||
}
|
}
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
go func() {
|
|
||||||
time.Sleep(time.Duration(viper.GetInt("a-general.page_max_time")) * time.Second)
|
|
||||||
res.Body.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
_, err := io.CopyN(buf, res.Body, viper.GetInt64("a-general.page_max_size")+1)
|
_, err := io.CopyN(buf, res.Body, viper.GetInt64("a-general.page_max_size")+1)
|
||||||
res.Body.Close()
|
res.Body.Close()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// Content was larger than max size
|
// Content was larger than max size
|
||||||
return nil, ErrTooLarge
|
return nil, ErrTooLarge
|
||||||
} else if err != io.EOF {
|
} else if err != io.EOF {
|
||||||
if strings.HasSuffix(err.Error(), "use of closed network connection") {
|
if errors.Is(err, os.ErrDeadlineExceeded) {
|
||||||
// Timed out
|
// Timed out
|
||||||
return nil, ErrTimedOut
|
return nil, ErrTimedOut
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user