mirror of
https://github.com/makew0rld/amfora.git
synced 2024-12-04 14:46:29 -05:00
🔀 Merge branch 'master' into feeds
This commit is contained in:
commit
3a63b73300
15
.github/workflows/golangci-lint.yml
vendored
Normal file
15
.github/workflows/golangci-lint.yml
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
name: golangci-lint
|
||||||
|
on: [push, pull_request]
|
||||||
|
jobs:
|
||||||
|
golangci:
|
||||||
|
name: lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v2
|
||||||
|
with:
|
||||||
|
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
|
||||||
|
version: v1.30
|
||||||
|
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
||||||
|
only-new-issues: true
|
38
.golangci.yml
Normal file
38
.golangci.yml
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
linters:
|
||||||
|
fast: false
|
||||||
|
disable-all: true
|
||||||
|
enable:
|
||||||
|
- deadcode
|
||||||
|
- errcheck
|
||||||
|
- gosimple
|
||||||
|
- govet
|
||||||
|
- ineffassign
|
||||||
|
- staticcheck
|
||||||
|
- structcheck
|
||||||
|
- typecheck
|
||||||
|
- unused
|
||||||
|
- varcheck
|
||||||
|
- dupl
|
||||||
|
- exhaustive
|
||||||
|
- exportloopref
|
||||||
|
- goconst
|
||||||
|
- gocritic
|
||||||
|
- goerr113
|
||||||
|
- gofmt
|
||||||
|
- goimports
|
||||||
|
- golint
|
||||||
|
- goprintffuncname
|
||||||
|
- interfacer
|
||||||
|
- lll
|
||||||
|
- maligned
|
||||||
|
- misspell
|
||||||
|
- nakedret
|
||||||
|
- nolintlint
|
||||||
|
- prealloc
|
||||||
|
- scopelint
|
||||||
|
- unconvert
|
||||||
|
- unparam
|
||||||
|
|
||||||
|
issues:
|
||||||
|
exclude-use-default: true
|
||||||
|
max-issues-per-linter: 0
|
27
.travis.yml
Normal file
27
.travis.yml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
language: go
|
||||||
|
|
||||||
|
go:
|
||||||
|
#- "1.11" # Debian Stable golang version, fails - see below
|
||||||
|
#- "1.12" # Also fails due to progressbar Millisecond requirement
|
||||||
|
- "1.13"
|
||||||
|
- "1.14"
|
||||||
|
- "1.15"
|
||||||
|
|
||||||
|
script:
|
||||||
|
- go test ./...
|
||||||
|
- go build
|
||||||
|
|
||||||
|
env:
|
||||||
|
GO111MODULE=on
|
||||||
|
|
||||||
|
cache:
|
||||||
|
directories:
|
||||||
|
- $HOME/.cache/go-build
|
||||||
|
- $GOPATH/pkg/mod
|
||||||
|
|
||||||
|
# TODO: GitHub Releases deploy
|
||||||
|
|
||||||
|
notifications:
|
||||||
|
email:
|
||||||
|
on_success: never
|
||||||
|
on_failure: always
|
@ -9,14 +9,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Added
|
### Added
|
||||||
- **Feed & page subscription** (#61)
|
- **Feed & page subscription** (#61)
|
||||||
- **Emoji favicons** can now be seen if `emoji_favicons` is enabled in the config (#62)
|
- **Emoji favicons** can now be seen if `emoji_favicons` is enabled in the config (#62)
|
||||||
|
- **Proxy support** - specify a proxy in the config for all requests to go through it (#66)
|
||||||
- The `shift_numbers` key in the config was added, so that non US keyboard users can navigate tabs (#64)
|
- The `shift_numbers` key in the config was added, so that non US keyboard users can navigate tabs (#64)
|
||||||
- <kbd>F1</kbd> and <kbd>F2</kbd> keys for navigating to the previous and next tabs (#64)
|
- <kbd>F1</kbd> and <kbd>F2</kbd> keys for navigating to the previous and next tabs (#64)
|
||||||
|
- Resolving any relative path (starting with a `.`) in the bottom bar is supported, not just `..` (#71)
|
||||||
|
- Set programs in config to open other schemes like `gopher://` or `magnet:` (#74)
|
||||||
|
- Auto-redirecting can be enabled - redirect within Gemini up to 5 times automatically (#75)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
- Update to [go-gemini](https://github.com/makeworld-the-better-one/go-gemini) v0.8.4
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Two digit (and higher) link texts are now in line with one digit ones (#60)
|
- Two digit (and higher) link texts are now in line with one digit ones (#60)
|
||||||
- Race condition when reloading pages, could have caused the cache to still be used
|
- Race condition when reloading pages, could have caused the cache to still be used
|
||||||
|
- Prevent panic (crash) when the server sends an error with an empty meta string (#73)
|
||||||
|
- URLs with with colon-only schemes (like `mailto:`) are properly recognized
|
||||||
|
|
||||||
|
|
||||||
## [1.4.0] - 2020-07-28
|
## [1.4.0] - 2020-07-28
|
||||||
|
19
README.md
19
README.md
@ -5,7 +5,7 @@
|
|||||||
<h6>Image modified from: amphora by Alvaro Cabrera from the Noun Project</h6>
|
<h6>Image modified from: amphora by Alvaro Cabrera from the Noun Project</h6>
|
||||||
</center>
|
</center>
|
||||||
|
|
||||||
|
[![travis build status](https://img.shields.io/travis/com/makeworld-the-better-one/amfora)](https://https://travis-ci.com/github/makeworld-the-better-one/amfora)
|
||||||
[![go reportcard](https://goreportcard.com/badge/github.com/makeworld-the-better-one/amfora)](https://goreportcard.com/report/github.com/makeworld-the-better-one/amfora)
|
[![go reportcard](https://goreportcard.com/badge/github.com/makeworld-the-better-one/amfora)](https://goreportcard.com/report/github.com/makeworld-the-better-one/amfora)
|
||||||
[![license GPLv3](https://img.shields.io/github/license/makeworld-the-better-one/amfora)](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
[![license GPLv3](https://img.shields.io/github/license/makeworld-the-better-one/amfora)](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||||
|
|
||||||
@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
Amfora aims to be the best looking [Gemini](https://gemini.circumlunar.space/) client with the most features... all in the terminal. It does not support Gopher or other non-Web protocols - check out [Bombadillo](http://bombadillo.colorfield.space/) for that.
|
Amfora aims to be the best looking [Gemini](https://gemini.circumlunar.space/) client with the most features... all in the terminal. It does not support Gopher or other non-Web protocols - check out [Bombadillo](http://bombadillo.colorfield.space/) for that.
|
||||||
|
|
||||||
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. Maybe use Powershell (comes with Windows) or [Cmder](https://cmder.net/) instead. Note that some of the application colors will not display correctly on most Windows terminals, but all functionality will still work.
|
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. 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, including the new Unicode tests. It mostly passes the Egsam test.
|
||||||
|
|
||||||
@ -57,16 +57,16 @@ brew upgrade amfora
|
|||||||
```
|
```
|
||||||
|
|
||||||
### From Source
|
### From Source
|
||||||
This section is for programmers who want to install from source.
|
This section is for programmers who want to install from source. Make sure you're using Go 1.13 at least, as earlier versions will fail to build.
|
||||||
|
|
||||||
Install latest release:
|
Install latest release:
|
||||||
```
|
```
|
||||||
GO111MODULE=on go get -u github.com/makeworld-the-better-one/amfora
|
GO111MODULE=on go get github.com/makeworld-the-better-one/amfora
|
||||||
```
|
```
|
||||||
|
|
||||||
Install latest commit:
|
Install latest commit:
|
||||||
```
|
```
|
||||||
GO111MODULE=on go get -u github.com/makeworld-the-better-one/amfora@master
|
GO111MODULE=on go get github.com/makeworld-the-better-one/amfora@master
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@ -99,15 +99,18 @@ Features in *italics* are in the master branch, but not in the latest release.
|
|||||||
- See `gemini://mozz.us/files/rfc_gemini_favicon.gmi` for details
|
- See `gemini://mozz.us/files/rfc_gemini_favicon.gmi` for details
|
||||||
- [x] *Subscribe to RSS and Atom feeds and display them*
|
- [x] *Subscribe to RSS and Atom feeds and display them*
|
||||||
- Subscribing to page changes, similar to how Spacewalk works, is also supported
|
- Subscribing to page changes, similar to how Spacewalk works, is also supported
|
||||||
- [ ] Stream support
|
- [x] *Proxying*
|
||||||
|
- All requests can optionally be sent through another server
|
||||||
|
- A gemini proxy server implementation currently does not exist, but Amfora will support it when it does!
|
||||||
|
- [ ] Support Markdown rendering
|
||||||
|
- [ ] Search in pages with <kbd>Ctrl-F</kbd>
|
||||||
- [ ] Full client certificate UX within the client
|
- [ ] Full client certificate UX within the client
|
||||||
- Create transient and permanent certs within the client, per domain
|
- Create transient and permanent certs within the client, per domain
|
||||||
- Manage and browse them
|
- Manage and browse them
|
||||||
- Similar to [Kristall](https://github.com/MasterQ32/kristall)
|
- Similar to [Kristall](https://github.com/MasterQ32/kristall)
|
||||||
- https://lists.orbitalfox.eu/archives/gemini/2020/001400.html
|
- https://lists.orbitalfox.eu/archives/gemini/2020/001400.html
|
||||||
|
- [ ] Stream support
|
||||||
- [ ] Table of contents for pages
|
- [ ] Table of contents for pages
|
||||||
- [ ] Search in pages with <kbd>Ctrl-F</kbd>
|
|
||||||
- [ ] Support Markdown rendering
|
|
||||||
- [ ] History browser
|
- [ ] History browser
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
@ -19,7 +19,7 @@ func bkmkKey(url string) string {
|
|||||||
|
|
||||||
func Set(url, name string) {
|
func Set(url, name string) {
|
||||||
bkmkStore.Set(bkmkKey(url), name)
|
bkmkStore.Set(bkmkKey(url), name)
|
||||||
bkmkStore.WriteConfig()
|
bkmkStore.WriteConfig() //nolint:errcheck
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns the NAME of the bookmark, given the URL.
|
// Get returns the NAME of the bookmark, given the URL.
|
||||||
@ -33,7 +33,7 @@ func Remove(url string) {
|
|||||||
// XXX: Viper can't actually delete keys, which means the bookmarks file might get clouded
|
// XXX: Viper can't actually delete keys, which means the bookmarks file might get clouded
|
||||||
// with non-entries over time.
|
// with non-entries over time.
|
||||||
bkmkStore.Set(bkmkKey(url), "")
|
bkmkStore.Set(bkmkKey(url), "")
|
||||||
bkmkStore.WriteConfig()
|
bkmkStore.WriteConfig() //nolint:errcheck
|
||||||
}
|
}
|
||||||
|
|
||||||
// All returns all the bookmarks in a map of URLs to names.
|
// All returns all the bookmarks in a map of URLs to names.
|
||||||
@ -48,9 +48,9 @@ func All() (map[string]string, []string) {
|
|||||||
return bkmks, []string{}
|
return bkmks, []string{}
|
||||||
}
|
}
|
||||||
|
|
||||||
inverted := make(map[string]string) // Holds inverted map, name->URL
|
inverted := make(map[string]string) // Holds inverted map, name->URL
|
||||||
var names []string // Holds bookmark names, for sorting
|
names := make([]string, 0, len(bkmksMap)) // Holds bookmark names, for sorting
|
||||||
var keys []string // Final sorted keys (URLs), for returning at the end
|
keys := make([]string, 0, len(bkmksMap)) // Final sorted keys (URLs), for returning at the end
|
||||||
|
|
||||||
for b32Url, name := range bkmksMap {
|
for b32Url, name := range bkmksMap {
|
||||||
if n, ok := name.(string); n == "" || !ok {
|
if n, ok := name.(string); n == "" || !ok {
|
||||||
|
12
cache/cache.go
vendored
12
cache/cache.go
vendored
@ -33,7 +33,7 @@ func removeIndex(s []string, i int) []string {
|
|||||||
return s[:len(s)-1]
|
return s[:len(s)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeUrl(url string) {
|
func removeURL(url string) {
|
||||||
for i := range urls {
|
for i := range urls {
|
||||||
if urls[i] == url {
|
if urls[i] == url {
|
||||||
urls = removeIndex(urls, i)
|
urls = removeIndex(urls, i)
|
||||||
@ -48,7 +48,7 @@ func removeUrl(url string) {
|
|||||||
// If your page is larger than the max cache size, the provided page
|
// If your page is larger than the max cache size, the provided page
|
||||||
// will silently not be added to the cache.
|
// will silently not be added to the cache.
|
||||||
func AddPage(p *structs.Page) {
|
func AddPage(p *structs.Page) {
|
||||||
if p.Url == "" || strings.HasPrefix(p.Url, "about:") {
|
if p.URL == "" || strings.HasPrefix(p.URL, "about:") {
|
||||||
// Just in case, these pages shouldn't be cached
|
// Just in case, these pages shouldn't be cached
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -71,10 +71,10 @@ func AddPage(p *structs.Page) {
|
|||||||
|
|
||||||
lock.Lock()
|
lock.Lock()
|
||||||
defer lock.Unlock()
|
defer lock.Unlock()
|
||||||
pages[p.Url] = p
|
pages[p.URL] = p
|
||||||
// Remove the URL if it was already there, then add it to the end
|
// Remove the URL if it was already there, then add it to the end
|
||||||
removeUrl(p.Url)
|
removeURL(p.URL)
|
||||||
urls = append(urls, p.Url)
|
urls = append(urls, p.URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemovePage will remove a page from the cache.
|
// RemovePage will remove a page from the cache.
|
||||||
@ -83,7 +83,7 @@ func RemovePage(url string) {
|
|||||||
lock.Lock()
|
lock.Lock()
|
||||||
defer lock.Unlock()
|
defer lock.Unlock()
|
||||||
delete(pages, url)
|
delete(pages, url)
|
||||||
removeUrl(url)
|
removeURL(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClearPages removes all pages from the cache.
|
// ClearPages removes all pages from the cache.
|
||||||
|
13
cache/cache_test.go
vendored
13
cache/cache_test.go
vendored
@ -7,9 +7,8 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
var p = structs.Page{Url: "example.com"}
|
var p = structs.Page{URL: "example.com"}
|
||||||
var p2 = structs.Page{Url: "example.org"}
|
var p2 = structs.Page{URL: "example.org"}
|
||||||
var queryPage = structs.Page{Url: "gemini://example.com/test?query"}
|
|
||||||
|
|
||||||
func reset() {
|
func reset() {
|
||||||
ClearPages()
|
ClearPages()
|
||||||
@ -33,13 +32,13 @@ func TestMaxSize(t *testing.T) {
|
|||||||
assert.Equal(1, NumPages(), "one page should be added")
|
assert.Equal(1, NumPages(), "one page should be added")
|
||||||
AddPage(&p2)
|
AddPage(&p2)
|
||||||
assert.Equal(1, NumPages(), "there should still be just one page due to cache size limits")
|
assert.Equal(1, NumPages(), "there should still be just one page due to cache size limits")
|
||||||
assert.Equal(p2.Url, urls[0], "the only page url should be the second page one")
|
assert.Equal(p2.URL, urls[0], "the only page url should be the second page one")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRemove(t *testing.T) {
|
func TestRemove(t *testing.T) {
|
||||||
reset()
|
reset()
|
||||||
AddPage(&p)
|
AddPage(&p)
|
||||||
RemovePage(p.Url)
|
RemovePage(p.URL)
|
||||||
assert.Equal(t, 0, NumPages(), "there shouldn't be any pages after the removal")
|
assert.Equal(t, 0, NumPages(), "there shouldn't be any pages after the removal")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,11 +61,11 @@ func TestGet(t *testing.T) {
|
|||||||
reset()
|
reset()
|
||||||
AddPage(&p)
|
AddPage(&p)
|
||||||
AddPage(&p2)
|
AddPage(&p2)
|
||||||
page, ok := GetPage(p.Url)
|
page, ok := GetPage(p.URL)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("Get should say that the page was found")
|
t.Fatal("Get should say that the page was found")
|
||||||
}
|
}
|
||||||
if page.Url != p.Url {
|
if page.URL != p.URL {
|
||||||
t.Error("page urls don't match")
|
t.Error("page urls don't match")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,12 +5,20 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/makeworld-the-better-one/go-gemini"
|
"github.com/makeworld-the-better-one/go-gemini"
|
||||||
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Fetch returns response data and an error.
|
// Fetch returns response data and an error.
|
||||||
// The error text is human friendly and should be displayed.
|
// The error text is human friendly and should be displayed.
|
||||||
func Fetch(u string) (*gemini.Response, error) {
|
func Fetch(u string) (*gemini.Response, error) {
|
||||||
res, err := gemini.Fetch(u)
|
var res *gemini.Response
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if viper.GetString("a-general.proxy") == "" {
|
||||||
|
res, err = gemini.Fetch(u)
|
||||||
|
} else {
|
||||||
|
res, err = gemini.FetchWithHost(viper.GetString("a-general.proxy"), u)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -39,19 +39,20 @@ func expiryKey(domain string, port string) string {
|
|||||||
|
|
||||||
func loadTofuEntry(domain string, port string) (string, time.Time, error) {
|
func loadTofuEntry(domain string, port string) (string, time.Time, error) {
|
||||||
id := tofuStore.GetString(idKey(domain, port)) // Fingerprint
|
id := tofuStore.GetString(idKey(domain, port)) // Fingerprint
|
||||||
if len(id) != 64 {
|
if len(id) != sha256.Size*2 {
|
||||||
// Not set, or invalid
|
// Not set, or invalid
|
||||||
return "", time.Time{}, errors.New("not found")
|
return "", time.Time{}, errors.New("not found") //nolint:goerr113
|
||||||
}
|
}
|
||||||
|
|
||||||
expiry := tofuStore.GetTime(expiryKey(domain, port))
|
expiry := tofuStore.GetTime(expiryKey(domain, port))
|
||||||
if expiry.IsZero() {
|
if expiry.IsZero() {
|
||||||
// Not set
|
// Not set
|
||||||
return id, time.Time{}, errors.New("not found")
|
return id, time.Time{}, errors.New("not found") //nolint:goerr113
|
||||||
}
|
}
|
||||||
return id, expiry, nil
|
return id, expiry, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint:errcheck
|
||||||
// certID returns a generic string representing a cert or domain.
|
// certID returns a generic string representing a cert or domain.
|
||||||
func certID(cert *x509.Certificate) string {
|
func certID(cert *x509.Certificate) string {
|
||||||
h := sha256.New()
|
h := sha256.New()
|
||||||
@ -62,14 +63,14 @@ func certID(cert *x509.Certificate) string {
|
|||||||
// origCertID uses cert.Raw, which was used in v1.0.0 of the app.
|
// origCertID uses cert.Raw, which was used in v1.0.0 of the app.
|
||||||
func origCertID(cert *x509.Certificate) string {
|
func origCertID(cert *x509.Certificate) string {
|
||||||
h := sha256.New()
|
h := sha256.New()
|
||||||
h.Write(cert.Raw)
|
h.Write(cert.Raw) //nolint:errcheck
|
||||||
return fmt.Sprintf("%X", h.Sum(nil))
|
return fmt.Sprintf("%X", h.Sum(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveTofuEntry(domain, port string, cert *x509.Certificate) {
|
func saveTofuEntry(domain, port string, cert *x509.Certificate) {
|
||||||
tofuStore.Set(idKey(domain, port), certID(cert))
|
tofuStore.Set(idKey(domain, port), certID(cert))
|
||||||
tofuStore.Set(expiryKey(domain, port), cert.NotAfter.UTC())
|
tofuStore.Set(expiryKey(domain, port), cert.NotAfter.UTC())
|
||||||
tofuStore.WriteConfig()
|
tofuStore.WriteConfig() //nolint:errcheck // Not an issue if it's not saved, only cached data
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleTofu is the abstracted interface for taking care of TOFU.
|
// handleTofu is the abstracted interface for taking care of TOFU.
|
||||||
@ -90,7 +91,7 @@ func handleTofu(domain, port string, cert *x509.Certificate) bool {
|
|||||||
|
|
||||||
// Store expiry again in case it changed
|
// Store expiry again in case it changed
|
||||||
tofuStore.Set(expiryKey(domain, port), cert.NotAfter.UTC())
|
tofuStore.Set(expiryKey(domain, port), cert.NotAfter.UTC())
|
||||||
tofuStore.WriteConfig()
|
tofuStore.WriteConfig() //nolint:errcheck
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -27,11 +27,11 @@ var tofuDBDir string
|
|||||||
var tofuDBPath string
|
var tofuDBPath string
|
||||||
|
|
||||||
// Bookmarks
|
// Bookmarks
|
||||||
|
|
||||||
var BkmkStore = viper.New()
|
var BkmkStore = viper.New()
|
||||||
var bkmkDir string
|
var bkmkDir string
|
||||||
var bkmkPath string
|
var bkmkPath string
|
||||||
|
|
||||||
// For other pkgs to use
|
|
||||||
var DownloadsDir string
|
var DownloadsDir string
|
||||||
|
|
||||||
// Feeds
|
// Feeds
|
||||||
@ -39,6 +39,7 @@ var FeedJson io.ReadCloser
|
|||||||
var feedDir string
|
var feedDir string
|
||||||
var FeedPath string
|
var FeedPath string
|
||||||
|
|
||||||
|
//nolint:golint,goerr113
|
||||||
func Init() error {
|
func Init() error {
|
||||||
|
|
||||||
// *** Set paths ***
|
// *** Set paths ***
|
||||||
@ -48,7 +49,7 @@ func Init() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Store AppData path
|
// Store AppData path
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" { //nolint:goconst
|
||||||
appdata, ok := os.LookupEnv("APPDATA")
|
appdata, ok := os.LookupEnv("APPDATA")
|
||||||
if ok {
|
if ok {
|
||||||
amforaAppData = filepath.Join(appdata, "amfora")
|
amforaAppData = filepath.Join(appdata, "amfora")
|
||||||
@ -220,6 +221,7 @@ func Init() error {
|
|||||||
// Setup main config
|
// Setup main config
|
||||||
|
|
||||||
viper.SetDefault("a-general.home", "gemini.circumlunar.space")
|
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.http", "default")
|
||||||
viper.SetDefault("a-general.search", "gus.guru/search")
|
viper.SetDefault("a-general.search", "gus.guru/search")
|
||||||
viper.SetDefault("a-general.color", true)
|
viper.SetDefault("a-general.color", true)
|
||||||
@ -231,6 +233,7 @@ func Init() error {
|
|||||||
viper.SetDefault("a-general.page_max_time", 10)
|
viper.SetDefault("a-general.page_max_time", 10)
|
||||||
viper.SetDefault("a-general.emoji_favicons", false)
|
viper.SetDefault("a-general.emoji_favicons", false)
|
||||||
viper.SetDefault("keybindings.shift_numbers", "!@#$%^&*()")
|
viper.SetDefault("keybindings.shift_numbers", "!@#$%^&*()")
|
||||||
|
viper.SetDefault("url-handlers.other", "off")
|
||||||
viper.SetDefault("cache.max_size", 0)
|
viper.SetDefault("cache.max_size", 0)
|
||||||
viper.SetDefault("cache.max_pages", 20)
|
viper.SetDefault("cache.max_pages", 20)
|
||||||
|
|
||||||
|
@ -16,6 +16,11 @@ var defaultConf = []byte(`# This is the default config file.
|
|||||||
# Press Ctrl-H to access it
|
# Press Ctrl-H to access it
|
||||||
home = "gemini://gemini.circumlunar.space"
|
home = "gemini://gemini.circumlunar.space"
|
||||||
|
|
||||||
|
# Follow up to 5 Gemini redirects without prompting.
|
||||||
|
# A prompt is always shown after the 5th redirect and for redirects to protocols other than Gemini.
|
||||||
|
# If set to false, a prompt will be shown before following redirects.
|
||||||
|
auto_redirect = false
|
||||||
|
|
||||||
# What command to run to open a HTTP URL. Set to "default" to try to guess the browser,
|
# What command to run to open a HTTP URL. Set to "default" to try to guess the browser,
|
||||||
# or set to "off" to not open HTTP URLs.
|
# or set to "off" to not open HTTP URLs.
|
||||||
# If a command is set, than the URL will be added (in quotes) to the end of the command.
|
# If a command is set, than the URL will be added (in quotes) to the end of the command.
|
||||||
@ -50,6 +55,13 @@ page_max_time = 10
|
|||||||
# Whether to replace tab numbers with emoji favicons, which are cached.
|
# Whether to replace tab numbers with emoji favicons, which are cached.
|
||||||
emoji_favicons = false
|
emoji_favicons = false
|
||||||
|
|
||||||
|
# Proxy server, through which all requests would be sent.
|
||||||
|
# String should be a host: a domain/IP with an optional port. Port 1965 is assumed otherwise.
|
||||||
|
# The proxy server needs to be a Gemini server that supports proxying.
|
||||||
|
# By default it is empty, which disables the proxy.
|
||||||
|
proxy = ""
|
||||||
|
|
||||||
|
|
||||||
[keybindings]
|
[keybindings]
|
||||||
# In the future there will be more settings here.
|
# In the future there will be more settings here.
|
||||||
|
|
||||||
@ -58,6 +70,20 @@ emoji_favicons = false
|
|||||||
shift_numbers = "!@#$%^&*()"
|
shift_numbers = "!@#$%^&*()"
|
||||||
|
|
||||||
|
|
||||||
|
[url-handlers]
|
||||||
|
# Allows setting the commands to run for various URL schemes.
|
||||||
|
# E.g. to open FTP URLs with FileZilla set the following key:
|
||||||
|
# ftp = "filezilla"
|
||||||
|
# You can set any scheme to "off" to disable handling it.
|
||||||
|
#
|
||||||
|
# DO NOT use this for setting the HTTP command.
|
||||||
|
# Use the http setting in the "a-general" section above
|
||||||
|
|
||||||
|
# This is a special key that defines the handler for all URL schemes for which
|
||||||
|
# no handler is defined.
|
||||||
|
other = "off"
|
||||||
|
|
||||||
|
|
||||||
[cache]
|
[cache]
|
||||||
# Options for page cache - which is only for text/gemini pages
|
# Options for page cache - which is only for text/gemini pages
|
||||||
# Increase the cache size to speed up browsing at the expense of memory
|
# Increase the cache size to speed up browsing at the expense of memory
|
||||||
@ -135,4 +161,5 @@ max_pages = 30 # The maximum number of pages the cache will store
|
|||||||
# bkmk_modal_text
|
# bkmk_modal_text
|
||||||
# bkmk_modal_label
|
# bkmk_modal_label
|
||||||
# bkmk_modal_field_bg
|
# bkmk_modal_field_bg
|
||||||
# bkmk_modal_field_text`)
|
# bkmk_modal_field_text
|
||||||
|
`)
|
||||||
|
@ -20,5 +20,5 @@ func KeyToNum(key rune) (int, error) {
|
|||||||
return i + 1, nil
|
return i + 1, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return -1, errors.New("provided key is invalid")
|
return -1, errors.New("provided key is invalid") //nolint:goerr113
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,11 @@
|
|||||||
# Press Ctrl-H to access it
|
# Press Ctrl-H to access it
|
||||||
home = "gemini://gemini.circumlunar.space"
|
home = "gemini://gemini.circumlunar.space"
|
||||||
|
|
||||||
|
# Follow up to 5 Gemini redirects without prompting.
|
||||||
|
# A prompt is always shown after the 5th redirect and for redirects to protocols other than Gemini.
|
||||||
|
# If set to false, a prompt will be shown before following redirects.
|
||||||
|
auto_redirect = false
|
||||||
|
|
||||||
# What command to run to open a HTTP URL. Set to "default" to try to guess the browser,
|
# What command to run to open a HTTP URL. Set to "default" to try to guess the browser,
|
||||||
# or set to "off" to not open HTTP URLs.
|
# or set to "off" to not open HTTP URLs.
|
||||||
# If a command is set, than the URL will be added (in quotes) to the end of the command.
|
# If a command is set, than the URL will be added (in quotes) to the end of the command.
|
||||||
@ -47,6 +52,13 @@ page_max_time = 10
|
|||||||
# Whether to replace tab numbers with emoji favicons, which are cached.
|
# Whether to replace tab numbers with emoji favicons, which are cached.
|
||||||
emoji_favicons = false
|
emoji_favicons = false
|
||||||
|
|
||||||
|
# Proxy server, through which all requests would be sent.
|
||||||
|
# String should be a host: a domain/IP with an optional port. Port 1965 is assumed otherwise.
|
||||||
|
# The proxy server needs to be a Gemini server that supports proxying.
|
||||||
|
# By default it is empty, which disables the proxy.
|
||||||
|
proxy = ""
|
||||||
|
|
||||||
|
|
||||||
[keybindings]
|
[keybindings]
|
||||||
# In the future there will be more settings here.
|
# In the future there will be more settings here.
|
||||||
|
|
||||||
@ -55,6 +67,20 @@ emoji_favicons = false
|
|||||||
shift_numbers = "!@#$%^&*()"
|
shift_numbers = "!@#$%^&*()"
|
||||||
|
|
||||||
|
|
||||||
|
[url-handlers]
|
||||||
|
# Allows setting the commands to run for various URL schemes.
|
||||||
|
# E.g. to open FTP URLs with FileZilla set the following key:
|
||||||
|
# ftp = "filezilla"
|
||||||
|
# You can set any scheme to "off" to disable handling it.
|
||||||
|
#
|
||||||
|
# DO NOT use this for setting the HTTP command.
|
||||||
|
# Use the http setting in the "a-general" section above
|
||||||
|
|
||||||
|
# This is a special key that defines the handler for all URL schemes for which
|
||||||
|
# no handler is defined.
|
||||||
|
other = "off"
|
||||||
|
|
||||||
|
|
||||||
[cache]
|
[cache]
|
||||||
# Options for page cache - which is only for text/gemini pages
|
# Options for page cache - which is only for text/gemini pages
|
||||||
# Increase the cache size to speed up browsing at the expense of memory
|
# Increase the cache size to speed up browsing at the expense of memory
|
||||||
@ -132,4 +158,4 @@ max_pages = 30 # The maximum number of pages the cache will store
|
|||||||
# bkmk_modal_text
|
# bkmk_modal_text
|
||||||
# bkmk_modal_label
|
# bkmk_modal_label
|
||||||
# bkmk_modal_field_bg
|
# bkmk_modal_field_bg
|
||||||
# bkmk_modal_field_text
|
# bkmk_modal_field_text
|
||||||
|
@ -121,7 +121,7 @@ func Bookmarks(t *tab) {
|
|||||||
Raw: bkmkPageRaw,
|
Raw: bkmkPageRaw,
|
||||||
Content: content,
|
Content: content,
|
||||||
Links: links,
|
Links: links,
|
||||||
Url: "about:bookmarks",
|
URL: "about:bookmarks",
|
||||||
Width: termW,
|
Width: termW,
|
||||||
Mediatype: structs.TextGemini,
|
Mediatype: structs.TextGemini,
|
||||||
}
|
}
|
||||||
@ -133,20 +133,20 @@ func Bookmarks(t *tab) {
|
|||||||
// It is the high-level way of doing it. It should be called in a goroutine.
|
// It is the high-level way of doing it. It should be called in a goroutine.
|
||||||
// It can also be called to edit an existing bookmark.
|
// It can also be called to edit an existing bookmark.
|
||||||
func addBookmark() {
|
func addBookmark() {
|
||||||
if !strings.HasPrefix(tabs[curTab].page.Url, "gemini://") {
|
if !strings.HasPrefix(tabs[curTab].page.URL, "gemini://") {
|
||||||
// Can't make bookmarks for other kinds of URLs
|
// Can't make bookmarks for other kinds of URLs
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
name, exists := bookmarks.Get(tabs[curTab].page.Url)
|
name, exists := bookmarks.Get(tabs[curTab].page.URL)
|
||||||
// Open a bookmark modal with the current name of the bookmark, if it exists
|
// Open a bookmark modal with the current name of the bookmark, if it exists
|
||||||
newName, action := openBkmkModal(name, exists)
|
newName, action := openBkmkModal(name, exists)
|
||||||
switch action {
|
switch action {
|
||||||
case 1:
|
case 1:
|
||||||
// Add/change the bookmark
|
// Add/change the bookmark
|
||||||
bookmarks.Set(tabs[curTab].page.Url, newName)
|
bookmarks.Set(tabs[curTab].page.URL, newName)
|
||||||
case -1:
|
case -1:
|
||||||
bookmarks.Remove(tabs[curTab].page.Url)
|
bookmarks.Remove(tabs[curTab].page.URL)
|
||||||
}
|
}
|
||||||
// Other case is action = 0, meaning "Cancel", so nothing needs to happen
|
// Other case is action = 0, meaning "Cancel", so nothing needs to happen
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ package display
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -12,6 +11,7 @@ import (
|
|||||||
"github.com/makeworld-the-better-one/amfora/config"
|
"github.com/makeworld-the-better-one/amfora/config"
|
||||||
"github.com/makeworld-the-better-one/amfora/renderer"
|
"github.com/makeworld-the-better-one/amfora/renderer"
|
||||||
"github.com/makeworld-the-better-one/amfora/structs"
|
"github.com/makeworld-the-better-one/amfora/structs"
|
||||||
|
"github.com/makeworld-the-better-one/go-gemini"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"gitlab.com/tslocum/cview"
|
"gitlab.com/tslocum/cview"
|
||||||
)
|
)
|
||||||
@ -115,6 +115,7 @@ func Init() {
|
|||||||
App.SetFocus(tabs[tab].view)
|
App.SetFocus(tabs[tab].view)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint:exhaustive
|
||||||
switch key {
|
switch key {
|
||||||
case tcell.KeyEnter:
|
case tcell.KeyEnter:
|
||||||
// Figure out whether it's a URL, link number, or search
|
// Figure out whether it's a URL, link number, or search
|
||||||
@ -127,27 +128,19 @@ func Init() {
|
|||||||
reset()
|
reset()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if query == ".." && tabs[tab].hasContent() {
|
if query[0] == '.' && tabs[tab].hasContent() {
|
||||||
// Go up a directory
|
// Relative url
|
||||||
parsed, err := url.Parse(tabs[tab].page.Url)
|
current, err := url.Parse(tabs[tab].page.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// This shouldn't occur
|
// This shouldn't occur
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if parsed.Path == "/" {
|
target, err := current.Parse(query)
|
||||||
// Can't go up further
|
if err != nil {
|
||||||
reset()
|
// Invalid relative url
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
URL(target.String())
|
||||||
// Ex: /test/foo/ -> /test/foo//.. -> /test -> /test/
|
|
||||||
parsed.Path = path.Clean(parsed.Path+"/..") + "/"
|
|
||||||
if parsed.Path == "//" {
|
|
||||||
// Fix double slash that occurs at domain root
|
|
||||||
parsed.Path = "/"
|
|
||||||
}
|
|
||||||
parsed.RawQuery = "" // Remove query
|
|
||||||
URL(parsed.String())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,7 +158,7 @@ func Init() {
|
|||||||
oldTab := tab
|
oldTab := tab
|
||||||
NewTab()
|
NewTab()
|
||||||
// Resolve and follow link manually
|
// Resolve and follow link manually
|
||||||
prevParsed, _ := url.Parse(tabs[oldTab].page.Url)
|
prevParsed, _ := url.Parse(tabs[oldTab].page.URL)
|
||||||
nextParsed, err := url.Parse(tabs[oldTab].page.Links[i-1])
|
nextParsed, err := url.Parse(tabs[oldTab].page.Links[i-1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error("URL Error", "link URL could not be parsed")
|
Error("URL Error", "link URL could not be parsed")
|
||||||
@ -178,8 +171,9 @@ func Init() {
|
|||||||
} else {
|
} else {
|
||||||
// It's a full URL or search term
|
// It's a full URL or search term
|
||||||
// Detect if it's a search or URL
|
// 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, " ") ||
|
||||||
u := viper.GetString("a-general.search") + "?" + queryEscape(query)
|
(!strings.Contains(query, "//") && !strings.Contains(query, ".") && !strings.HasPrefix(query, "about:")) {
|
||||||
|
u := viper.GetString("a-general.search") + "?" + gemini.QueryEscape(query)
|
||||||
cache.RemovePage(u) // Don't use the cached version of the search
|
cache.RemovePage(u) // Don't use the cached version of the search
|
||||||
URL(u)
|
URL(u)
|
||||||
} else {
|
} else {
|
||||||
@ -192,7 +186,7 @@ func Init() {
|
|||||||
}
|
}
|
||||||
if i <= len(tabs[tab].page.Links) && i > 0 {
|
if i <= len(tabs[tab].page.Links) && i > 0 {
|
||||||
// It's a valid link number
|
// It's a valid link number
|
||||||
followLink(tabs[tab], tabs[tab].page.Url, tabs[tab].page.Links[i-1])
|
followLink(tabs[tab], tabs[tab].page.URL, tabs[tab].page.Links[i-1])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Invalid link number, don't do anything
|
// Invalid link number, don't do anything
|
||||||
@ -213,7 +207,7 @@ func Init() {
|
|||||||
Raw: newTabContent,
|
Raw: newTabContent,
|
||||||
Content: renderedNewTabContent,
|
Content: renderedNewTabContent,
|
||||||
Links: newTabLinks,
|
Links: newTabLinks,
|
||||||
Url: "about:newtab",
|
URL: "about:newtab",
|
||||||
Width: -1, // Force reformatting on first display
|
Width: -1, // Force reformatting on first display
|
||||||
Mediatype: structs.TextGemini,
|
Mediatype: structs.TextGemini,
|
||||||
}
|
}
|
||||||
@ -254,6 +248,7 @@ func Init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint:exhaustive
|
||||||
switch event.Key() {
|
switch event.Key() {
|
||||||
case tcell.KeyCtrlR:
|
case tcell.KeyCtrlR:
|
||||||
Reload()
|
Reload()
|
||||||
@ -321,18 +316,20 @@ func Init() {
|
|||||||
}
|
}
|
||||||
if i <= len(tabs[curTab].page.Links) && i > 0 {
|
if i <= len(tabs[curTab].page.Links) && i > 0 {
|
||||||
// It's a valid link number
|
// It's a valid link number
|
||||||
followLink(tabs[curTab], tabs[curTab].page.Url, tabs[curTab].page.Links[i-1])
|
followLink(tabs[curTab], tabs[curTab].page.URL, tabs[curTab].page.Links[i-1])
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// All the keys and operations that can work while a tab IS loading
|
// All the keys and operations that can work while a tab IS loading
|
||||||
|
|
||||||
|
//nolint:exhaustive
|
||||||
switch event.Key() {
|
switch event.Key() {
|
||||||
case tcell.KeyCtrlT:
|
case tcell.KeyCtrlT:
|
||||||
if tabs[curTab].page.Mode == structs.ModeLinkSelect {
|
if tabs[curTab].page.Mode == structs.ModeLinkSelect {
|
||||||
next, err := resolveRelLink(tabs[curTab], tabs[curTab].page.Url, tabs[curTab].page.Selected)
|
next, err := resolveRelLink(tabs[curTab], tabs[curTab].page.URL, tabs[curTab].page.Selected)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error("URL Error", err.Error())
|
Error("URL Error", err.Error())
|
||||||
return nil
|
return nil
|
||||||
@ -521,11 +518,11 @@ func Reload() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
parsed, _ := url.Parse(tabs[curTab].page.Url)
|
parsed, _ := url.Parse(tabs[curTab].page.URL)
|
||||||
go func(t *tab) {
|
go func(t *tab) {
|
||||||
cache.RemovePage(tabs[curTab].page.Url)
|
cache.RemovePage(tabs[curTab].page.URL)
|
||||||
cache.RemoveFavicon(parsed.Host)
|
cache.RemoveFavicon(parsed.Host)
|
||||||
handleURL(t, t.page.Url) // goURL is not used bc history shouldn't be added to
|
handleURL(t, t.page.URL, 0) // goURL is not used bc history shouldn't be added to
|
||||||
if t == tabs[curTab] {
|
if t == tabs[curTab] {
|
||||||
// Display the bottomBar state that handleURL set
|
// Display the bottomBar state that handleURL set
|
||||||
t.applyBottomBar()
|
t.applyBottomBar()
|
||||||
@ -538,7 +535,7 @@ func Reload() {
|
|||||||
func URL(u string) {
|
func URL(u string) {
|
||||||
// Some code is copied in followLink()
|
// Some code is copied in followLink()
|
||||||
|
|
||||||
if u == "about:bookmarks" {
|
if u == "about:bookmarks" { //nolint:goconst
|
||||||
Bookmarks(tabs[curTab])
|
Bookmarks(tabs[curTab])
|
||||||
tabs[curTab].addToHistory("about:bookmarks")
|
tabs[curTab].addToHistory("about:bookmarks")
|
||||||
return
|
return
|
||||||
@ -553,6 +550,10 @@ func URL(u string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(u, "//") && !strings.HasPrefix(u, "gemini://") && !strings.Contains(u, "://") {
|
||||||
|
// Assume it's a Gemini URL
|
||||||
|
u = "gemini://" + u
|
||||||
|
}
|
||||||
go goURL(tabs[curTab], u)
|
go goURL(tabs[curTab], u)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,7 +142,7 @@ func downloadURL(u string, resp *gemini.Response) {
|
|||||||
progressbar.OptionShowCount(),
|
progressbar.OptionShowCount(),
|
||||||
progressbar.OptionSpinnerType(14),
|
progressbar.OptionSpinnerType(14),
|
||||||
)
|
)
|
||||||
bar.RenderBlank()
|
bar.RenderBlank() //nolint:errcheck
|
||||||
|
|
||||||
savePath, err := downloadNameFromURL(u, "")
|
savePath, err := downloadNameFromURL(u, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -200,9 +200,9 @@ func downloadPage(p *structs.Page) (string, error) {
|
|||||||
var err error
|
var err error
|
||||||
|
|
||||||
if p.Mediatype == structs.TextGemini {
|
if p.Mediatype == structs.TextGemini {
|
||||||
savePath, err = downloadNameFromURL(p.Url, ".gmi")
|
savePath, err = downloadNameFromURL(p.URL, ".gmi")
|
||||||
} else {
|
} else {
|
||||||
savePath, err = downloadNameFromURL(p.Url, ".txt")
|
savePath, err = downloadNameFromURL(p.URL, ".txt")
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@ -261,13 +261,12 @@ func getSafeDownloadName(name string, lastDot bool, n int) (string, error) {
|
|||||||
if lastDot {
|
if lastDot {
|
||||||
ext := filepath.Ext(name)
|
ext := filepath.Ext(name)
|
||||||
return strings.TrimSuffix(name, ext) + "(" + strconv.Itoa(n) + ")" + ext
|
return strings.TrimSuffix(name, ext) + "(" + strconv.Itoa(n) + ")" + ext
|
||||||
} else {
|
|
||||||
idx := strings.Index(name, ".")
|
|
||||||
if idx == -1 {
|
|
||||||
return name + "(" + strconv.Itoa(n) + ")"
|
|
||||||
}
|
|
||||||
return name[:idx] + "(" + strconv.Itoa(n) + ")" + name[idx:]
|
|
||||||
}
|
}
|
||||||
|
idx := strings.Index(name, ".")
|
||||||
|
if idx == -1 {
|
||||||
|
return name + "(" + strconv.Itoa(n) + ")"
|
||||||
|
}
|
||||||
|
return name[:idx] + "(" + strconv.Itoa(n) + ")" + name[idx:]
|
||||||
}
|
}
|
||||||
|
|
||||||
d, err := os.Open(config.DownloadsDir)
|
d, err := os.Open(config.DownloadsDir)
|
||||||
|
@ -38,7 +38,7 @@ func Feeds(t *tab) {
|
|||||||
Raw: feedPageRaw,
|
Raw: feedPageRaw,
|
||||||
Content: content,
|
Content: content,
|
||||||
Links: links,
|
Links: links,
|
||||||
Url: "about:feeds",
|
URL: "about:feeds",
|
||||||
Width: termW,
|
Width: termW,
|
||||||
Mediatype: structs.TextGemini,
|
Mediatype: structs.TextGemini,
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ package display
|
|||||||
|
|
||||||
// applyHist is a history.go internal function, to load a URL in the history.
|
// applyHist is a history.go internal function, to load a URL in the history.
|
||||||
func applyHist(t *tab) {
|
func applyHist(t *tab) {
|
||||||
handleURL(t, t.history.urls[t.history.pos]) // Load that position in history
|
handleURL(t, t.history.urls[t.history.pos], 0) // Load that position in history
|
||||||
t.applyAll()
|
t.applyAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
humanize "github.com/dustin/go-humanize"
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
"github.com/makeworld-the-better-one/amfora/config"
|
"github.com/makeworld-the-better-one/amfora/config"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
@ -154,10 +154,13 @@ func modalInit() {
|
|||||||
|
|
||||||
// Error displays an error on the screen in a modal.
|
// Error displays an error on the screen in a modal.
|
||||||
func Error(title, text string) {
|
func Error(title, text string) {
|
||||||
// Capitalize and add period if necessary - because most errors don't do that
|
if text == "" {
|
||||||
text = strings.ToUpper(string([]rune(text)[0])) + text[1:]
|
text = "No additional information."
|
||||||
if !strings.HasSuffix(text, ".") && !strings.HasSuffix(text, "!") && !strings.HasSuffix(text, "?") {
|
} else {
|
||||||
text += "."
|
text = strings.ToUpper(string([]rune(text)[0])) + text[1:]
|
||||||
|
if !strings.HasSuffix(text, ".") && !strings.HasSuffix(text, "!") && !strings.HasSuffix(text, "?") {
|
||||||
|
text += "."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Add spaces to title for aesthetic reasons
|
// Add spaces to title for aesthetic reasons
|
||||||
title = " " + strings.TrimSpace(title) + " "
|
title = " " + strings.TrimSpace(title) + " "
|
||||||
@ -265,6 +268,7 @@ func Tofu(host string, expiry time.Time) bool {
|
|||||||
}
|
}
|
||||||
yesNoModal.GetFrame().SetTitle(" TOFU ")
|
yesNoModal.GetFrame().SetTitle(" TOFU ")
|
||||||
yesNoModal.SetText(
|
yesNoModal.SetText(
|
||||||
|
//nolint:lll
|
||||||
fmt.Sprintf("%s's certificate has changed, possibly indicating an security issue. The certificate would have expired %s. Are you sure you want to continue? ",
|
fmt.Sprintf("%s's certificate has changed, possibly indicating an security issue. The certificate would have expired %s. Are you sure you want to continue? ",
|
||||||
host,
|
host,
|
||||||
humanize.Time(expiry),
|
humanize.Time(expiry),
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
//nolint
|
||||||
package display
|
package display
|
||||||
|
|
||||||
var newTabContent = `# New Tab
|
var newTabContent = `# New Tab
|
||||||
|
@ -2,6 +2,7 @@ package display
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -70,15 +71,18 @@ func reformatPage(p *structs.Page) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Setup a renderer.RenderFromMediatype func so this isn't needed
|
||||||
|
|
||||||
var rendered string
|
var rendered string
|
||||||
if p.Mediatype == structs.TextGemini {
|
switch p.Mediatype {
|
||||||
|
case structs.TextGemini:
|
||||||
// Links are not recorded because they won't change
|
// Links are not recorded because they won't change
|
||||||
rendered, _ = renderer.RenderGemini(p.Raw, textWidth(), leftMargin())
|
rendered, _ = renderer.RenderGemini(p.Raw, textWidth(), leftMargin())
|
||||||
} else if p.Mediatype == structs.TextPlain {
|
case structs.TextPlain:
|
||||||
rendered = renderer.RenderPlainText(p.Raw, leftMargin())
|
rendered = renderer.RenderPlainText(p.Raw, leftMargin())
|
||||||
} else if p.Mediatype == structs.TextAnsi {
|
case structs.TextAnsi:
|
||||||
rendered = renderer.RenderANSI(p.Raw, leftMargin())
|
rendered = renderer.RenderANSI(p.Raw, leftMargin())
|
||||||
} else {
|
default:
|
||||||
// Rendering this type is not implemented
|
// Rendering this type is not implemented
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -117,7 +121,7 @@ func setPage(t *tab, p *structs.Page) {
|
|||||||
t.page = p
|
t.page = p
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
parsed, _ := url.Parse(p.Url)
|
parsed, _ := url.Parse(p.URL)
|
||||||
handleFavicon(t, parsed.Host, oldFav)
|
handleFavicon(t, parsed.Host, oldFav)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -131,7 +135,7 @@ func setPage(t *tab, p *structs.Page) {
|
|||||||
|
|
||||||
// Save bottom bar for the tab - other funcs will apply/display it
|
// Save bottom bar for the tab - other funcs will apply/display it
|
||||||
t.barLabel = ""
|
t.barLabel = ""
|
||||||
t.barText = p.Url
|
t.barText = p.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleHTTP is used by handleURL.
|
// handleHTTP is used by handleURL.
|
||||||
@ -158,6 +162,30 @@ func handleHTTP(u string, showInfo bool) {
|
|||||||
App.Draw()
|
App.Draw()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleOther is used by handleURL.
|
||||||
|
// It opens links other than Gemini and HTTP and displays Error modals.
|
||||||
|
func handleOther(u string) {
|
||||||
|
// The URL should have a scheme due to a previous call to normalizeURL
|
||||||
|
parsed, _ := url.Parse(u)
|
||||||
|
// Search for a handler for the URL scheme
|
||||||
|
handler := strings.TrimSpace(viper.GetString("url-handlers." + parsed.Scheme))
|
||||||
|
if len(handler) == 0 {
|
||||||
|
handler = strings.TrimSpace(viper.GetString("url-handlers.other"))
|
||||||
|
}
|
||||||
|
switch handler {
|
||||||
|
case "", "off":
|
||||||
|
Error("URL Error", "Opening "+parsed.Scheme+" URLs is turned off.")
|
||||||
|
default:
|
||||||
|
// The config has a custom command to execute for URLs
|
||||||
|
fields := strings.Fields(handler)
|
||||||
|
err := exec.Command(fields[0], append(fields[1:], u)...).Start()
|
||||||
|
if err != nil {
|
||||||
|
Error("URL Error", "Error executing custom command: "+err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
App.Draw()
|
||||||
|
}
|
||||||
|
|
||||||
// handleFavicon handles getting and displaying a favicon.
|
// handleFavicon handles getting and displaying a favicon.
|
||||||
// `old` is the previous favicon for the tab.
|
// `old` is the previous favicon for the tab.
|
||||||
func handleFavicon(t *tab, host, old string) {
|
func handleFavicon(t *tab, host, old string) {
|
||||||
@ -239,7 +267,7 @@ func handleFavicon(t *tab, host, old string) {
|
|||||||
//
|
//
|
||||||
// It should be called in a goroutine.
|
// It should be called in a goroutine.
|
||||||
func goURL(t *tab, u string) {
|
func goURL(t *tab, u string) {
|
||||||
final, displayed := handleURL(t, u)
|
final, displayed := handleURL(t, u, 0)
|
||||||
if displayed {
|
if displayed {
|
||||||
t.addToHistory(final)
|
t.addToHistory(final)
|
||||||
}
|
}
|
||||||
@ -261,7 +289,10 @@ func goURL(t *tab, u string) {
|
|||||||
//
|
//
|
||||||
// The bottomBar is not actually changed in this func, except during loading.
|
// The bottomBar is not actually changed in this func, except during loading.
|
||||||
// The func that calls this one should apply the bottomBar values if necessary.
|
// The func that calls this one should apply the bottomBar values if necessary.
|
||||||
func handleURL(t *tab, u string) (string, bool) {
|
//
|
||||||
|
// numRedirects is the number of redirects that resulted in the provided URL.
|
||||||
|
// It should typically be 0.
|
||||||
|
func handleURL(t *tab, u string, numRedirects int) (string, bool) {
|
||||||
defer App.Draw() // Just in case
|
defer App.Draw() // Just in case
|
||||||
|
|
||||||
// Save for resetting on error
|
// Save for resetting on error
|
||||||
@ -304,7 +335,7 @@ func handleURL(t *tab, u string) (string, bool) {
|
|||||||
return ret("", false)
|
return ret("", false)
|
||||||
}
|
}
|
||||||
if !strings.HasPrefix(u, "gemini") {
|
if !strings.HasPrefix(u, "gemini") {
|
||||||
Error("Protocol Error", "Only gemini and HTTP are supported. URL was "+u)
|
handleOther(u)
|
||||||
return ret("", false)
|
return ret("", false)
|
||||||
}
|
}
|
||||||
// Gemini URL
|
// Gemini URL
|
||||||
@ -328,7 +359,7 @@ func handleURL(t *tab, u string) (string, bool) {
|
|||||||
return ret("", false)
|
return ret("", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == client.ErrTofu {
|
if errors.Is(err, client.ErrTofu) {
|
||||||
if Tofu(parsed.Host, client.GetExpiry(parsed.Hostname(), parsed.Port())) {
|
if Tofu(parsed.Host, client.GetExpiry(parsed.Hostname(), parsed.Port())) {
|
||||||
// They want to continue anyway
|
// They want to continue anyway
|
||||||
client.ResetTofuEntry(parsed.Hostname(), parsed.Port(), res.Cert)
|
client.ResetTofuEntry(parsed.Hostname(), parsed.Port(), res.Cert)
|
||||||
@ -348,20 +379,20 @@ func handleURL(t *tab, u string) (string, bool) {
|
|||||||
return ret("", false)
|
return ret("", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == renderer.ErrTooLarge {
|
if errors.Is(err, renderer.ErrTooLarge) {
|
||||||
// Make new request for downloading purposes
|
// Make new request for downloading purposes
|
||||||
res, clientErr := client.Fetch(u)
|
res, clientErr := client.Fetch(u)
|
||||||
if clientErr != nil && clientErr != client.ErrTofu {
|
if clientErr != nil && !errors.Is(clientErr, client.ErrTofu) {
|
||||||
Error("URL Fetch Error", err.Error())
|
Error("URL Fetch Error", err.Error())
|
||||||
return ret("", false)
|
return ret("", false)
|
||||||
}
|
}
|
||||||
go dlChoice("That page is too large. What would you like to do?", u, res)
|
go dlChoice("That page is too large. What would you like to do?", u, res)
|
||||||
return ret("", false)
|
return ret("", false)
|
||||||
}
|
}
|
||||||
if err == renderer.ErrTimedOut {
|
if errors.Is(err, renderer.ErrTimedOut) {
|
||||||
// Make new request for downloading purposes
|
// Make new request for downloading purposes
|
||||||
res, clientErr := client.Fetch(u)
|
res, clientErr := client.Fetch(u)
|
||||||
if clientErr != nil && clientErr != client.ErrTofu {
|
if clientErr != nil && !errors.Is(clientErr, client.ErrTofu) {
|
||||||
Error("URL Fetch Error", err.Error())
|
Error("URL Fetch Error", err.Error())
|
||||||
return ret("", false)
|
return ret("", false)
|
||||||
}
|
}
|
||||||
@ -388,13 +419,12 @@ func handleURL(t *tab, u string) (string, bool) {
|
|||||||
if ok {
|
if ok {
|
||||||
// Make another request with the query string added
|
// Make another request with the query string added
|
||||||
// + chars are replaced because PathEscape doesn't do that
|
// + chars are replaced because PathEscape doesn't do that
|
||||||
parsed.RawQuery = queryEscape(userInput)
|
parsed.RawQuery = gemini.QueryEscape(userInput)
|
||||||
if len(parsed.String()) > 1024 {
|
if len(parsed.String()) > gemini.URLMaxLength {
|
||||||
// 1024 is the max size for URLs in the spec
|
|
||||||
Error("Input Error", "URL for that input would be too long.")
|
Error("Input Error", "URL for that input would be too long.")
|
||||||
return ret("", false)
|
return ret("", false)
|
||||||
}
|
}
|
||||||
return ret(handleURL(t, parsed.String()))
|
return ret(handleURL(t, parsed.String(), 0))
|
||||||
}
|
}
|
||||||
return ret("", false)
|
return ret("", false)
|
||||||
case 30:
|
case 30:
|
||||||
@ -404,11 +434,22 @@ func handleURL(t *tab, u string) (string, bool) {
|
|||||||
return ret("", false)
|
return ret("", false)
|
||||||
}
|
}
|
||||||
redir := parsed.ResolveReference(parsedMeta).String()
|
redir := parsed.ResolveReference(parsedMeta).String()
|
||||||
if YesNo("Follow redirect?\n" + redir) {
|
// Prompt before redirecting to non-Gemini protocol
|
||||||
|
redirect := false
|
||||||
|
if !strings.HasPrefix(redir, "gemini") {
|
||||||
|
if YesNo("Follow redirect to non-Gemini URL?\n" + redir) {
|
||||||
|
redirect = true
|
||||||
|
} else {
|
||||||
|
return ret("", false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Prompt before redirecting
|
||||||
|
autoRedirect := viper.GetBool("a-general.auto_redirect")
|
||||||
|
if redirect || (autoRedirect && numRedirects < 5) || YesNo("Follow redirect?\n"+redir) {
|
||||||
if res.Status == gemini.StatusRedirectPermanent {
|
if res.Status == gemini.StatusRedirectPermanent {
|
||||||
go cache.AddRedir(u, redir)
|
go cache.AddRedir(u, redir)
|
||||||
}
|
}
|
||||||
return ret(handleURL(t, redir))
|
return ret(handleURL(t, redir, numRedirects+1))
|
||||||
}
|
}
|
||||||
return ret("", false)
|
return ret("", false)
|
||||||
case 40:
|
case 40:
|
||||||
|
@ -62,13 +62,13 @@ func makeNewTab() *tab {
|
|||||||
if key == tcell.KeyEsc {
|
if key == tcell.KeyEsc {
|
||||||
// Stop highlighting
|
// Stop highlighting
|
||||||
bottomBar.SetLabel("")
|
bottomBar.SetLabel("")
|
||||||
bottomBar.SetText(tabs[tab].page.Url)
|
bottomBar.SetText(tabs[tab].page.URL)
|
||||||
tabs[tab].clearSelected()
|
tabs[tab].clearSelected()
|
||||||
tabs[tab].saveBottomBar()
|
tabs[tab].saveBottomBar()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(tabs[tab].page.Links) <= 0 {
|
if len(tabs[tab].page.Links) == 0 {
|
||||||
// No links on page
|
// No links on page
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -82,10 +82,10 @@ func makeNewTab() *tab {
|
|||||||
linkN, _ := strconv.Atoi(currentSelection[0])
|
linkN, _ := strconv.Atoi(currentSelection[0])
|
||||||
tabs[tab].page.Selected = tabs[tab].page.Links[linkN]
|
tabs[tab].page.Selected = tabs[tab].page.Links[linkN]
|
||||||
tabs[tab].page.SelectedID = currentSelection[0]
|
tabs[tab].page.SelectedID = currentSelection[0]
|
||||||
followLink(tabs[tab], tabs[tab].page.Url, tabs[tab].page.Links[linkN])
|
followLink(tabs[tab], tabs[tab].page.URL, tabs[tab].page.Links[linkN])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(currentSelection) <= 0 && (key == tcell.KeyEnter || key == tcell.KeyTab) {
|
if len(currentSelection) == 0 && (key == tcell.KeyEnter || key == tcell.KeyTab) {
|
||||||
// They've started link highlighting
|
// They've started link highlighting
|
||||||
tabs[tab].page.Mode = structs.ModeLinkSelect
|
tabs[tab].page.Mode = structs.ModeLinkSelect
|
||||||
|
|
||||||
@ -102,7 +102,7 @@ func makeNewTab() *tab {
|
|||||||
// There's still a selection, but a different key was pressed, not Enter
|
// There's still a selection, but a different key was pressed, not Enter
|
||||||
|
|
||||||
index, _ := strconv.Atoi(currentSelection[0])
|
index, _ := strconv.Atoi(currentSelection[0])
|
||||||
if key == tcell.KeyTab {
|
if key == tcell.KeyTab { //nolint:gocritic
|
||||||
index = (index + 1) % numSelections
|
index = (index + 1) % numSelections
|
||||||
} else if key == tcell.KeyBacktab {
|
} else if key == tcell.KeyBacktab {
|
||||||
index = (index - 1 + numSelections) % numSelections
|
index = (index - 1 + numSelections) % numSelections
|
||||||
@ -153,10 +153,10 @@ func (t *tab) hasContent() bool {
|
|||||||
if t.page == nil || t.view == nil {
|
if t.page == nil || t.view == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if t.page.Url == "" {
|
if t.page.URL == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(t.page.Url, "about:") {
|
if strings.HasPrefix(t.page.URL, "about:") {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if t.page.Content == "" {
|
if t.page.Content == "" {
|
||||||
|
@ -3,7 +3,6 @@ package display
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
@ -45,12 +44,6 @@ func textWidth() int {
|
|||||||
return viper.GetInt("a-general.max_width")
|
return viper.GetInt("a-general.max_width")
|
||||||
}
|
}
|
||||||
|
|
||||||
// queryEscape is the same as url.PathEscape, but it also replaces the +.
|
|
||||||
// This is because Gemini requires percent-escaping for queries.
|
|
||||||
func queryEscape(query string) string {
|
|
||||||
return strings.ReplaceAll(url.PathEscape(query), "+", "%2B")
|
|
||||||
}
|
|
||||||
|
|
||||||
// resolveRelLink returns an absolute link for the given absolute link and relative one.
|
// resolveRelLink returns an absolute link for the given absolute link and relative one.
|
||||||
// It also returns an error if it could not resolve the links, which should be displayed
|
// It also returns an error if it could not resolve the links, which should be displayed
|
||||||
// to the user.
|
// to the user.
|
||||||
@ -62,7 +55,7 @@ func resolveRelLink(t *tab, prev, next string) (string, error) {
|
|||||||
prevParsed, _ := url.Parse(prev)
|
prevParsed, _ := url.Parse(prev)
|
||||||
nextParsed, err := url.Parse(next)
|
nextParsed, err := url.Parse(next)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.New("link URL could not be parsed")
|
return "", errors.New("link URL could not be parsed") //nolint:goerr113
|
||||||
}
|
}
|
||||||
return prevParsed.ResolveReference(nextParsed).String(), nil
|
return prevParsed.ResolveReference(nextParsed).String(), nil
|
||||||
}
|
}
|
||||||
@ -83,13 +76,6 @@ func normalizeURL(u string) string {
|
|||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.Contains(u, "://") && !strings.HasPrefix(u, "//") {
|
|
||||||
// No scheme at all in the URL
|
|
||||||
parsed, err = url.Parse("gemini://" + u)
|
|
||||||
if err != nil {
|
|
||||||
return u
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if parsed.Scheme == "" {
|
if parsed.Scheme == "" {
|
||||||
// Always add scheme
|
// Always add scheme
|
||||||
parsed.Scheme = "gemini"
|
parsed.Scheme = "gemini"
|
||||||
|
33
display/util_test.go
Normal file
33
display/util_test.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package display
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var normalizeURLTests = []struct {
|
||||||
|
u string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"gemini://example.com:1965/", "gemini://example.com/"},
|
||||||
|
{"gemini://example.com", "gemini://example.com/"},
|
||||||
|
{"//example.com", "gemini://example.com/"},
|
||||||
|
{"//example.com:1965", "gemini://example.com/"},
|
||||||
|
{"//example.com:123/", "gemini://example.com:123/"},
|
||||||
|
{"gemini://example.com/", "gemini://example.com/"},
|
||||||
|
{"gemini://example.com/#fragment", "gemini://example.com/"},
|
||||||
|
{"gemini://example.com#fragment", "gemini://example.com/"},
|
||||||
|
{"gemini://user@example.com/", "gemini://example.com/"},
|
||||||
|
// Other schemes, URL isn't modified
|
||||||
|
{"mailto:example@example.com", "mailto:example@example.com"},
|
||||||
|
{"magnet:?xt=urn:btih:test", "magnet:?xt=urn:btih:test"},
|
||||||
|
{"https://example.com", "https://example.com"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeURL(t *testing.T) {
|
||||||
|
for _, tt := range normalizeURLTests {
|
||||||
|
actual := normalizeURL(tt.u)
|
||||||
|
if actual != tt.expected {
|
||||||
|
t.Errorf("normalizeURL(%s): expected %s, actual %s", tt.u, tt.expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
6
go.mod
6
go.mod
@ -6,7 +6,8 @@ require (
|
|||||||
github.com/dustin/go-humanize v1.0.0
|
github.com/dustin/go-humanize v1.0.0
|
||||||
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
||||||
github.com/gdamore/tcell v1.3.1-0.20200608133353-cb1e5d6fa606
|
github.com/gdamore/tcell v1.3.1-0.20200608133353-cb1e5d6fa606
|
||||||
github.com/makeworld-the-better-one/go-gemini v0.7.0
|
github.com/google/go-cmp v0.5.0 // indirect
|
||||||
|
github.com/makeworld-the-better-one/go-gemini v0.8.4
|
||||||
github.com/makeworld-the-better-one/go-isemoji v1.0.0
|
github.com/makeworld-the-better-one/go-isemoji v1.0.0
|
||||||
github.com/makeworld-the-better-one/progressbar/v3 v3.3.5-0.20200710151429-125743e22b4f
|
github.com/makeworld-the-better-one/progressbar/v3 v3.3.5-0.20200710151429-125743e22b4f
|
||||||
github.com/mitchellh/go-homedir v1.1.0
|
github.com/mitchellh/go-homedir v1.1.0
|
||||||
@ -20,7 +21,8 @@ require (
|
|||||||
github.com/spf13/viper v1.7.0
|
github.com/spf13/viper v1.7.0
|
||||||
github.com/stretchr/testify v1.6.1
|
github.com/stretchr/testify v1.6.1
|
||||||
gitlab.com/tslocum/cview v1.4.8-0.20200713214710-cc7796c4ca44
|
gitlab.com/tslocum/cview v1.4.8-0.20200713214710-cc7796c4ca44
|
||||||
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1 // indirect
|
golang.org/x/sys v0.0.0-20200817155316-9781c653f443 // indirect
|
||||||
golang.org/x/text v0.3.3
|
golang.org/x/text v0.3.3
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||||
gopkg.in/ini.v1 v1.57.0 // indirect
|
gopkg.in/ini.v1 v1.57.0 // indirect
|
||||||
)
|
)
|
||||||
|
17
go.sum
17
go.sum
@ -73,8 +73,9 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z
|
|||||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
|
||||||
|
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
@ -129,8 +130,8 @@ github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tW
|
|||||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
github.com/makeworld-the-better-one/go-gemini v0.7.0 h1:TCerE47eYHLXj6RQDjfd5HdGVbcVqpBC6OoPBlyY7q4=
|
github.com/makeworld-the-better-one/go-gemini v0.8.4 h1:ntsQ9HnlJCmC9PDqXp/f1SCALjBMwh69BbT4BhFRFaw=
|
||||||
github.com/makeworld-the-better-one/go-gemini v0.7.0/go.mod h1:P7/FbZ+IEIbA/d+A0Y3w2GNgD8SA2AcNv7aDGJbaWG4=
|
github.com/makeworld-the-better-one/go-gemini v0.8.4/go.mod h1:P7/FbZ+IEIbA/d+A0Y3w2GNgD8SA2AcNv7aDGJbaWG4=
|
||||||
github.com/makeworld-the-better-one/go-isemoji v1.0.0 h1:W3O4+qwtXeT8PUDzcQ1UjxiupQWgc/oJHpqwrllx3xM=
|
github.com/makeworld-the-better-one/go-isemoji v1.0.0 h1:W3O4+qwtXeT8PUDzcQ1UjxiupQWgc/oJHpqwrllx3xM=
|
||||||
github.com/makeworld-the-better-one/go-isemoji v1.0.0/go.mod h1:FBjkPl9rr0G4vlZCc+Mr+QcnOfGCTbGWYW8/1sp06I0=
|
github.com/makeworld-the-better-one/go-isemoji v1.0.0/go.mod h1:FBjkPl9rr0G4vlZCc+Mr+QcnOfGCTbGWYW8/1sp06I0=
|
||||||
github.com/makeworld-the-better-one/progressbar/v3 v3.3.5-0.20200710151429-125743e22b4f h1:YEUlTs5gb35UlBLTgqrub9axWTYB3d7/8TxrkJDZpRI=
|
github.com/makeworld-the-better-one/progressbar/v3 v3.3.5-0.20200710151429-125743e22b4f h1:YEUlTs5gb35UlBLTgqrub9axWTYB3d7/8TxrkJDZpRI=
|
||||||
@ -294,10 +295,9 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/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-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/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 h1:bGb80FudwxpeucJUjPYJXuJ8Hk91vNtfvrymzwiei38=
|
|
||||||
golang.org/x/sys v0.0.0-20200610111108-226ff32320da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200610111108-226ff32320da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1 h1:sIky/MyNRSHTrdxfsiUSS4WIAMvInbeXljJz+jDjeYE=
|
golang.org/x/sys v0.0.0-20200817155316-9781c653f443 h1:X18bCaipMcoJGm27Nv7zr4XYPKGUy92GtqboKC2Hxaw=
|
||||||
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
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.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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
@ -323,6 +323,8 @@ golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtn
|
|||||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||||
@ -346,8 +348,9 @@ google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiq
|
|||||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
|
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
|
||||||
|
@ -16,6 +16,9 @@ import (
|
|||||||
|
|
||||||
var ErrTooLarge = errors.New("page content would be too large")
|
var ErrTooLarge = errors.New("page content would be too large")
|
||||||
var ErrTimedOut = errors.New("page download timed out")
|
var ErrTimedOut = errors.New("page download timed out")
|
||||||
|
var ErrCantDisplay = errors.New("invalid content for a page")
|
||||||
|
var ErrBadEncoding = errors.New("unsupported encoding")
|
||||||
|
var ErrBadMediatype = errors.New("displayable mediatype is not handled in the code, implementation error")
|
||||||
|
|
||||||
// isUTF8 returns true for charsets that are compatible with UTF-8 and don't need to be decoded.
|
// isUTF8 returns true for charsets that are compatible with UTF-8 and don't need to be decoded.
|
||||||
func isUTF8(charset string) bool {
|
func isUTF8(charset string) bool {
|
||||||
@ -56,7 +59,7 @@ func CanDisplay(res *gemini.Response) bool {
|
|||||||
// You must set the Page.Width value yourself.
|
// You must set the Page.Width value yourself.
|
||||||
func MakePage(url string, res *gemini.Response, width, leftMargin int) (*structs.Page, error) {
|
func MakePage(url string, res *gemini.Response, width, leftMargin int) (*structs.Page, error) {
|
||||||
if !CanDisplay(res) {
|
if !CanDisplay(res) {
|
||||||
return nil, errors.New("not valid content for a Page")
|
return nil, ErrCantDisplay
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
@ -90,7 +93,7 @@ func MakePage(url string, res *gemini.Response, width, leftMargin int) (*structs
|
|||||||
encoding, err := ianaindex.MIME.Encoding(params["charset"])
|
encoding, err := ianaindex.MIME.Encoding(params["charset"])
|
||||||
if encoding == nil || err != nil {
|
if encoding == nil || err != nil {
|
||||||
// Some encoding doesn't exist and wasn't caught in CanDisplay()
|
// Some encoding doesn't exist and wasn't caught in CanDisplay()
|
||||||
return nil, errors.New("unsupported encoding")
|
return nil, ErrBadEncoding
|
||||||
}
|
}
|
||||||
utfText, err = encoding.NewDecoder().String(buf.String())
|
utfText, err = encoding.NewDecoder().String(buf.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -102,7 +105,7 @@ func MakePage(url string, res *gemini.Response, width, leftMargin int) (*structs
|
|||||||
rendered, links := RenderGemini(utfText, width, leftMargin)
|
rendered, links := RenderGemini(utfText, width, leftMargin)
|
||||||
return &structs.Page{
|
return &structs.Page{
|
||||||
Mediatype: structs.TextGemini,
|
Mediatype: structs.TextGemini,
|
||||||
Url: url,
|
URL: url,
|
||||||
Raw: utfText,
|
Raw: utfText,
|
||||||
Content: rendered,
|
Content: rendered,
|
||||||
Links: links,
|
Links: links,
|
||||||
@ -112,22 +115,22 @@ func MakePage(url string, res *gemini.Response, width, leftMargin int) (*structs
|
|||||||
// ANSI
|
// ANSI
|
||||||
return &structs.Page{
|
return &structs.Page{
|
||||||
Mediatype: structs.TextAnsi,
|
Mediatype: structs.TextAnsi,
|
||||||
Url: url,
|
URL: url,
|
||||||
Raw: utfText,
|
Raw: utfText,
|
||||||
Content: RenderANSI(utfText, leftMargin),
|
Content: RenderANSI(utfText, leftMargin),
|
||||||
Links: []string{},
|
Links: []string{},
|
||||||
}, nil
|
}, nil
|
||||||
} else {
|
|
||||||
// Treated as plaintext
|
|
||||||
return &structs.Page{
|
|
||||||
Mediatype: structs.TextPlain,
|
|
||||||
Url: url,
|
|
||||||
Raw: utfText,
|
|
||||||
Content: RenderPlainText(utfText, leftMargin),
|
|
||||||
Links: []string{},
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Treated as plaintext
|
||||||
|
return &structs.Page{
|
||||||
|
Mediatype: structs.TextPlain,
|
||||||
|
URL: url,
|
||||||
|
Raw: utfText,
|
||||||
|
Content: RenderPlainText(utfText, leftMargin),
|
||||||
|
Links: []string{},
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.New("displayable mediatype is not handled in the code, implementation error")
|
return nil, ErrBadMediatype
|
||||||
}
|
}
|
||||||
|
@ -101,11 +101,11 @@ func convertRegularGemini(s string, numLinks, width int) (string, []string) {
|
|||||||
for i := range lines {
|
for i := range lines {
|
||||||
lines[i] = strings.TrimRight(lines[i], " \r\t\n")
|
lines[i] = strings.TrimRight(lines[i], " \r\t\n")
|
||||||
|
|
||||||
if strings.HasPrefix(lines[i], "#") {
|
if strings.HasPrefix(lines[i], "#") { //nolint:gocritic
|
||||||
// Headings
|
// Headings
|
||||||
var tag string
|
var tag string
|
||||||
if viper.GetBool("a-general.color") {
|
if viper.GetBool("a-general.color") {
|
||||||
if strings.HasPrefix(lines[i], "###") {
|
if strings.HasPrefix(lines[i], "###") { //nolint:gocritic
|
||||||
tag = fmt.Sprintf("[%s::b]", config.GetColorString("hdg_3"))
|
tag = fmt.Sprintf("[%s::b]", config.GetColorString("hdg_3"))
|
||||||
} else if strings.HasPrefix(lines[i], "##") {
|
} else if strings.HasPrefix(lines[i], "##") {
|
||||||
tag = fmt.Sprintf("[%s::b]", config.GetColorString("hdg_2"))
|
tag = fmt.Sprintf("[%s::b]", config.GetColorString("hdg_2"))
|
||||||
|
@ -18,14 +18,14 @@ const (
|
|||||||
|
|
||||||
// Page is for storing UTF-8 text/gemini pages, as well as text/plain pages.
|
// Page is for storing UTF-8 text/gemini pages, as well as text/plain pages.
|
||||||
type Page struct {
|
type Page struct {
|
||||||
Url string
|
URL string
|
||||||
Mediatype Mediatype
|
Mediatype Mediatype
|
||||||
Raw string // The raw response, as received over the network
|
Raw string // The raw response, as received over the network
|
||||||
Content string // The processed content, NOT raw. Uses cview color tags. All link/link texts must have region tags. It will also have a left margin.
|
Content string // The processed content, NOT raw. Uses cview color tags. It will also have a left margin.
|
||||||
Links []string // URLs, for each region in the content.
|
Links []string // URLs, for each region in the content.
|
||||||
Row int // Scroll position
|
Row int // Scroll position
|
||||||
Column int // ditto
|
Column int // ditto
|
||||||
Width int // The width of the terminal at the time when the Content was set. This is to know when reformatting should happen.
|
Width int // The terminal width when the Content was set, to know when reformatting should happen.
|
||||||
Selected string // The current text or link selected
|
Selected string // The current text or link selected
|
||||||
SelectedID string // The cview region ID for the selected text/link
|
SelectedID string // The cview region ID for the selected text/link
|
||||||
Mode PageMode
|
Mode PageMode
|
||||||
@ -34,7 +34,7 @@ type Page struct {
|
|||||||
|
|
||||||
// Size returns an approx. size of a Page in bytes.
|
// Size returns an approx. size of a Page in bytes.
|
||||||
func (p *Page) Size() int {
|
func (p *Page) Size() int {
|
||||||
n := len(p.Raw) + len(p.Content) + len(p.Url) + len(p.Selected) + len(p.SelectedID)
|
n := len(p.Raw) + len(p.Content) + len(p.URL) + len(p.Selected) + len(p.SelectedID)
|
||||||
for i := range p.Links {
|
for i := range p.Links {
|
||||||
n += len(p.Links[i])
|
n += len(p.Links[i])
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
// +build linux freebsd netbsd openbsd
|
// +build linux freebsd netbsd openbsd
|
||||||
|
|
||||||
|
//nolint:goerr113
|
||||||
package webbrowser
|
package webbrowser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
Loading…
Reference in New Issue
Block a user