mirror of
https://github.com/makeworld-the-better-one/amfora.git
synced 2024-08-17 02:30:39 +03:00
Merge branch 'master' into mediatype-handlers
This commit is contained in:
commit
7c2f59fcdc
154
.gitignore
vendored
154
.gitignore
vendored
@ -14,15 +14,15 @@ rec.yml
|
||||
# GIMP files
|
||||
*.xcf
|
||||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/code,go,linux,macos,windows
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=code,go,linux,macos,windows
|
||||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/code,go,linux,macos,windows,python
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=code,go,linux,macos,windows,python
|
||||
|
||||
### Code ###
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
|
||||
### Go ###
|
||||
# Binaries for programs and plugins
|
||||
@ -38,6 +38,9 @@ rec.yml
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
### Go Patch ###
|
||||
/vendor/
|
||||
/Godeps/
|
||||
@ -66,6 +69,7 @@ rec.yml
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
@ -85,9 +89,149 @@ Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
### Python ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
pytestdebug.log
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
doc/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
pythonenv*
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# profiling data
|
||||
.prof
|
||||
|
||||
### Windows ###
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
Thumbs.db:encryptable
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
@ -110,4 +254,4 @@ $RECYCLE.BIN/
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/code,go,linux,macos,windows
|
||||
# End of https://www.toptal.com/developers/gitignore/api/code,go,linux,macos,windows,python
|
||||
|
@ -25,7 +25,6 @@ linters:
|
||||
- lll
|
||||
- maligned
|
||||
- misspell
|
||||
- nakedret
|
||||
- nolintlint
|
||||
- prealloc
|
||||
- scopelint
|
||||
|
30
CHANGELOG.md
30
CHANGELOG.md
@ -6,19 +6,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- Ability to set custom keybindings in config (#135)
|
||||
|
||||
### Fixed
|
||||
- Don't use cache when URL is typed in bottom bar (#159)
|
||||
|
||||
## [1.7.2] - 2020-12-21
|
||||
### Fixed
|
||||
- Viewing subscriptions after subscribing to a certain user page won't crash Amfora (#157)
|
||||
|
||||
|
||||
## [1.7.1] - 2020-12-21
|
||||
### Fixed
|
||||
- Fixed bug that caused Amfora to crash when subscribing to a page (#151)
|
||||
|
||||
|
||||
## [1.7.0] - 2020-12-20
|
||||
### Added
|
||||
- **Subscriptions** to feeds and page changes (#61)
|
||||
- Opening local files with `file://` URIs (#103, #117)
|
||||
- `show_link` option added in config to optionally see the URL (#133)
|
||||
- Support for Unicode in domain names (IDNs)
|
||||
- Unnecessarily encoded characters in URLs will be decoded (#138)
|
||||
- URLs are NFC-normalized before any processing (#138)
|
||||
- Links to the wiki in the new tab
|
||||
- Cache times out after 30 minutes by default (#110)
|
||||
- `about:version` page (#126)
|
||||
|
||||
### Changed
|
||||
- Updated [go-gemini](https://github.com/makeworld-the-better-one/go-gemini) to v0.9.3
|
||||
- Updated [go-gemini](https://github.com/makeworld-the-better-one/go-gemini) to v0.11.0
|
||||
- Supports CN-only wildcard certs
|
||||
- Time out when header takes too long
|
||||
- Preformatted text is now light yellow by default
|
||||
- Downloading a file no longer uses a second request
|
||||
- You can go back to the new tab page in history (#96)
|
||||
|
||||
### 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
|
||||
- User-entered URLs with invalid characters will be percent-encoded (#138)
|
||||
- Custom downloads dir is actually used (#148)
|
||||
- Empty quote lines no longer disappear
|
||||
|
||||
|
||||
## [1.6.0] - 2020-11-04
|
||||
|
36
README.md
36
README.md
@ -20,13 +20,13 @@ Amfora aims to be the best looking [Gemini](https://gemini.circumlunar.space/) c
|
||||
|
||||
It also aims to be completely cross platform, with full Windows support. If you're on Windows, I would not recommend using the default terminal software. Use [Windows Terminal](https://www.microsoft.com/en-us/p/windows-terminal/9n0dx20hk701) instead, and make sure it [works with UTF-8](https://akr.am/blog/posts/using-utf-8-in-the-windows-terminal). Note that some of the application colors might not display correctly on Windows, but all functionality will still work.
|
||||
|
||||
It fully passes Sean Conman's client torture test, including the new Unicode tests. It mostly passes the Egsam test.
|
||||
It fully passes Sean Conman's client torture test, as well as the Egsam one.
|
||||
|
||||
## Installation
|
||||
|
||||
### Binary
|
||||
|
||||
Download a binary from the [releases](https://github.com/makeworld-the-better-one/amfora/releases) page. On Unix-based systems you might have to make the file executable with `chmod +x <filename>`. You can rename the file to just `amfora` for easy access, and move it to `/usr/local/bin/`.
|
||||
Download a binary from the [releases](https://github.com/makeworld-the-better-one/amfora/releases) page. On Unix-based systems you will have to make the file executable with `chmod +x <filename>`. You can rename the file to just `amfora` for easy access, and move it to `/usr/local/bin/`.
|
||||
|
||||
On Windows, make sure you click "Advanced > Run anyway" after double-clicking, or something like that.
|
||||
|
||||
@ -36,7 +36,7 @@ curl -sSL https://raw.githubusercontent.com/makeworld-the-better-one/amfora/mast
|
||||
update-desktop-database ~/.local/share/applications
|
||||
```
|
||||
|
||||
Make sure to click "Watch" > "Releases only" in the top right to get notified about new releases!
|
||||
Make sure to click "Watch" in the top right, then "Custom" > "Releases" to get notified about new releases!
|
||||
|
||||
|
||||
### Arch Linux
|
||||
@ -49,7 +49,7 @@ sudo pacman -S amfora
|
||||
|
||||
### Homebrew
|
||||
|
||||
If you use [Homebrew](https://brew.sh/), you can install Amfora through the official tap.
|
||||
If you use [Homebrew](https://brew.sh/), you can install Amfora through the my personal tap.
|
||||
```
|
||||
brew tap makeworld-the-better-one/tap
|
||||
brew install amfora
|
||||
@ -59,13 +59,29 @@ You can update it with:
|
||||
brew upgrade amfora
|
||||
```
|
||||
|
||||
### KISS Linux
|
||||
|
||||
[KISS](k1ss.org) Linux users can install Amfora from jedahan's repository.
|
||||
|
||||
Add jedahan's kiss repository:
|
||||
```
|
||||
git clone https://github.com/jedahan/kiss-repo.git repo-jedahan
|
||||
export KISS_PATH="$KISS_PATH:$PWD/repo-jedahan"
|
||||
```
|
||||
|
||||
Build and install Amfora:
|
||||
```
|
||||
kiss build amfora
|
||||
kiss install amfora
|
||||
```
|
||||
|
||||
### From Source
|
||||
|
||||
This section is for advanced users who want to install the latest (possibly unstable) version of Amfora.
|
||||
|
||||
<details>
|
||||
<summary>Click to expand</summary>
|
||||
|
||||
This section is for advanced users who want to install the latest (possibly unstable) version of Amfora.
|
||||
|
||||
**Requirements:**
|
||||
- Go 1.13 or later
|
||||
- GNU Make
|
||||
@ -82,6 +98,12 @@ sudo make install # If you want to install the binary for all users
|
||||
|
||||
Because you installed with the Makefile, running `amfora -v` will tell you exactly what commit the binary was built from.
|
||||
|
||||
Arch Linux users can also install the latest commit of Amfora from the AUR. It has the package name `amfora-git`, and is maintained by @lovetocode999
|
||||
|
||||
```
|
||||
yay -S amfora-git
|
||||
```
|
||||
|
||||
MacOS users can also use [Homebrew](https://brew.sh/) to install the latest commit of Amfora:
|
||||
|
||||
```
|
||||
@ -124,7 +146,7 @@ Features in *italics* are in the master branch, but not in the latest release.
|
||||
- Manage and browse them
|
||||
- Similar to [Kristall](https://github.com/MasterQ32/kristall)
|
||||
- https://lists.orbitalfox.eu/archives/gemini/2020/001400.html
|
||||
- [x] *Subscriptions*
|
||||
- [x] Subscriptions
|
||||
- Subscribing to RSS, Atom, and [JSON Feeds](https://jsonfeed.org/) are all supported
|
||||
- So is subscribing to a page, to know when it changes
|
||||
- [ ] Stream support
|
||||
|
@ -12,4 +12,6 @@ Thank you to the following contributors, who have helped make Amfora great. FOSS
|
||||
- Patryk Niedźwiedziński (@pniedzwiedzinski)
|
||||
- Trevor Slocum (@tsclocum)
|
||||
- Mattias Jadelius (@jedthehumanoid)
|
||||
- Lokesh Krishna (@lokesh-krishna)
|
||||
- Jeff (@phaedrus-jaf)
|
||||
- Stephen Robinson (@sudobash1)
|
||||
|
@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
version = "v1.6.0"
|
||||
version = "v1.7.2"
|
||||
commit = "unknown"
|
||||
builtBy = "unknown"
|
||||
)
|
||||
@ -52,7 +52,7 @@ func main() {
|
||||
|
||||
client.Init()
|
||||
|
||||
display.Init()
|
||||
display.Init(version, commit, builtBy)
|
||||
display.NewTab()
|
||||
display.NewTab() // Open extra tab and close it to fully initialize the app and wrapping
|
||||
display.CloseTab()
|
||||
|
20
cache/page.go
vendored
20
cache/page.go
vendored
@ -4,6 +4,7 @@ package cache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/makeworld-the-better-one/amfora/structs"
|
||||
)
|
||||
@ -13,6 +14,7 @@ var urls = make([]string, 0) // Duplicate of the keys in the `page
|
||||
var maxPages = 0 // Max allowed number of pages in cache
|
||||
var maxSize = 0 // Max allowed cache size in bytes
|
||||
var lock = sync.RWMutex{}
|
||||
var timeout = time.Duration(0)
|
||||
|
||||
// SetMaxPages sets the max number of pages the cache can hold.
|
||||
// A value <= 0 means infinite pages.
|
||||
@ -26,6 +28,16 @@ func SetMaxSize(max int) {
|
||||
maxSize = max
|
||||
}
|
||||
|
||||
// SetTimeout sets the max number of a seconds a page can still
|
||||
// be valid for. A value <= 0 means forever.
|
||||
func SetTimeout(t int) {
|
||||
if t <= 0 {
|
||||
timeout = time.Duration(0)
|
||||
return
|
||||
}
|
||||
timeout = time.Duration(t) * time.Second
|
||||
}
|
||||
|
||||
func removeIndex(s []string, i int) []string {
|
||||
s[len(s)-1], s[i] = s[i], s[len(s)-1]
|
||||
return s[:len(s)-1]
|
||||
@ -110,10 +122,14 @@ func NumPages() int {
|
||||
}
|
||||
|
||||
// GetPage returns the page struct, and a bool indicating if the page was in the cache or not.
|
||||
// An empty page struct is returned if the page isn't in the cache.
|
||||
// (nil, false) is returned if the page isn't in the cache.
|
||||
func GetPage(url string) (*structs.Page, bool) {
|
||||
lock.RLock()
|
||||
defer lock.RUnlock()
|
||||
|
||||
p, ok := pages[url]
|
||||
return p, ok
|
||||
if ok && (timeout == 0 || time.Since(p.MadeAt) < timeout) {
|
||||
return p, ok
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
@ -18,7 +18,6 @@ var (
|
||||
certCacheMu = &sync.RWMutex{}
|
||||
|
||||
fetchClient *gemini.Client
|
||||
dlClient *gemini.Client // For downloading
|
||||
)
|
||||
|
||||
func Init() {
|
||||
@ -26,10 +25,6 @@ func Init() {
|
||||
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) {
|
||||
@ -112,11 +107,6 @@ 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)
|
||||
cert, key := clientCert(parsed.Host)
|
||||
@ -145,8 +135,3 @@ func fetchWithProxy(proxyHostname, proxyPort, u string, c *gemini.Client) (*gemi
|
||||
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)
|
||||
}
|
||||
|
183
config/config.go
183
config/config.go
@ -165,12 +165,108 @@ func Init() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// *** Setup vipers ***
|
||||
|
||||
TofuStore.SetConfigFile(tofuDBPath)
|
||||
TofuStore.SetConfigType("toml")
|
||||
err = TofuStore.ReadInConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
BkmkStore.SetConfigFile(bkmkPath)
|
||||
BkmkStore.SetConfigType("toml")
|
||||
err = BkmkStore.ReadInConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
BkmkStore.Set("DO NOT TOUCH", true)
|
||||
err = BkmkStore.WriteConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Setup main config
|
||||
|
||||
viper.SetDefault("a-general.home", "gemini://gemini.circumlunar.space")
|
||||
viper.SetDefault("a-general.auto_redirect", false)
|
||||
viper.SetDefault("a-general.http", "default")
|
||||
viper.SetDefault("a-general.search", "gemini://gus.guru/search")
|
||||
viper.SetDefault("a-general.color", true)
|
||||
viper.SetDefault("a-general.ansi", true)
|
||||
viper.SetDefault("a-general.bullets", true)
|
||||
viper.SetDefault("a-general.show_link", false)
|
||||
viper.SetDefault("a-general.left_margin", 0.15)
|
||||
viper.SetDefault("a-general.max_width", 100)
|
||||
viper.SetDefault("a-general.downloads", "")
|
||||
viper.SetDefault("a-general.temp_downloads", "")
|
||||
viper.SetDefault("a-general.page_max_size", 2097152)
|
||||
viper.SetDefault("a-general.page_max_time", 10)
|
||||
viper.SetDefault("a-general.emoji_favicons", false)
|
||||
viper.SetDefault("keybindings.bind_reload", []string{"R", "Ctrl-R"})
|
||||
viper.SetDefault("keybindings.bind_home", "Backspace")
|
||||
viper.SetDefault("keybindings.bind_bookmarks", "Ctrl-B")
|
||||
viper.SetDefault("keybindings.bind_add_bookmark", "Ctrl-D")
|
||||
viper.SetDefault("keybindings.bind_sub", "Ctrl-A")
|
||||
viper.SetDefault("keybindings.bind_add_sub", "Ctrl-X")
|
||||
viper.SetDefault("keybindings.bind_save", "Ctrl-S")
|
||||
viper.SetDefault("keybindings.bind_pgup", []string{"PgUp", "u"})
|
||||
viper.SetDefault("keybindings.bind_pgdn", []string{"PgDn", "d"})
|
||||
viper.SetDefault("keybindings.bind_bottom", "Space")
|
||||
viper.SetDefault("keybindings.bind_edit", "e")
|
||||
viper.SetDefault("keybindings.bind_back", []string{"b", "Alt-Left"})
|
||||
viper.SetDefault("keybindings.bind_forward", []string{"f", "Alt-Right"})
|
||||
viper.SetDefault("keybindings.bind_new_tab", "Ctrl-T")
|
||||
viper.SetDefault("keybindings.bind_close_tab", "Ctrl-W")
|
||||
viper.SetDefault("keybindings.bind_next_tab", "F2")
|
||||
viper.SetDefault("keybindings.bind_prev_tab", "F1")
|
||||
viper.SetDefault("keybindings.bind_quit", []string{"Ctrl-C", "Ctrl-Q", "q"})
|
||||
viper.SetDefault("keybindings.bind_help", "?")
|
||||
viper.SetDefault("keybindings.bind_link1", "1")
|
||||
viper.SetDefault("keybindings.bind_link2", "2")
|
||||
viper.SetDefault("keybindings.bind_link3", "3")
|
||||
viper.SetDefault("keybindings.bind_link4", "4")
|
||||
viper.SetDefault("keybindings.bind_link5", "5")
|
||||
viper.SetDefault("keybindings.bind_link6", "6")
|
||||
viper.SetDefault("keybindings.bind_link7", "7")
|
||||
viper.SetDefault("keybindings.bind_link8", "8")
|
||||
viper.SetDefault("keybindings.bind_link9", "9")
|
||||
viper.SetDefault("keybindings.bind_link0", "0")
|
||||
viper.SetDefault("keybindings.bind_tab1", "!")
|
||||
viper.SetDefault("keybindings.bind_tab2", "@")
|
||||
viper.SetDefault("keybindings.bind_tab3", "#")
|
||||
viper.SetDefault("keybindings.bind_tab4", "$")
|
||||
viper.SetDefault("keybindings.bind_tab5", "%")
|
||||
viper.SetDefault("keybindings.bind_tab6", "^")
|
||||
viper.SetDefault("keybindings.bind_tab7", "&")
|
||||
viper.SetDefault("keybindings.bind_tab8", "*")
|
||||
viper.SetDefault("keybindings.bind_tab9", "(")
|
||||
viper.SetDefault("keybindings.bind_tab0", ")")
|
||||
viper.SetDefault("keybindings.shift_numbers", "")
|
||||
viper.SetDefault("url-handlers.other", "off")
|
||||
viper.SetDefault("cache.max_size", 0)
|
||||
viper.SetDefault("cache.max_pages", 20)
|
||||
viper.SetDefault("cache.timeout", 1800)
|
||||
viper.SetDefault("subscriptions.popup", true)
|
||||
viper.SetDefault("subscriptions.update_interval", 1800)
|
||||
viper.SetDefault("subscriptions.workers", 3)
|
||||
viper.SetDefault("subscriptions.entries_per_page", 20)
|
||||
|
||||
viper.SetConfigFile(configPath)
|
||||
viper.SetConfigType("toml")
|
||||
err = viper.ReadInConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Setup the key bindings
|
||||
KeyInit()
|
||||
|
||||
// *** Downloads paths, setup, and creation ***
|
||||
|
||||
// Setup downloads dir
|
||||
if viper.GetString("a-general.downloads") == "" {
|
||||
// Find default Downloads dir
|
||||
// This seems to work for all OSes?
|
||||
if userdirs.Download == "" {
|
||||
DownloadsDir = filepath.Join(home, "Downloads")
|
||||
} else {
|
||||
@ -202,93 +298,10 @@ func Init() error {
|
||||
DownloadsDir = dDir
|
||||
}
|
||||
|
||||
// Setup temporary downloads dir
|
||||
if viper.GetString("a-general.temp_downloads") == "" {
|
||||
TempDownloadsDir = filepath.Join(os.TempDir(), "amfora_temp")
|
||||
|
||||
// Make sure it exists
|
||||
err = os.MkdirAll(TempDownloadsDir, 0755)
|
||||
if err != nil {
|
||||
return fmt.Errorf("temp downloads path could not be created: %s", TempDownloadsDir)
|
||||
}
|
||||
} else {
|
||||
// Validate path
|
||||
dDir := viper.GetString("a-general.temp_downloads")
|
||||
di, err := os.Stat(dDir)
|
||||
if err == nil {
|
||||
if !di.IsDir() {
|
||||
return fmt.Errorf("temp downloads path specified is not a directory: %s", dDir)
|
||||
}
|
||||
} else if os.IsNotExist(err) {
|
||||
// Try to create path
|
||||
err = os.MkdirAll(dDir, 0755)
|
||||
if err != nil {
|
||||
return fmt.Errorf("temp downloads path could not be created: %s", dDir)
|
||||
}
|
||||
} else {
|
||||
// Some other error
|
||||
return fmt.Errorf("couldn't access temp downloads directory: %s", dDir)
|
||||
}
|
||||
TempDownloadsDir = dDir
|
||||
}
|
||||
|
||||
// *** Setup vipers ***
|
||||
|
||||
TofuStore.SetConfigFile(tofuDBPath)
|
||||
TofuStore.SetConfigType("toml")
|
||||
err = TofuStore.ReadInConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
BkmkStore.SetConfigFile(bkmkPath)
|
||||
BkmkStore.SetConfigType("toml")
|
||||
err = BkmkStore.ReadInConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
BkmkStore.Set("DO NOT TOUCH", true)
|
||||
err = BkmkStore.WriteConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Setup main config
|
||||
|
||||
viper.SetDefault("a-general.home", "gemini.circumlunar.space")
|
||||
viper.SetDefault("a-general.auto_redirect", false)
|
||||
viper.SetDefault("a-general.http", "default")
|
||||
viper.SetDefault("a-general.search", "gus.guru/search")
|
||||
viper.SetDefault("a-general.color", true)
|
||||
viper.SetDefault("a-general.ansi", true)
|
||||
viper.SetDefault("a-general.bullets", true)
|
||||
viper.SetDefault("a-general.show_link", false)
|
||||
viper.SetDefault("a-general.left_margin", 0.15)
|
||||
viper.SetDefault("a-general.max_width", 100)
|
||||
viper.SetDefault("a-general.downloads", "")
|
||||
viper.SetDefault("a-general.temp_downloads", "")
|
||||
viper.SetDefault("a-general.page_max_size", 2097152)
|
||||
viper.SetDefault("a-general.page_max_time", 10)
|
||||
viper.SetDefault("a-general.emoji_favicons", false)
|
||||
viper.SetDefault("keybindings.shift_numbers", "!@#$%^&*()")
|
||||
viper.SetDefault("url-handlers.other", "off")
|
||||
viper.SetDefault("cache.max_size", 0)
|
||||
viper.SetDefault("cache.max_pages", 20)
|
||||
viper.SetDefault("subscriptions.popup", true)
|
||||
viper.SetDefault("subscriptions.update_interval", 1800)
|
||||
viper.SetDefault("subscriptions.workers", 3)
|
||||
viper.SetDefault("subscriptions.entries_per_page", 20)
|
||||
|
||||
viper.SetConfigFile(configPath)
|
||||
viper.SetConfigType("toml")
|
||||
err = viper.ReadInConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Setup cache from config
|
||||
cache.SetMaxSize(viper.GetInt("cache.max_size"))
|
||||
cache.SetMaxPages(viper.GetInt("cache.max_pages"))
|
||||
cache.SetTimeout(viper.GetInt("cache.timeout"))
|
||||
|
||||
// Setup theme
|
||||
configTheme := viper.Sub("theme")
|
||||
|
@ -90,12 +90,51 @@ emoji_favicons = false
|
||||
|
||||
|
||||
[keybindings]
|
||||
# In the future there will be more settings here.
|
||||
# If you have a non-US keyboard, use bind_tab1 through bind_tab0 to
|
||||
# setup the shift-number bindings: Eg, for US keyboards (the default):
|
||||
# bind_tab1 = "!"
|
||||
# bind_tab2 = "@"
|
||||
# bind_tab3 = "#"
|
||||
# bind_tab4 = "$"
|
||||
# bind_tab5 = "%"
|
||||
# bind_tab6 = "^"
|
||||
# bind_tab7 = "&"
|
||||
# bind_tab8 = "*"
|
||||
# bind_tab9 = "("
|
||||
# bind_tab0 = ")"
|
||||
|
||||
# Hold down shift and press the numbers on your keyboard (1,2,3,4,5,6,7,8,9,0) to set this up.
|
||||
# It is default set to be accurate for US keyboards.
|
||||
shift_numbers = "!@#$%^&*()"
|
||||
# Whitespace is not allowed in any of the keybindings! Use 'Space' and 'Tab' to bind to those keys.
|
||||
# Multiple keys can be bound to one command, just use a TOML array.
|
||||
# To add the Alt modifier, the binding must start with Alt-, should be reasonably universal
|
||||
# Ctrl- won't work on all keys, see this for a list:
|
||||
# https://github.com/gdamore/tcell/blob/cb1e5d6fa606/key.go#L83
|
||||
|
||||
# An example of a TOML array for multiple keys being bound to one command is the default
|
||||
# binding for reload:
|
||||
# bind_reload = ["R","Ctrl-R"]
|
||||
# One thing to note here is that "R" is capitalization sensitive, so it means shift-r.
|
||||
# "Ctrl-R" means both ctrl-r and ctrl-shift-R (this is a quirk of what ctrl-r means on
|
||||
# an ANSI terminal)
|
||||
|
||||
# The default binding for opening the bottom bar for entering a URL or link number is:
|
||||
# bind_bottom = "Space"
|
||||
# This is how to get the Spacebar as a keybinding, if you try to use " ", it won't work.
|
||||
# And, finally, an example of a simple, unmodified character is:
|
||||
# bind_edit = "e"
|
||||
# This binds the "e" key to the command to edit the current URL.
|
||||
|
||||
# The bind_link[1-90] options are for the commands to go to the first 10 links on a page,
|
||||
# typically these are bound to the number keys:
|
||||
# bind_link1 = "1"
|
||||
# bind_link2 = "2"
|
||||
# bind_link3 = "3"
|
||||
# bind_link4 = "4"
|
||||
# bind_link5 = "5"
|
||||
# bind_link6 = "6"
|
||||
# bind_link7 = "7"
|
||||
# bind_link8 = "8"
|
||||
# bind_link9 = "9"
|
||||
# bind_link0 = "0"
|
||||
|
||||
[url-handlers]
|
||||
# Allows setting the commands to run for various URL schemes.
|
||||
@ -164,13 +203,15 @@ other = 'off'
|
||||
|
||||
|
||||
[cache]
|
||||
# Options for page cache - which is only for text/gemini pages
|
||||
# Options for page cache - which is only for text pages
|
||||
# Increase the cache size to speed up browsing at the expense of memory
|
||||
|
||||
# Zero values mean there is no limit
|
||||
|
||||
max_size = 0 # Size in bytes
|
||||
max_pages = 30 # The maximum number of pages the cache will store
|
||||
|
||||
# How long a page will stay in cache, in seconds.
|
||||
timeout = 1800 # 30 mins
|
||||
|
||||
[proxies]
|
||||
# Allows setting a Gemini proxy for different schemes.
|
||||
|
@ -1,24 +1,237 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// KeyToNum returns the number on the user's keyboard they pressed,
|
||||
// using the rune returned when when they press Shift+Num.
|
||||
// The error is not nil if the provided key is invalid.
|
||||
func KeyToNum(key rune) (int, error) {
|
||||
runes := []rune(viper.GetString("keybindings.shift_numbers"))
|
||||
for i := range runes {
|
||||
if key == runes[i] {
|
||||
if i == len(runes)-1 {
|
||||
// Last key is 0, not 10
|
||||
return 0, nil
|
||||
// NOTE: CmdLink[1-90] and CmdTab[1-90] need to be in-order and consecutive
|
||||
// This property is used to simplify key handling in display/display.go
|
||||
type Command int
|
||||
|
||||
const (
|
||||
CmdInvalid Command = 0
|
||||
CmdLink1 = 1
|
||||
CmdLink2 = 2
|
||||
CmdLink3 = 3
|
||||
CmdLink4 = 4
|
||||
CmdLink5 = 5
|
||||
CmdLink6 = 6
|
||||
CmdLink7 = 7
|
||||
CmdLink8 = 8
|
||||
CmdLink9 = 9
|
||||
CmdLink0 = 10
|
||||
CmdTab1 = 11
|
||||
CmdTab2 = 12
|
||||
CmdTab3 = 13
|
||||
CmdTab4 = 14
|
||||
CmdTab5 = 15
|
||||
CmdTab6 = 16
|
||||
CmdTab7 = 17
|
||||
CmdTab8 = 18
|
||||
CmdTab9 = 19
|
||||
CmdTab0 = 20
|
||||
CmdBottom = iota
|
||||
CmdEdit
|
||||
CmdHome
|
||||
CmdBookmarks
|
||||
CmdAddBookmark
|
||||
CmdSave
|
||||
CmdReload
|
||||
CmdBack
|
||||
CmdForward
|
||||
CmdPgup
|
||||
CmdPgdn
|
||||
CmdNewTab
|
||||
CmdCloseTab
|
||||
CmdNextTab
|
||||
CmdPrevTab
|
||||
CmdQuit
|
||||
CmdHelp
|
||||
CmdSub
|
||||
CmdAddSub
|
||||
)
|
||||
|
||||
type keyBinding struct {
|
||||
key tcell.Key
|
||||
mod tcell.ModMask
|
||||
r rune
|
||||
}
|
||||
|
||||
// Map of active keybindings to commands.
|
||||
var bindings map[keyBinding]Command
|
||||
|
||||
// inversion of tcell.KeyNames, used to simplify config parsing.
|
||||
// used by parseBinding() below.
|
||||
var tcellKeys map[string]tcell.Key
|
||||
|
||||
// helper function that takes a single keyBinding object and returns
|
||||
// a string in the format used by the configuration file. Support
|
||||
// function for GetKeyBinding(), used to make the help panel helpful.
|
||||
func keyBindingToString(kb keyBinding) (string, bool) {
|
||||
var prefix string = ""
|
||||
|
||||
if kb.mod&tcell.ModAlt == tcell.ModAlt {
|
||||
prefix = "Alt-"
|
||||
}
|
||||
|
||||
if kb.key == tcell.KeyRune {
|
||||
if kb.r == ' ' {
|
||||
return prefix + "Space", true
|
||||
}
|
||||
return prefix + string(kb.r), true
|
||||
}
|
||||
s, ok := tcell.KeyNames[kb.key]
|
||||
if ok {
|
||||
return prefix + s, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Get all keybindings for a Command as a string.
|
||||
// Used by the help panel so bindable keys display with their
|
||||
// bound values rather than hardcoded defaults.
|
||||
func GetKeyBinding(cmd Command) string {
|
||||
var s string = ""
|
||||
for kb, c := range bindings {
|
||||
if c == cmd {
|
||||
t, ok := keyBindingToString(kb)
|
||||
if ok {
|
||||
s += t + ", "
|
||||
}
|
||||
return i + 1, nil
|
||||
}
|
||||
}
|
||||
return -1, errors.New("provided key is invalid") //nolint:goerr113
|
||||
|
||||
if len(s) > 0 {
|
||||
return s[:len(s)-2]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Parse a single keybinding string and add it to the binding map
|
||||
func parseBinding(cmd Command, binding string) {
|
||||
var k tcell.Key
|
||||
var m tcell.ModMask = 0
|
||||
var r rune = 0
|
||||
|
||||
if strings.HasPrefix(binding, "Alt-") {
|
||||
m = tcell.ModAlt
|
||||
binding = binding[4:]
|
||||
}
|
||||
|
||||
if len(binding) == 1 {
|
||||
k = tcell.KeyRune
|
||||
r = []rune(binding)[0]
|
||||
} else if len(binding) == 0 {
|
||||
return
|
||||
} else if binding == "Space" {
|
||||
k = tcell.KeyRune
|
||||
r = ' '
|
||||
} else {
|
||||
var ok bool
|
||||
k, ok = tcellKeys[binding]
|
||||
if !ok { // Bad keybinding! Quietly ignore...
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(binding, "Ctrl") {
|
||||
m += tcell.ModCtrl
|
||||
}
|
||||
}
|
||||
|
||||
bindings[keyBinding{k, m, r}] = cmd
|
||||
}
|
||||
|
||||
// Generate the bindings map from the TOML configuration file.
|
||||
// Called by config.Init()
|
||||
func KeyInit() {
|
||||
configBindings := map[Command]string{
|
||||
CmdLink1: "keybindings.bind_link1",
|
||||
CmdLink2: "keybindings.bind_link2",
|
||||
CmdLink3: "keybindings.bind_link3",
|
||||
CmdLink4: "keybindings.bind_link4",
|
||||
CmdLink5: "keybindings.bind_link5",
|
||||
CmdLink6: "keybindings.bind_link6",
|
||||
CmdLink7: "keybindings.bind_link7",
|
||||
CmdLink8: "keybindings.bind_link8",
|
||||
CmdLink9: "keybindings.bind_link9",
|
||||
CmdLink0: "keybindings.bind_link0",
|
||||
CmdBottom: "keybindings.bind_bottom",
|
||||
CmdEdit: "keybindings.bind_edit",
|
||||
CmdHome: "keybindings.bind_home",
|
||||
CmdBookmarks: "keybindings.bind_bookmarks",
|
||||
CmdAddBookmark: "keybindings.bind_add_bookmark",
|
||||
CmdSave: "keybindings.bind_save",
|
||||
CmdReload: "keybindings.bind_reload",
|
||||
CmdBack: "keybindings.bind_back",
|
||||
CmdForward: "keybindings.bind_forward",
|
||||
CmdPgup: "keybindings.bind_pgup",
|
||||
CmdPgdn: "keybindings.bind_pgdn",
|
||||
CmdNewTab: "keybindings.bind_new_tab",
|
||||
CmdCloseTab: "keybindings.bind_close_tab",
|
||||
CmdNextTab: "keybindings.bind_next_tab",
|
||||
CmdPrevTab: "keybindings.bind_prev_tab",
|
||||
CmdQuit: "keybindings.bind_quit",
|
||||
CmdHelp: "keybindings.bind_help",
|
||||
CmdSub: "keybindings.bind_sub",
|
||||
CmdAddSub: "keybindings.bind_add_sub",
|
||||
}
|
||||
// This is split off to allow shift_numbers to override bind_tab[1-90]
|
||||
// (This is needed for older configs so that the default bind_tab values
|
||||
// aren't used)
|
||||
configTabNBindings := map[Command]string{
|
||||
CmdTab1: "keybindings.bind_tab1",
|
||||
CmdTab2: "keybindings.bind_tab2",
|
||||
CmdTab3: "keybindings.bind_tab3",
|
||||
CmdTab4: "keybindings.bind_tab4",
|
||||
CmdTab5: "keybindings.bind_tab5",
|
||||
CmdTab6: "keybindings.bind_tab6",
|
||||
CmdTab7: "keybindings.bind_tab7",
|
||||
CmdTab8: "keybindings.bind_tab8",
|
||||
CmdTab9: "keybindings.bind_tab9",
|
||||
CmdTab0: "keybindings.bind_tab0",
|
||||
}
|
||||
tcellKeys = make(map[string]tcell.Key)
|
||||
bindings = make(map[keyBinding]Command)
|
||||
|
||||
for k, kname := range tcell.KeyNames {
|
||||
tcellKeys[kname] = k
|
||||
}
|
||||
|
||||
for c, allb := range configBindings {
|
||||
for _, b := range viper.GetStringSlice(allb) {
|
||||
parseBinding(c, b)
|
||||
}
|
||||
}
|
||||
|
||||
// Backwards compatibility with the old shift_numbers config line.
|
||||
shiftNumbers := []rune(viper.GetString("keybindings.shift_numbers"))
|
||||
if len(shiftNumbers) > 0 && len(shiftNumbers) <= 10 {
|
||||
for i, r := range shiftNumbers {
|
||||
bindings[keyBinding{tcell.KeyRune, 0, r}] = CmdTab1 + Command(i)
|
||||
}
|
||||
} else {
|
||||
for c, allb := range configTabNBindings {
|
||||
for _, b := range viper.GetStringSlice(allb) {
|
||||
parseBinding(c, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Used by the display package to turn a tcell.EventKey into a Command
|
||||
func TranslateKeyEvent(e *tcell.EventKey) Command {
|
||||
var ok bool
|
||||
var cmd Command
|
||||
k := e.Key()
|
||||
if k == tcell.KeyRune {
|
||||
cmd, ok = bindings[keyBinding{k, e.Modifiers(), e.Rune()}]
|
||||
} else { // Sometimes tcell sets e.Rune() on non-KeyRune events.
|
||||
cmd, ok = bindings[keyBinding{k, e.Modifiers(), 0}]
|
||||
}
|
||||
if ok {
|
||||
return cmd
|
||||
}
|
||||
return CmdInvalid
|
||||
}
|
||||
|
8
contrib/gemini-wiki/README.md
Normal file
8
contrib/gemini-wiki/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
# gemini-wiki
|
||||
|
||||
This folder contains a Python script that downloads the Amfora [wiki](https://github.com/makeworld-the-better-one/amfora/wiki)
|
||||
and converts it to gemtext, incorporating the sidebar and footer as well.
|
||||
|
||||
The script expects to be run inside the folder where the Gemini version of the wiki should be.
|
||||
|
||||
The output of this script can be viewed at `gemini://makeworld.gq/amfora-wiki/`.
|
111
contrib/gemini-wiki/main.py
Normal file
111
contrib/gemini-wiki/main.py
Normal file
@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Formatted with black.
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
import md2gemini
|
||||
|
||||
TMP_WIKI_CLONE = "/tmp/amfora.wiki"
|
||||
|
||||
|
||||
def md2gem(markdown):
|
||||
return md2gemini.md2gemini(
|
||||
markdown,
|
||||
links="copy",
|
||||
plain=False,
|
||||
strip_html=True,
|
||||
md_links=True,
|
||||
link_func=link_func,
|
||||
)
|
||||
|
||||
|
||||
def link_func(link):
|
||||
if "://" in link:
|
||||
# Absolute URL
|
||||
return link
|
||||
|
||||
# Link to other wiki page
|
||||
return link + ".gmi"
|
||||
|
||||
|
||||
def run_cmd(*args):
|
||||
proc = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
if proc.returncode != 0:
|
||||
print(
|
||||
"Command "
|
||||
+ " ".join(args)
|
||||
+ "failed with exit code "
|
||||
+ str(proc.returncode)
|
||||
)
|
||||
print("Output was:")
|
||||
print()
|
||||
print(proc.stdout.decode())
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# Delete leftover git repo
|
||||
try:
|
||||
shutil.rmtree(TMP_WIKI_CLONE)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
os.mkdir(TMP_WIKI_CLONE)
|
||||
|
||||
run_cmd(
|
||||
"git",
|
||||
"clone",
|
||||
"--depth",
|
||||
"1",
|
||||
"https://github.com/makeworld-the-better-one/amfora.wiki.git",
|
||||
TMP_WIKI_CLONE,
|
||||
)
|
||||
|
||||
# Save special files
|
||||
|
||||
with open(os.path.join(TMP_WIKI_CLONE, "_Footer.md"), "r") as f:
|
||||
footer = md2gem(f.read())
|
||||
|
||||
# Get files
|
||||
(_, _, files) = next(os.walk(TMP_WIKI_CLONE))
|
||||
|
||||
# Create list of pages
|
||||
pages = "## Pages\n\n=>.. Home\n"
|
||||
for file in files:
|
||||
|
||||
if file in ["_Footer.md", "_Sidebar.md", "Home.md"]:
|
||||
continue
|
||||
if not file.endswith(".md"):
|
||||
continue
|
||||
pages += "=>" + file[:-2] + "gmi " + file[:-3].replace("-", " ") + "\n"
|
||||
|
||||
pages += "\n\n"
|
||||
|
||||
for file in files:
|
||||
filepath = os.path.join(TMP_WIKI_CLONE, file)
|
||||
|
||||
if file in ["_Footer.md", "_Sidebar.md"]:
|
||||
continue
|
||||
if not file.endswith(".md"):
|
||||
# Could be a resource like an image file, copy it
|
||||
shutil.copyfile(filepath, file)
|
||||
continue
|
||||
|
||||
# Markdown file
|
||||
|
||||
with open(filepath, "r") as f:
|
||||
gemtext = md2gem(f.read())
|
||||
|
||||
# Add title, sidebar, footer
|
||||
gemtext = "# " + file[:-3].replace("-", " ") + "\n\n" + pages + gemtext
|
||||
gemtext += "\n\n\n\n" + footer
|
||||
|
||||
if file == "Home.md":
|
||||
file = "index.md"
|
||||
|
||||
new_name = file[:-2] + "gmi"
|
||||
|
||||
with open(new_name, "w") as f:
|
||||
f.write(gemtext)
|
1
contrib/gemini-wiki/requirements.txt
Normal file
1
contrib/gemini-wiki/requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
md2gemini<2
|
@ -6,7 +6,7 @@ You can use these themes by replacing the `[theme]` section of your config with
|
||||
|
||||
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)
|
||||
![screenshot of the nord theme](https://user-images.githubusercontent.com/20235646/102846450-005dc480-4436-11eb-89a9-a1a4350f5415.png)
|
||||
|
||||
## Dracula
|
||||
|
||||
|
@ -101,3 +101,5 @@ bkmk_modal_label = "#f8f8f2"
|
||||
bkmk_modal_field_bg = "#000000"
|
||||
bkmk_modal_field_text = "#f8f8f2"
|
||||
|
||||
subscription_modal_bg = "#282a36"
|
||||
subscription_modal_text = "#f8f8f2"
|
||||
|
@ -29,13 +29,12 @@
|
||||
# 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"
|
||||
bg = "#2e3440"
|
||||
tab_num = "#88c0d0"
|
||||
tab_divider = "#4c566a"
|
||||
bottombar_label = "#88c0d0"
|
||||
bottombar_text = "#eceff4"
|
||||
bottombar_bg = "#3b4252"
|
||||
|
||||
# hdg_1
|
||||
# hdg_2
|
||||
@ -47,21 +46,21 @@ bottombar_label = "#88c0d0"
|
||||
# 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"
|
||||
hdg_1 = "#5e81ac"
|
||||
hdg_2 = "#81a1c1"
|
||||
hdg_3 = "#8fbcbb"
|
||||
amfora_link = "#88c0d0"
|
||||
foreign_link = "#b48ead"
|
||||
link_number = "#a3be8c"
|
||||
regular_text = "#eceff4"
|
||||
quote_text = "#81a1c1"
|
||||
preformatted_text = "#8fbcbb"
|
||||
list_text = "#d8dee9"
|
||||
|
||||
# btn_bg: The bg color for all modal buttons
|
||||
# btn_text: The text color for all modal buttons
|
||||
btn_bg = "#4c566a"
|
||||
btn_text = "#eceff4"
|
||||
btn_bg = "#4c566a"
|
||||
btn_text = "#eceff4"
|
||||
|
||||
# dl_choice_modal_bg
|
||||
# dl_choice_modal_text
|
||||
@ -75,37 +74,39 @@ btn_text = "#eceff4"
|
||||
# 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"
|
||||
# subscription_modal_bg
|
||||
# subscription_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 = "#eceff4"
|
||||
yesno_modal_bg = "#3b4252"
|
||||
yesno_modal_text = "#eceff4"
|
||||
tofu_modal_bg = "#3b4252"
|
||||
tofu_modal_text = "#eceff4"
|
||||
subscription_modal_bg = "#3b4252"
|
||||
subscription_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"
|
||||
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_bg = "#3b4252"
|
||||
bkmk_modal_text = "#eceff4"
|
||||
bkmk_modal_label = "#eceff4"
|
||||
bkmk_modal_field_bg = "#4c566a"
|
||||
bkmk_modal_field_text = "#eceff4"
|
||||
|
@ -52,15 +52,15 @@ bottombar_label = "#282c34"
|
||||
# preformatted_text
|
||||
# list_text
|
||||
|
||||
hdg_1 = "#c678dd"
|
||||
hdg_1 = "#e06c75"
|
||||
hdg_2 = "#c678dd"
|
||||
hdg_3 = "#c678dd"
|
||||
amfora_link = "#61afef"
|
||||
foreign_link = "#56b6c2"
|
||||
link_number = "#abb2bf"
|
||||
regular_text = "#abb2bf"
|
||||
quote_text = "#abb2bf"
|
||||
preformatted_text = "#abb2bf"
|
||||
quote_text = "#98c379"
|
||||
preformatted_text = "#e5c07b"
|
||||
list_text = "#abb2bf"
|
||||
|
||||
# btn_bg: The bg color for all modal buttons
|
||||
@ -121,3 +121,8 @@ bkmk_modal_text = "#282c34"
|
||||
bkmk_modal_label = "#282c34"
|
||||
bkmk_modal_field_bg = "#282c34"
|
||||
bkmk_modal_field_text = "#abb2bf"
|
||||
|
||||
# subscription_modal_bg
|
||||
# subscription_modal_text
|
||||
subscription_modal_bg = "#c678dd"
|
||||
subscription_modal_text = "#282c34"
|
@ -87,12 +87,51 @@ emoji_favicons = false
|
||||
|
||||
|
||||
[keybindings]
|
||||
# In the future there will be more settings here.
|
||||
# If you have a non-US keyboard, use bind_tab1 through bind_tab0 to
|
||||
# setup the shift-number bindings: Eg, for US keyboards (the default):
|
||||
# bind_tab1 = "!"
|
||||
# bind_tab2 = "@"
|
||||
# bind_tab3 = "#"
|
||||
# bind_tab4 = "$"
|
||||
# bind_tab5 = "%"
|
||||
# bind_tab6 = "^"
|
||||
# bind_tab7 = "&"
|
||||
# bind_tab8 = "*"
|
||||
# bind_tab9 = "("
|
||||
# bind_tab0 = ")"
|
||||
|
||||
# Hold down shift and press the numbers on your keyboard (1,2,3,4,5,6,7,8,9,0) to set this up.
|
||||
# It is default set to be accurate for US keyboards.
|
||||
shift_numbers = "!@#$%^&*()"
|
||||
# Whitespace is not allowed in any of the keybindings! Use 'Space' and 'Tab' to bind to those keys.
|
||||
# Multiple keys can be bound to one command, just use a TOML array.
|
||||
# To add the Alt modifier, the binding must start with Alt-, should be reasonably universal
|
||||
# Ctrl- won't work on all keys, see this for a list:
|
||||
# https://github.com/gdamore/tcell/blob/cb1e5d6fa606/key.go#L83
|
||||
|
||||
# An example of a TOML array for multiple keys being bound to one command is the default
|
||||
# binding for reload:
|
||||
# bind_reload = ["R","Ctrl-R"]
|
||||
# One thing to note here is that "R" is capitalization sensitive, so it means shift-r.
|
||||
# "Ctrl-R" means both ctrl-r and ctrl-shift-R (this is a quirk of what ctrl-r means on
|
||||
# an ANSI terminal)
|
||||
|
||||
# The default binding for opening the bottom bar for entering a URL or link number is:
|
||||
# bind_bottom = "Space"
|
||||
# This is how to get the Spacebar as a keybinding, if you try to use " ", it won't work.
|
||||
# And, finally, an example of a simple, unmodified character is:
|
||||
# bind_edit = "e"
|
||||
# This binds the "e" key to the command to edit the current URL.
|
||||
|
||||
# The bind_link[1-90] options are for the commands to go to the first 10 links on a page,
|
||||
# typically these are bound to the number keys:
|
||||
# bind_link1 = "1"
|
||||
# bind_link2 = "2"
|
||||
# bind_link3 = "3"
|
||||
# bind_link4 = "4"
|
||||
# bind_link5 = "5"
|
||||
# bind_link6 = "6"
|
||||
# bind_link7 = "7"
|
||||
# bind_link8 = "8"
|
||||
# bind_link9 = "9"
|
||||
# bind_link0 = "0"
|
||||
|
||||
[url-handlers]
|
||||
# Allows setting the commands to run for various URL schemes.
|
||||
@ -161,13 +200,15 @@ other = 'off'
|
||||
|
||||
|
||||
[cache]
|
||||
# Options for page cache - which is only for text/gemini pages
|
||||
# Options for page cache - which is only for text pages
|
||||
# Increase the cache size to speed up browsing at the expense of memory
|
||||
|
||||
# Zero values mean there is no limit
|
||||
|
||||
max_size = 0 # Size in bytes
|
||||
max_pages = 30 # The maximum number of pages the cache will store
|
||||
|
||||
# How long a page will stay in cache, in seconds.
|
||||
timeout = 1800 # 30 mins
|
||||
|
||||
[proxies]
|
||||
# Allows setting a Gemini proxy for different schemes.
|
||||
|
@ -3,6 +3,7 @@ package display
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@ -26,6 +27,12 @@ var termH int
|
||||
// The user input and URL display bar at the bottom
|
||||
var bottomBar = cview.NewInputField()
|
||||
|
||||
// When the bottom bar string has a space, this regex decides whether it's
|
||||
// a non-encoded URL or a search string.
|
||||
// See this comment for details:
|
||||
// https://github.com/makeworld-the-better-one/amfora/issues/138#issuecomment-740961292
|
||||
var hasSpaceisURL = regexp.MustCompile(`[^ ]+\.[^ ].*/.`)
|
||||
|
||||
// Viewer for the tab primitives
|
||||
// Pages are named as strings of tab numbers - so the textview for the first tab
|
||||
// is held in the page named "0".
|
||||
@ -52,6 +59,7 @@ var layout = cview.NewFlex().
|
||||
SetDirection(cview.FlexRow)
|
||||
|
||||
var newTabPage structs.Page
|
||||
var versionPage structs.Page
|
||||
|
||||
var App = cview.NewApplication().
|
||||
EnableMouse(false).
|
||||
@ -70,7 +78,21 @@ var App = cview.NewApplication().
|
||||
}(tabs[curTab])
|
||||
})
|
||||
|
||||
func Init() {
|
||||
func Init(version, commit, builtBy string) {
|
||||
versionContent := fmt.Sprintf(
|
||||
"# Amfora Version Info\n\nAmfora: %s\nCommit: %s\nBuilt by: %s",
|
||||
version, commit, builtBy,
|
||||
)
|
||||
renderVersionContent, versionLinks := renderer.RenderGemini(versionContent, textWidth(), leftMargin(), false)
|
||||
versionPage = structs.Page{
|
||||
Raw: versionContent,
|
||||
Content: renderVersionContent,
|
||||
Links: versionLinks,
|
||||
URL: "about:version",
|
||||
Width: -1, // Force reformatting on first display
|
||||
Mediatype: structs.TextGemini,
|
||||
}
|
||||
|
||||
tabRow.SetChangedFunc(func() {
|
||||
App.Draw()
|
||||
})
|
||||
@ -176,14 +198,21 @@ func Init() {
|
||||
} else {
|
||||
// It's a full URL or search term
|
||||
// Detect if it's a search or URL
|
||||
if strings.Contains(query, " ") ||
|
||||
(!strings.Contains(query, "//") && !strings.Contains(query, ".") && !strings.HasPrefix(query, "about:")) {
|
||||
if (strings.Contains(query, " ") && !hasSpaceisURL.MatchString(query)) ||
|
||||
(!strings.HasPrefix(query, "//") && !strings.Contains(query, "://") &&
|
||||
!strings.Contains(query, ".")) {
|
||||
// Has a space and follows regex, OR
|
||||
// doesn't start with "//", contain "://", and doesn't have a dot either.
|
||||
// Then it's a search
|
||||
|
||||
u := viper.GetString("a-general.search") + "?" + gemini.QueryEscape(query)
|
||||
cache.RemovePage(u) // Don't use the cached version of the search
|
||||
// Don't use the cached version of the search
|
||||
cache.RemovePage(normalizeURL(u))
|
||||
URL(u)
|
||||
} else {
|
||||
// Full URL
|
||||
cache.RemovePage(query) // Don't use cached version for manually entered URL
|
||||
// Don't use cached version for manually entered URL
|
||||
cache.RemovePage(normalizeURL(fixUserURL(query)))
|
||||
URL(query)
|
||||
}
|
||||
return
|
||||
@ -245,43 +274,36 @@ func Init() {
|
||||
return event
|
||||
}
|
||||
|
||||
// To add a configurable global key command, you'll need to update one of
|
||||
// the two switch statements here. You'll also need to add an enum entry in
|
||||
// config/keybindings.go, update KeyInit() in config/keybindings.go, add a default
|
||||
// keybinding in config/config.go and update the help panel in display/help.go
|
||||
|
||||
cmd := config.TranslateKeyEvent(event)
|
||||
if tabs[curTab].mode == tabModeDone {
|
||||
// All the keys and operations that can only work while NOT loading
|
||||
|
||||
// History arrow keys
|
||||
if event.Modifiers() == tcell.ModAlt {
|
||||
if event.Key() == tcell.KeyLeft {
|
||||
histBack(tabs[curTab])
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyRight {
|
||||
histForward(tabs[curTab])
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:exhaustive
|
||||
switch event.Key() {
|
||||
case tcell.KeyCtrlR:
|
||||
switch cmd {
|
||||
case config.CmdReload:
|
||||
Reload()
|
||||
return nil
|
||||
case tcell.KeyCtrlH:
|
||||
case config.CmdHome:
|
||||
URL(viper.GetString("a-general.home"))
|
||||
return nil
|
||||
case tcell.KeyCtrlB:
|
||||
case config.CmdBookmarks:
|
||||
Bookmarks(tabs[curTab])
|
||||
tabs[curTab].addToHistory("about:bookmarks")
|
||||
return nil
|
||||
case tcell.KeyCtrlD:
|
||||
case config.CmdAddBookmark:
|
||||
go addBookmark()
|
||||
return nil
|
||||
case tcell.KeyPgUp:
|
||||
case config.CmdPgup:
|
||||
tabs[curTab].pageUp()
|
||||
return nil
|
||||
case tcell.KeyPgDn:
|
||||
case config.CmdPgdn:
|
||||
tabs[curTab].pageDown()
|
||||
return nil
|
||||
case tcell.KeyCtrlS:
|
||||
case config.CmdSave:
|
||||
if tabs[curTab].hasContent() {
|
||||
savePath, err := downloadPage(tabs[curTab].page)
|
||||
if err != nil {
|
||||
@ -293,66 +315,48 @@ func Init() {
|
||||
Info("The current page has no content, so it couldn't be downloaded.")
|
||||
}
|
||||
return nil
|
||||
case tcell.KeyCtrlA:
|
||||
case config.CmdBottom:
|
||||
// Space starts typing, like Bombadillo
|
||||
bottomBar.SetLabel("[::b]URL/Num./Search: [::-]")
|
||||
bottomBar.SetText("")
|
||||
// Don't save bottom bar, so that whenever you switch tabs, it's not in that mode
|
||||
App.SetFocus(bottomBar)
|
||||
return nil
|
||||
case config.CmdEdit:
|
||||
// Letter e allows to edit current URL
|
||||
bottomBar.SetLabel("[::b]Edit URL: [::-]")
|
||||
bottomBar.SetText(tabs[curTab].page.URL)
|
||||
App.SetFocus(bottomBar)
|
||||
return nil
|
||||
case config.CmdBack:
|
||||
histBack(tabs[curTab])
|
||||
return nil
|
||||
case config.CmdForward:
|
||||
histForward(tabs[curTab])
|
||||
return nil
|
||||
case config.CmdSub:
|
||||
Subscriptions(tabs[curTab], "about:subscriptions")
|
||||
tabs[curTab].addToHistory("about:subscriptions")
|
||||
return nil
|
||||
case tcell.KeyCtrlX:
|
||||
case config.CmdAddSub:
|
||||
go addSubscription()
|
||||
return nil
|
||||
case tcell.KeyRune:
|
||||
// Regular key was sent
|
||||
switch string(event.Rune()) {
|
||||
case " ":
|
||||
// Space starts typing, like Bombadillo
|
||||
bottomBar.SetLabel("[::b]URL/Num./Search: [::-]")
|
||||
bottomBar.SetText("")
|
||||
// Don't save bottom bar, so that whenever you switch tabs, it's not in that mode
|
||||
App.SetFocus(bottomBar)
|
||||
return nil
|
||||
case "e":
|
||||
// Letter e allows to edit current URL
|
||||
bottomBar.SetLabel("[::b]Edit URL: [::-]")
|
||||
bottomBar.SetText(tabs[curTab].page.URL)
|
||||
App.SetFocus(bottomBar)
|
||||
return nil
|
||||
case "R":
|
||||
Reload()
|
||||
return nil
|
||||
case "b":
|
||||
histBack(tabs[curTab])
|
||||
return nil
|
||||
case "f":
|
||||
histForward(tabs[curTab])
|
||||
return nil
|
||||
case "u":
|
||||
tabs[curTab].pageUp()
|
||||
return nil
|
||||
case "d":
|
||||
tabs[curTab].pageDown()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Number key: 1-9, 0
|
||||
i, err := strconv.Atoi(string(event.Rune()))
|
||||
if err == nil {
|
||||
if i == 0 {
|
||||
i = 10 // 0 key is for link 10
|
||||
}
|
||||
if i <= len(tabs[curTab].page.Links) && i > 0 {
|
||||
// It's a valid link number
|
||||
followLink(tabs[curTab], tabs[curTab].page.URL, tabs[curTab].page.Links[i-1])
|
||||
return nil
|
||||
}
|
||||
// Number key: 1-9, 0, LINK1-LINK10
|
||||
if cmd >= config.CmdLink1 && cmd <= config.CmdLink0 {
|
||||
if int(cmd) <= len(tabs[curTab].page.Links) {
|
||||
// It's a valid link number
|
||||
followLink(tabs[curTab], tabs[curTab].page.URL, tabs[curTab].page.Links[cmd-1])
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All the keys and operations that can work while a tab IS loading
|
||||
|
||||
//nolint:exhaustive
|
||||
switch event.Key() {
|
||||
case tcell.KeyCtrlT:
|
||||
switch cmd {
|
||||
case config.CmdNewTab:
|
||||
if tabs[curTab].page.Mode == structs.ModeLinkSelect {
|
||||
next, err := resolveRelLink(tabs[curTab], tabs[curTab].page.URL, tabs[curTab].page.Selected)
|
||||
if err != nil {
|
||||
@ -365,45 +369,33 @@ func Init() {
|
||||
NewTab()
|
||||
}
|
||||
return nil
|
||||
case tcell.KeyCtrlW:
|
||||
case config.CmdCloseTab:
|
||||
CloseTab()
|
||||
return nil
|
||||
case tcell.KeyCtrlQ:
|
||||
case config.CmdQuit:
|
||||
Stop()
|
||||
return nil
|
||||
case tcell.KeyCtrlC:
|
||||
Stop()
|
||||
return nil
|
||||
case tcell.KeyF1:
|
||||
case config.CmdPrevTab:
|
||||
// Wrap around, allow for modulo with negative numbers
|
||||
n := NumTabs()
|
||||
SwitchTab((((curTab - 1) % n) + n) % n)
|
||||
return nil
|
||||
case tcell.KeyF2:
|
||||
case config.CmdNextTab:
|
||||
SwitchTab((curTab + 1) % NumTabs())
|
||||
return nil
|
||||
case tcell.KeyRune:
|
||||
// Regular key was sent
|
||||
case config.CmdHelp:
|
||||
Help()
|
||||
return nil
|
||||
}
|
||||
|
||||
if num, err := config.KeyToNum(event.Rune()); err == nil {
|
||||
// It's a Shift+Num key
|
||||
if num == 0 {
|
||||
// Zero key goes to the last tab
|
||||
SwitchTab(NumTabs() - 1)
|
||||
} else {
|
||||
SwitchTab(num - 1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
switch string(event.Rune()) {
|
||||
case "q":
|
||||
Stop()
|
||||
return nil
|
||||
case "?":
|
||||
Help()
|
||||
return nil
|
||||
if cmd >= config.CmdTab1 && cmd <= config.CmdTab0 {
|
||||
if cmd == config.CmdTab0 {
|
||||
// Zero key goes to the last tab
|
||||
SwitchTab(NumTabs() - 1)
|
||||
} else {
|
||||
SwitchTab(int(cmd - config.CmdTab1))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Let another element handle the event, it's not a special global key
|
||||
@ -439,10 +431,8 @@ func NewTab() {
|
||||
tabs = append(tabs, makeNewTab())
|
||||
temp := newTabPage // Copy
|
||||
setPage(tabs[curTab], &temp)
|
||||
|
||||
// Can't go backwards, but this isn't the first page either.
|
||||
// The first page will be the next one the user goes to.
|
||||
tabs[curTab].history.pos = -1
|
||||
tabs[curTab].addToHistory("about:newtab")
|
||||
tabs[curTab].history.pos = 0 // Manually set as first page
|
||||
|
||||
tabPages.AddAndSwitchToPage(strconv.Itoa(curTab), tabs[curTab].view, true)
|
||||
App.SetFocus(tabs[curTab].view)
|
||||
@ -584,11 +574,7 @@ func URL(u string) {
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(u, "//") && !strings.HasPrefix(u, "gemini://") && !strings.Contains(u, "://") {
|
||||
// Assume it's a Gemini URL
|
||||
u = "gemini://" + u
|
||||
}
|
||||
go goURL(t, u)
|
||||
go goURL(t, fixUserURL(u))
|
||||
}
|
||||
|
||||
func NumTabs() int {
|
||||
|
@ -19,7 +19,7 @@ import (
|
||||
"github.com/makeworld-the-better-one/amfora/structs"
|
||||
"github.com/makeworld-the-better-one/amfora/sysopen"
|
||||
"github.com/makeworld-the-better-one/go-gemini"
|
||||
"github.com/makeworld-the-better-one/progressbar/v3"
|
||||
"github.com/schollz/progressbar/v3"
|
||||
"github.com/spf13/viper"
|
||||
"gitlab.com/tslocum/cview"
|
||||
)
|
||||
|
@ -31,6 +31,10 @@ func handleFile(u string) (*structs.Page, bool) {
|
||||
|
||||
switch mode := fi.Mode(); {
|
||||
case mode.IsDir():
|
||||
// Must end in slash
|
||||
if u[len(u)-1] != '/' {
|
||||
u += "/"
|
||||
}
|
||||
return createDirectoryListing(u)
|
||||
case mode.IsRegular():
|
||||
if fi.Size() > viper.GetInt64("a-general.page_max_size") {
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"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/rr"
|
||||
"github.com/makeworld-the-better-one/amfora/structs"
|
||||
"github.com/makeworld-the-better-one/amfora/subscriptions"
|
||||
"github.com/makeworld-the-better-one/amfora/webbrowser"
|
||||
@ -186,6 +187,11 @@ func handleAbout(t *tab, u string) (string, bool) {
|
||||
setPage(t, &temp)
|
||||
t.applyBottomBar()
|
||||
return u, true
|
||||
case "about:version":
|
||||
temp := versionPage
|
||||
setPage(t, &temp)
|
||||
t.applyBottomBar()
|
||||
return u, true
|
||||
}
|
||||
|
||||
if u == "about:subscriptions" || (len(u) > 20 && u[:20] == "about:subscriptions?") {
|
||||
@ -362,6 +368,10 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
|
||||
Error("URL Fetch Error", err.Error())
|
||||
return ret("", false)
|
||||
}
|
||||
|
||||
// Fetch happened successfully, use RestartReader to buffer read data
|
||||
res.Body = rr.NewRestartReader(res.Body)
|
||||
|
||||
if renderer.CanDisplay(res) {
|
||||
page, err := renderer.MakePage(u, res, textWidth(), leftMargin(), usingProxy)
|
||||
// Rendering may have taken a while, make sure tab is still valid
|
||||
@ -369,35 +379,20 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
|
||||
return ret("", false)
|
||||
}
|
||||
|
||||
var res2 *gemini.Response
|
||||
var dlErr error
|
||||
|
||||
if errors.Is(err, renderer.ErrTooLarge) {
|
||||
// Make new request for downloading purposes
|
||||
if usingProxy {
|
||||
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())
|
||||
return ret("", false)
|
||||
}
|
||||
go dlChoice("That page is too large. What would you like to do?", u, res2)
|
||||
// Downloading now
|
||||
// Disable read timeout and go back to start
|
||||
res.SetReadTimeout(0) //nolint: errcheck
|
||||
res.Body.(*rr.RestartReader).Restart()
|
||||
go dlChoice("That page is too large. What would you like to do?", u, res)
|
||||
return ret("", false)
|
||||
}
|
||||
if errors.Is(err, renderer.ErrTimedOut) {
|
||||
// Make new request for downloading purposes
|
||||
if usingProxy {
|
||||
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())
|
||||
return ret("", false)
|
||||
}
|
||||
go dlChoice("Loading that page timed out. What would you like to do?", u, res2)
|
||||
// Downloading now
|
||||
// Disable read timeout and go back to start
|
||||
res.SetReadTimeout(0) //nolint: errcheck
|
||||
res.Body.(*rr.RestartReader).Restart()
|
||||
go dlChoice("Loading that page timed out. What would you like to do?", u, res)
|
||||
return ret("", false)
|
||||
}
|
||||
if err != nil {
|
||||
@ -416,7 +411,7 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
|
||||
return ret(u, true)
|
||||
}
|
||||
// Not displayable
|
||||
// Could be a non 20 (or 21) status code, or a different kind of document
|
||||
// Could be a non 20 status code, or a different kind of document
|
||||
|
||||
// Handle each status code
|
||||
switch res.Status {
|
||||
@ -424,7 +419,6 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
|
||||
userInput, ok := Input(res.Meta)
|
||||
if ok {
|
||||
// Make another request with the query string added
|
||||
// + chars are replaced because PathEscape doesn't do that
|
||||
parsed.RawQuery = gemini.QueryEscape(userInput)
|
||||
if len(parsed.String()) > gemini.URLMaxLength {
|
||||
Error("Input Error", "URL for that input would be too long.")
|
||||
@ -510,6 +504,9 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
|
||||
added := addFeedDirect(u, feed, subscriptions.IsSubscribed(u))
|
||||
if !added {
|
||||
// Otherwise offer download choices
|
||||
// Disable read timeout and go back to start
|
||||
res.SetReadTimeout(0) //nolint: errcheck
|
||||
res.Body.(*rr.RestartReader).Restart()
|
||||
go dlChoice("That file could not be displayed. What would you like to do?", u, res)
|
||||
}
|
||||
}()
|
||||
@ -517,6 +514,9 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
|
||||
}
|
||||
|
||||
// Otherwise offer download choices
|
||||
// Disable read timeout and go back to start
|
||||
res.SetReadTimeout(0) //nolint: errcheck
|
||||
res.Body.(*rr.RestartReader).Restart()
|
||||
go dlChoice("That file could not be displayed. What would you like to do?", u, res)
|
||||
return ret("", false)
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
package display
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/makeworld-the-better-one/amfora/config"
|
||||
"gitlab.com/tslocum/cview"
|
||||
)
|
||||
|
||||
@ -12,41 +14,39 @@ var helpCells = strings.TrimSpace(`
|
||||
?|Bring up this help. You can scroll!
|
||||
Esc|Leave the help
|
||||
Arrow keys, h/j/k/l|Scroll and move a page.
|
||||
PgUp, u|Go up a page in document
|
||||
PgDn, d|Go down a page in document
|
||||
%s|Go up a page in document
|
||||
%s|Go down a page in document
|
||||
g|Go to top of document
|
||||
G|Go to bottom of document
|
||||
Tab|Navigate to the next item in a popup.
|
||||
Shift-Tab|Navigate to the previous item in a popup.
|
||||
b, Alt-Left|Go back in the history
|
||||
f, Alt-Right|Go forward in the history
|
||||
spacebar|Open bar at the bottom - type a URL, link number, search term.
|
||||
%s|Go back in the history
|
||||
%s|Go forward in the history
|
||||
%s|Open bar at the bottom - type a URL, link number, search term.
|
||||
|You can also type two dots (..) to go up a directory in the URL.
|
||||
|Typing new:N will open link number N in a new tab
|
||||
|instead of the current one.
|
||||
Numbers|Go to links 1-10 respectively.
|
||||
e|Edit current URL
|
||||
%s|Go to links 1-10 respectively.
|
||||
%s|Edit current URL
|
||||
Enter, Tab|On a page this will start link highlighting.
|
||||
|Press Tab and Shift-Tab to pick different links.
|
||||
|Press Enter again to go to one, or Esc to stop.
|
||||
Shift-NUMBER|Go to a specific tab.
|
||||
Shift-0, )|Go to the last tab.
|
||||
F1|Previous tab
|
||||
F2|Next tab
|
||||
Ctrl-H|Go home
|
||||
Ctrl-T|New tab, or if a link is selected,
|
||||
%s|Go to a specific tab. (Default: Shift-NUMBER)
|
||||
%s|Go to the last tab.
|
||||
%s|Previous tab
|
||||
%s|Next tab
|
||||
%s|Go home
|
||||
%s|New tab, or if a link is selected,
|
||||
|this will open the link in a new tab.
|
||||
Ctrl-W|Close tab. For now, only the right-most tab can be closed.
|
||||
Ctrl-R, R|Reload a page, discarding the cached version.
|
||||
%s|Close tab. For now, only the right-most tab can be closed.
|
||||
%s|Reload a page, discarding the cached version.
|
||||
|This can also be used if you resize your terminal.
|
||||
Ctrl-B|View bookmarks
|
||||
Ctrl-D|Add, change, or remove a bookmark for the current page.
|
||||
Ctrl-S|Save the current page to your downloads.
|
||||
Ctrl-A|View subscriptions
|
||||
Ctrl-X|Add or update a subscription
|
||||
q, Ctrl-Q|Quit
|
||||
Ctrl-C|Hard quit. This can be used when in the middle of downloading,
|
||||
|for example.
|
||||
%s|View bookmarks
|
||||
%s|Add, change, or remove a bookmark for the current page.
|
||||
%s|Save the current page to your downloads.
|
||||
%s|View subscriptions
|
||||
%s|Add or update a subscription
|
||||
%s|Quit
|
||||
`)
|
||||
|
||||
var helpTable = cview.NewTable().
|
||||
@ -71,6 +71,36 @@ func helpInit() {
|
||||
App.Draw()
|
||||
}
|
||||
})
|
||||
|
||||
tabKeys := fmt.Sprintf("%s to %s", strings.Split(config.GetKeyBinding(config.CmdTab1), ",")[0],
|
||||
strings.Split(config.GetKeyBinding(config.CmdTab9), ",")[0])
|
||||
linkKeys := fmt.Sprintf("%s to %s", strings.Split(config.GetKeyBinding(config.CmdLink1), ",")[0],
|
||||
strings.Split(config.GetKeyBinding(config.CmdLink0), ",")[0])
|
||||
|
||||
helpCells = fmt.Sprintf(helpCells,
|
||||
config.GetKeyBinding(config.CmdPgup),
|
||||
config.GetKeyBinding(config.CmdPgdn),
|
||||
config.GetKeyBinding(config.CmdBack),
|
||||
config.GetKeyBinding(config.CmdForward),
|
||||
config.GetKeyBinding(config.CmdBottom),
|
||||
linkKeys,
|
||||
config.GetKeyBinding(config.CmdEdit),
|
||||
tabKeys,
|
||||
config.GetKeyBinding(config.CmdTab0),
|
||||
config.GetKeyBinding(config.CmdPrevTab),
|
||||
config.GetKeyBinding(config.CmdNextTab),
|
||||
config.GetKeyBinding(config.CmdHome),
|
||||
config.GetKeyBinding(config.CmdNewTab),
|
||||
config.GetKeyBinding(config.CmdCloseTab),
|
||||
config.GetKeyBinding(config.CmdReload),
|
||||
config.GetKeyBinding(config.CmdBookmarks),
|
||||
config.GetKeyBinding(config.CmdAddBookmark),
|
||||
config.GetKeyBinding(config.CmdSave),
|
||||
config.GetKeyBinding(config.CmdSub),
|
||||
config.GetKeyBinding(config.CmdAddSub),
|
||||
config.GetKeyBinding(config.CmdQuit),
|
||||
)
|
||||
|
||||
rows := strings.Count(helpCells, "\n") + 1
|
||||
cells := strings.Split(
|
||||
strings.ReplaceAll(helpCells, "\n", "|"),
|
||||
|
@ -17,11 +17,18 @@ You can customize this page by creating a gemtext file called newtab.gmi, in Amf
|
||||
|
||||
Happy browsing!
|
||||
|
||||
## Internal Pages
|
||||
|
||||
=> about:bookmarks Bookmarks
|
||||
=> about:subscriptions Subscriptions
|
||||
|
||||
## Learn more about Amfora!
|
||||
|
||||
=> https://github.com/makeworld-the-better-one/amfora Amfora homepage
|
||||
=> https://github.com/makeworld-the-better-one/amfora/wiki Amfora Wiki [GitHub]
|
||||
=> gemini://makeworld.gq/amfora-wiki/ Amfora Wiki [On Gemini!]
|
||||
|
||||
=> //gemini.circumlunar.space Project Gemini
|
||||
=> https://github.com/makeworld-the-better-one/amfora Amfora homepage [HTTPS]
|
||||
`
|
||||
|
||||
// Read the new tab content from a file if it exists or fallback to a default page.
|
||||
|
@ -5,8 +5,10 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/makeworld-the-better-one/go-gemini"
|
||||
"github.com/spf13/viper"
|
||||
"gitlab.com/tslocum/cview"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
|
||||
// This file contains funcs that are small, self-contained utilities.
|
||||
@ -73,15 +75,23 @@ func resolveRelLink(t *tab, prev, next string) (string, error) {
|
||||
// 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 {
|
||||
parsed, err := url.Parse(u)
|
||||
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
|
||||
@ -102,7 +112,32 @@ func normalizeURL(u string) string {
|
||||
// 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
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
//nolint: lll
|
||||
package display
|
||||
|
||||
import (
|
||||
@ -21,6 +22,11 @@ var normalizeURLTests = []struct {
|
||||
{"mailto:example@example.com", "mailto:example@example.com"},
|
||||
{"magnet:?xt=urn:btih:test", "magnet:?xt=urn:btih:test"},
|
||||
{"https://example.com", "https://example.com"},
|
||||
// Fixing URL tests
|
||||
{"gemini://gemini.circumlunar.space/%64%6f%63%73/%66%61%71%2e%67%6d%69", "gemini://gemini.circumlunar.space/docs/faq.gmi"},
|
||||
{"gemini://example.com/蛸", "gemini://example.com/%E8%9B%B8"},
|
||||
{"gemini://gemini.circumlunar.space/%64%6f%63%73/;;.'%66%61%71蛸%2e%67%6d%69", "gemini://gemini.circumlunar.space/docs/%3B%3B.%27faq%E8%9B%B8.gmi"},
|
||||
{"gemini://example.com/?%2Ch%64ello蛸", "gemini://example.com/?%2Chdello%E8%9B%B8"},
|
||||
}
|
||||
|
||||
func TestNormalizeURL(t *testing.T) {
|
||||
|
9
go.mod
9
go.mod
@ -7,14 +7,14 @@ require (
|
||||
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
||||
github.com/gdamore/tcell v1.3.1-0.20200608133353-cb1e5d6fa606
|
||||
github.com/google/go-cmp v0.5.0 // indirect
|
||||
github.com/makeworld-the-better-one/go-gemini v0.9.3
|
||||
github.com/makeworld-the-better-one/go-gemini v0.11.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/mitchellh/go-homedir v1.1.0
|
||||
github.com/mitchellh/mapstructure v1.3.1 // indirect
|
||||
github.com/mmcdole/gofeed v1.1.0
|
||||
github.com/pelletier/go-toml v1.8.0 // indirect
|
||||
github.com/rkoesters/xdg v0.0.0-20181125232953-edd15b846f9b
|
||||
github.com/schollz/progressbar/v3 v3.7.2
|
||||
github.com/spf13/afero v1.2.2 // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
@ -22,10 +22,11 @@ require (
|
||||
github.com/spf13/viper v1.7.0
|
||||
github.com/stretchr/testify v1.6.1
|
||||
gitlab.com/tslocum/cview v1.4.8-0.20200713214710-cc7796c4ca44
|
||||
golang.org/x/sys v0.0.0-20200817155316-9781c653f443 // indirect
|
||||
golang.org/x/text v0.3.3
|
||||
golang.org/x/text v0.3.5-0.20201208001344-75a595aef632
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
gopkg.in/ini.v1 v1.57.0 // indirect
|
||||
)
|
||||
|
||||
replace github.com/mmcdole/gofeed => github.com/makeworld-the-better-one/gofeed v1.1.1-0.20201123002655-c0c6354134fe
|
||||
|
||||
replace github.com/schollz/progressbar/v3 => github.com/makeworld-the-better-one/progressbar/v3 v3.3.5-0.20201220005701-b036c4d38568
|
||||
|
22
go.sum
22
go.sum
@ -133,16 +133,17 @@ github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tW
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.9.3 h1:vpJc1u4LYpEI5h7GcOE2zSfOmpE9gQzt0vEayp/ilWc=
|
||||
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-gemini v0.11.0 h1:MNGiULJFvcqls9oCy40tE897hDeKvNmEK9i5kRucgQk=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.11.0/go.mod h1:F+3x+R1xeYK90jMtBq+U+8Sh64r2dHleDZ/en3YgSmg=
|
||||
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/gofeed v1.1.1-0.20201123002655-c0c6354134fe h1:i3b9Qy5z23DcXRnrsMYcM5s9Ng5VIidM1xZd+szuTsY=
|
||||
github.com/makeworld-the-better-one/gofeed v1.1.1-0.20201123002655-c0c6354134fe/go.mod h1:QQO3maftbOu+hiVOGOZDRLymqGQCos4zxbA4j89gMrE=
|
||||
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/go.mod h1:X6sxWNi9PBgQybpR4fpXPVD5fm7svLqZTQ5DJuERIoM=
|
||||
github.com/makeworld-the-better-one/progressbar/v3 v3.3.5-0.20201220005701-b036c4d38568 h1:fod4pD+rsU73WIUxl8Kpo35LDuOx0uxzlprBKbm84vw=
|
||||
github.com/makeworld-the-better-one/progressbar/v3 v3.3.5-0.20201220005701-b036c4d38568/go.mod h1:CG/f0JmacksUc6TkZToO7tVq4t03zIQSQUtTd7F9GR4=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
@ -249,6 +250,8 @@ golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnf
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9 h1:umElSU9WZirRdgu2yFHY0ayQkEnKiOC1TtM3fWXFnoU=
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@ -286,6 +289,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
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 h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
|
||||
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/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=
|
||||
@ -309,15 +314,20 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200610111108-226ff32320da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200817155316-9781c653f443 h1:X18bCaipMcoJGm27Nv7zr4XYPKGUy92GtqboKC2Hxaw=
|
||||
golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201113135734-0a15ea8d9b02/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5-0.20201208001344-75a595aef632 h1:clKlpQ6BheG1zIRhU2SPRAXpLgol/tqWVEeRkjpsaDI=
|
||||
golang.org/x/text v0.3.5-0.20201208001344-75a595aef632/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"mime"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/makeworld-the-better-one/amfora/structs"
|
||||
"github.com/makeworld-the-better-one/go-gemini"
|
||||
@ -108,6 +109,7 @@ func MakePage(url string, res *gemini.Response, width, leftMargin int, proxied b
|
||||
Raw: utfText,
|
||||
Content: rendered,
|
||||
Links: links,
|
||||
MadeAt: time.Now(),
|
||||
}, nil
|
||||
} else if strings.HasPrefix(mediatype, "text/") {
|
||||
if mediatype == "text/x-ansi" || strings.HasSuffix(url, ".ans") || strings.HasSuffix(url, ".ansi") {
|
||||
@ -119,6 +121,7 @@ func MakePage(url string, res *gemini.Response, width, leftMargin int, proxied b
|
||||
Raw: utfText,
|
||||
Content: RenderANSI(utfText, leftMargin),
|
||||
Links: []string{},
|
||||
MadeAt: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -130,6 +133,7 @@ func MakePage(url string, res *gemini.Response, width, leftMargin int, proxied b
|
||||
Raw: utfText,
|
||||
Content: RenderPlainText(utfText, leftMargin),
|
||||
Links: []string{},
|
||||
MadeAt: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -243,13 +243,18 @@ func convertRegularGemini(s string, numLinks, width int, proxied bool) (string,
|
||||
} else if strings.HasPrefix(lines[i], ">") {
|
||||
// It's a quote line, add extra quote symbols and italics to the start of each wrapped line
|
||||
|
||||
// Remove beginning quote and maybe space
|
||||
lines[i] = strings.TrimPrefix(lines[i], ">")
|
||||
lines[i] = strings.TrimPrefix(lines[i], " ")
|
||||
wrappedLines = append(wrappedLines,
|
||||
wrapLine(lines[i], width, fmt.Sprintf("[%s::i]> ", config.GetColorString("quote_text")),
|
||||
"[-::-]", true)...,
|
||||
)
|
||||
if len(lines[i]) == 1 {
|
||||
// Just an empty quote line
|
||||
wrappedLines = append(wrappedLines, fmt.Sprintf("[%s::i]>[-::-]", config.GetColorString("quote_text")))
|
||||
} else {
|
||||
// Remove beginning quote and maybe space
|
||||
lines[i] = strings.TrimPrefix(lines[i], ">")
|
||||
lines[i] = strings.TrimPrefix(lines[i], " ")
|
||||
wrappedLines = append(wrappedLines,
|
||||
wrapLine(lines[i], width, fmt.Sprintf("[%s::i]> ", config.GetColorString("quote_text")),
|
||||
"[-::-]", true)...,
|
||||
)
|
||||
}
|
||||
|
||||
} else if strings.TrimSpace(lines[i]) == "" {
|
||||
// Just add empty line without processing
|
||||
|
37
rr/README.md
Normal file
37
rr/README.md
Normal file
@ -0,0 +1,37 @@
|
||||
# package `rr`, aka `RestartReader`
|
||||
|
||||
This package exists just to hold the `RestartReader` type. It wraps `io.ReadCloser` and implements it. It holds the data from every `Read` in a `[]byte` buffer, and allows you to call `.Restart()`, causing subsequent `Read` calls to start from the beginning again.
|
||||
|
||||
See [#140](https://github.com/makeworld-the-better-one/amfora/issues/140) for why this was needed.
|
||||
|
||||
Other projects are encouraged to copy this code if it's useful to them, and this package may move out of Amfora if I end up using it in multiple projects.
|
||||
|
||||
## License
|
||||
|
||||
If you prefer, you can consider the code in this package, and this package only, to be licensed under the MIT license instead.
|
||||
|
||||
<details>
|
||||
<summary>Click to see MIT license terms</summary>
|
||||
|
||||
```
|
||||
Copyright (c) 2020 makeworld
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
```
|
||||
</details>
|
81
rr/rr.go
Normal file
81
rr/rr.go
Normal file
@ -0,0 +1,81 @@
|
||||
package rr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
var ErrClosed = errors.New("RestartReader: closed")
|
||||
|
||||
type RestartReader struct {
|
||||
r io.ReadCloser
|
||||
buf []byte
|
||||
|
||||
// Where in the buffer we are. If it's equal to len(buf) then the reader
|
||||
// should be used.
|
||||
i int64
|
||||
}
|
||||
|
||||
func (rr *RestartReader) Read(p []byte) (n int, err error) {
|
||||
if rr.buf == nil {
|
||||
return 0, ErrClosed
|
||||
}
|
||||
|
||||
if rr.i >= int64(len(rr.buf)) {
|
||||
// Read new data
|
||||
tmp := make([]byte, len(p))
|
||||
n, err = rr.r.Read(tmp)
|
||||
if n > 0 {
|
||||
rr.buf = append(rr.buf, tmp[:n]...)
|
||||
copy(p, tmp[:n])
|
||||
}
|
||||
rr.i = int64(len(rr.buf))
|
||||
return
|
||||
}
|
||||
|
||||
// Reading from buffer
|
||||
|
||||
bufSize := len(rr.buf[rr.i:])
|
||||
|
||||
if len(p) > bufSize {
|
||||
// It wants more data then what's in the buffer
|
||||
tmp := make([]byte, len(p)-bufSize)
|
||||
n, err = rr.r.Read(tmp)
|
||||
if n > 0 {
|
||||
rr.buf = append(rr.buf, tmp[:n]...)
|
||||
}
|
||||
copy(p, rr.buf[rr.i:])
|
||||
n += bufSize
|
||||
rr.i = int64(len(rr.buf))
|
||||
return
|
||||
}
|
||||
// All the required data is in the buffer
|
||||
end := rr.i + int64(len(p))
|
||||
copy(p, rr.buf[rr.i:end])
|
||||
rr.i = end
|
||||
n = len(p)
|
||||
err = nil
|
||||
return
|
||||
}
|
||||
|
||||
// Restart causes subsequent Read calls to read from the beginning, instead
|
||||
// of where they left off.
|
||||
func (rr *RestartReader) Restart() {
|
||||
rr.i = 0
|
||||
}
|
||||
|
||||
// Close clears the buffer and closes the underlying io.ReadCloser, returning
|
||||
// its error.
|
||||
func (rr *RestartReader) Close() error {
|
||||
rr.buf = nil
|
||||
return rr.r.Close()
|
||||
}
|
||||
|
||||
// NewRestartReader creates and initializes a new RestartReader that reads from
|
||||
// the provided io.ReadCloser.
|
||||
func NewRestartReader(r io.ReadCloser) *RestartReader {
|
||||
return &RestartReader{
|
||||
r: r,
|
||||
buf: make([]byte, 0),
|
||||
}
|
||||
}
|
45
rr/rr_test.go
Normal file
45
rr/rr_test.go
Normal file
@ -0,0 +1,45 @@
|
||||
package rr
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var r1 *RestartReader
|
||||
|
||||
func reset() {
|
||||
r1 = NewRestartReader(ioutil.NopCloser(strings.NewReader("1234567890")))
|
||||
}
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
reset()
|
||||
p := make([]byte, 1)
|
||||
n, err := r1.Read(p)
|
||||
assert.Equal(t, 1, n, "should read one byte")
|
||||
assert.Equal(t, nil, err, "should be no error")
|
||||
assert.Equal(t, []byte{'1'}, p, "should have read one byte, '1'")
|
||||
}
|
||||
|
||||
//nolint
|
||||
func TestRestart(t *testing.T) {
|
||||
reset()
|
||||
p := make([]byte, 4)
|
||||
r1.Read(p)
|
||||
|
||||
r1.Restart()
|
||||
p = make([]byte, 5)
|
||||
n, err := r1.Read(p)
|
||||
assert.Equal(t, []byte("12345"), p, "should read the first 5 bytes again")
|
||||
assert.Equal(t, 5, n, "should have read 4 bytes")
|
||||
assert.Equal(t, nil, err, "err should be nil")
|
||||
|
||||
r1.Restart()
|
||||
p = make([]byte, 4)
|
||||
n, err = r1.Read(p)
|
||||
assert.Equal(t, []byte("1234"), p, "should read the first 4 bytes again")
|
||||
assert.Equal(t, 4, n, "should have read 4 bytes")
|
||||
assert.Equal(t, nil, err, "err should be nil")
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
package structs
|
||||
|
||||
import "time"
|
||||
|
||||
type Mediatype string
|
||||
|
||||
const (
|
||||
@ -31,6 +33,7 @@ type Page struct {
|
||||
SelectedID string // The cview region ID for the selected text/link
|
||||
Mode PageMode
|
||||
Favicon string
|
||||
MadeAt time.Time // When the page was made. Zero value indicates it should stay in cache forever.
|
||||
}
|
||||
|
||||
// Size returns an approx. size of a Page in bytes.
|
||||
|
@ -112,14 +112,14 @@ func GetPageEntries() *PageEntries {
|
||||
|
||||
// Path is title
|
||||
title := parsed.Path
|
||||
if strings.HasPrefix(title, "/~") {
|
||||
if strings.HasPrefix(title, "/~") && title != "/~" {
|
||||
// A user dir
|
||||
title = title[2:] // Remove beginning slash and tilde
|
||||
// Remove trailing slash if the root of a user dir is being tracked
|
||||
if strings.Count(title, "/") <= 1 && title[len(title)-1] == '/' {
|
||||
title = title[:len(title)-1]
|
||||
}
|
||||
} else if strings.HasPrefix(title, "/users/") {
|
||||
} else if strings.HasPrefix(title, "/users/") && title != "/users/" {
|
||||
// "/users/" is removed for aesthetics when tracking hosted users
|
||||
title = strings.TrimPrefix(title, "/users/")
|
||||
title = strings.TrimPrefix(title, "~") // Remove leading tilde
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
urlPkg "net/url"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
@ -23,9 +24,10 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrSaving = errors.New("couldn't save JSON to disk")
|
||||
ErrNotSuccess = errors.New("status 20 not returned")
|
||||
ErrNotFeed = errors.New("not a valid feed")
|
||||
ErrSaving = errors.New("couldn't save JSON to disk")
|
||||
ErrNotSuccess = errors.New("status 20 not returned")
|
||||
ErrNotFeed = errors.New("not a valid feed")
|
||||
ErrTooManyRedirects = errors.New("redirected more than 5 times")
|
||||
)
|
||||
|
||||
var writeMu = sync.Mutex{} // Prevent concurrent writes to subscriptions.json file
|
||||
@ -58,9 +60,12 @@ func Init() error {
|
||||
} else if !os.IsNotExist(err) {
|
||||
// There's an error opening the file, but it's not bc is doesn't exist
|
||||
return fmt.Errorf("open subscriptions.json error: %w", err)
|
||||
} else {
|
||||
// File does not exist, initialize maps
|
||||
}
|
||||
|
||||
if data.Feeds == nil {
|
||||
data.Feeds = make(map[string]*gofeed.Feed)
|
||||
}
|
||||
if data.Pages == nil {
|
||||
data.Pages = make(map[string]*pageJSON)
|
||||
}
|
||||
|
||||
@ -115,7 +120,8 @@ func GetFeed(mediatype, filename string, r io.Reader) (*gofeed.Feed, bool) {
|
||||
// Check mediatype and filename
|
||||
if mediatype != "application/atom+xml" && mediatype != "application/rss+xml" && mediatype != "application/json+feed" &&
|
||||
filename != "atom.xml" && filename != "feed.xml" && filename != "feed.json" &&
|
||||
!strings.HasSuffix(filename, ".atom") && !strings.HasSuffix(filename, ".rss") {
|
||||
!strings.HasSuffix(filename, ".atom") && !strings.HasSuffix(filename, ".rss") &&
|
||||
!strings.HasSuffix(filename, ".xml") {
|
||||
// No part of the above is true
|
||||
return nil, false
|
||||
}
|
||||
@ -229,46 +235,133 @@ func AddPage(url string, r io.Reader) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateFeed(url string) error {
|
||||
// getResource returns a URL and Response for the given URL.
|
||||
// It will follow up to 5 redirects, and if there is a permanent
|
||||
// redirect it will return the new URL. Otherwise the URL will
|
||||
// stay the same. THe returned URL will never be empty.
|
||||
//
|
||||
// If there is over 5 redirects the error will be ErrTooManyRedirects.
|
||||
// ErrNotSuccess, as well as other fetch errors will also be returned.
|
||||
func getResource(url string) (string, *gemini.Response, error) {
|
||||
res, err := client.Fetch(url)
|
||||
if err != nil {
|
||||
if res != nil {
|
||||
res.Body.Close()
|
||||
}
|
||||
return err
|
||||
return url, nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.Status != gemini.StatusSuccess {
|
||||
return ErrNotSuccess
|
||||
if res.Status == gemini.StatusSuccess {
|
||||
// No redirects
|
||||
return url, res, nil
|
||||
}
|
||||
mediatype, _, err := mime.ParseMediaType(res.Meta)
|
||||
|
||||
parsed, err := urlPkg.Parse(url)
|
||||
if err != nil {
|
||||
return err
|
||||
return url, nil, err
|
||||
}
|
||||
filename := path.Base(url)
|
||||
feed, ok := GetFeed(mediatype, filename, res.Body)
|
||||
if !ok {
|
||||
return ErrNotFeed
|
||||
|
||||
i := 0
|
||||
redirs := make([]int, 0)
|
||||
urls := make([]*urlPkg.URL, 0)
|
||||
|
||||
// Loop through redirects
|
||||
for (res.Status == gemini.StatusRedirectPermanent || res.Status == gemini.StatusRedirectTemporary) && i < 5 {
|
||||
redirs = append(redirs, res.Status)
|
||||
urls = append(urls, parsed)
|
||||
|
||||
tmp, err := parsed.Parse(res.Meta)
|
||||
if err != nil {
|
||||
// Redirect URL returned by the server is invalid
|
||||
return url, nil, err
|
||||
}
|
||||
parsed = tmp
|
||||
|
||||
// Make the new request
|
||||
res, err := client.Fetch(parsed.String())
|
||||
if err != nil {
|
||||
if res != nil {
|
||||
res.Body.Close()
|
||||
}
|
||||
return url, nil, err
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
return AddFeed(url, feed)
|
||||
|
||||
// Two possible options here:
|
||||
// - Never redirected, got error on start
|
||||
// - No more redirects, other status code
|
||||
// - Too many redirects
|
||||
|
||||
if i == 0 {
|
||||
// Never redirected or succeeded
|
||||
return url, res, ErrNotSuccess
|
||||
}
|
||||
|
||||
if i < 5 {
|
||||
// The server stopped redirecting after <5 redirects
|
||||
|
||||
if res.Status == gemini.StatusSuccess {
|
||||
// It ended by succeeding
|
||||
|
||||
for j := range redirs {
|
||||
if redirs[j] == gemini.StatusRedirectTemporary {
|
||||
if j == 0 {
|
||||
// First redirect is temporary
|
||||
return url, res, nil
|
||||
}
|
||||
// There were permanent redirects before this one
|
||||
// Return the URL of the latest permanent redirect
|
||||
return urls[j-1].String(), res, nil
|
||||
}
|
||||
}
|
||||
// They were all permanent redirects
|
||||
return urls[len(urls)-1].String(), res, nil
|
||||
}
|
||||
|
||||
// It stopped because there was a non-redirect, non-success response
|
||||
return url, res, ErrNotSuccess
|
||||
}
|
||||
|
||||
// Too many redirects, return original
|
||||
return url, nil, ErrTooManyRedirects
|
||||
}
|
||||
|
||||
func updatePage(url string) error {
|
||||
res, err := client.Fetch(url)
|
||||
func updateFeed(url string) {
|
||||
newURL, res, err := getResource(url)
|
||||
if err != nil {
|
||||
if res != nil {
|
||||
res.Body.Close()
|
||||
}
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.Status != gemini.StatusSuccess {
|
||||
return ErrNotSuccess
|
||||
return
|
||||
}
|
||||
|
||||
return AddPage(url, res.Body)
|
||||
mediatype, _, err := mime.ParseMediaType(res.Meta)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
filename := path.Base(newURL)
|
||||
feed, ok := GetFeed(mediatype, filename, res.Body)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
err = AddFeed(newURL, feed)
|
||||
if url != newURL && err == nil {
|
||||
// URL has changed, remove old one
|
||||
Remove(url) //nolint:errcheck
|
||||
}
|
||||
}
|
||||
|
||||
func updatePage(url string) {
|
||||
newURL, res, err := getResource(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = AddPage(newURL, res.Body)
|
||||
if url != newURL && err == nil {
|
||||
// URL has changed, remove old one
|
||||
Remove(url) //nolint:errcheck
|
||||
}
|
||||
}
|
||||
|
||||
// updateAll updates all subscriptions using workers.
|
||||
|
Loading…
Reference in New Issue
Block a user