mirror of
https://github.com/makew0rld/amfora.git
synced 2024-12-04 14:46:29 -05:00
🎉 Initial commit, full featured
This commit is contained in:
commit
4951ffa9fe
107
.gitignore
vendored
Normal file
107
.gitignore
vendored
Normal file
@ -0,0 +1,107 @@
|
||||
# Binary
|
||||
amfora
|
||||
build/
|
||||
|
||||
# Test logs
|
||||
*.log
|
||||
|
||||
# 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
|
||||
|
||||
### Code ###
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
### Go ###
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
### Go Patch ###
|
||||
/vendor/
|
||||
/Godeps/
|
||||
|
||||
### Linux ###
|
||||
*~
|
||||
|
||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||
.fuse_hidden*
|
||||
|
||||
# KDE directory preferences
|
||||
.directory
|
||||
|
||||
# Linux trash folder which might appear on any partition or disk
|
||||
.Trash-*
|
||||
|
||||
# .nfs files are created when an open file is removed but is still being accessed
|
||||
.nfs*
|
||||
|
||||
### macOS ###
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
### Windows ###
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/code,go,linux,macos,windows
|
21
NOTES.md
Normal file
21
NOTES.md
Normal file
@ -0,0 +1,21 @@
|
||||
# Notes
|
||||
|
||||
- All the maps and stuff could be replaced with a `tab` struct
|
||||
- And then just one single map of tab number to `tab`
|
||||
|
||||
## Bugs
|
||||
- Wrapping is messed up on CHAZ post, but nothing else
|
||||
- Filed [issue 23](https://gitlab.com/tslocum/cview/-/issues/23)
|
||||
- Error modal doesn't show the title
|
||||
- Filed [issue 24](https://gitlab.com/tslocum/cview/-/issues/24)
|
||||
- Text background not reset on ANSI pages
|
||||
- Filed [issue 25](https://gitlab.com/tslocum/cview/-/issues/25)
|
||||
- Inputfield isn't repeatedly in focus
|
||||
- Tried multiple focus options with App and Form funcs, but nothing worked
|
||||
|
||||
## Small todos
|
||||
- Look at other todos in code
|
||||
- Add "Why the name amfora" thing to README
|
||||
- Add GIF to README
|
||||
- Pass `gemini://egsam.pitr.ca/` test
|
||||
- Timeout for server not closing connection?
|
58
README.md
Normal file
58
README.md
Normal file
@ -0,0 +1,58 @@
|
||||
# Amfora
|
||||
|
||||
![Amfora logo](logo.png)
|
||||
##### Modified from: amphora by Alvaro Cabrera from the Noun Project
|
||||
|
||||
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.
|
||||
|
||||
It fully passes Sean Conman's client torture test, with exception of the alternate encodings section, as only UTF-8 and ASCII are supported. It mostly passes the Egsam test.
|
||||
|
||||
## Usage
|
||||
|
||||
Just call `amfora` or `amfora <url> <other-url>` on the terminal. On Windows it might be `amfora.exe` instead.
|
||||
|
||||
The project keeps many standard terminal keybindings and is intuitive. Press <kbd>?</kbd> inside the application to pull up the help menu with a list of all the keybindings, and <kbd>Esc</kbd> to leave it. If you have used Bombadillo you will find it similar.
|
||||
|
||||
It is designed with large or fullscreen terminals in mind. For optimal usage, make your terminal fullscreen. It was also designed with a dark background terminal in mind, but please file an issue if the colour choices look bad on your terminal setup.
|
||||
|
||||
## Features / Roadmap
|
||||
|
||||
- [x] URL browsing with TOFU and error handling
|
||||
- [x] Tabbed browsing
|
||||
- [x] Support ANSI color codes on pages, even for Windows
|
||||
- [x] Styled page content (headings, links)
|
||||
- [x] Basic forward/backward history, for each tab
|
||||
- [x] Input (Status Code 10 & 11)
|
||||
- [ ] Built-in search using GUS
|
||||
- [ ] Bookmarks
|
||||
- [ ] Search in pages with <kbd>Ctrl-F</kbd>
|
||||
- [ ] Download pages and arbitrary data
|
||||
- [ ] Full mouse support
|
||||
- [ ] Emoji favicons
|
||||
- See `gemini://mozz.us/files/rfc_gemini_favicon.gmi` for details
|
||||
- [ ] Table of contents for pages
|
||||
- [ ] ~~Collapsing of gemini site sections (as determined by headers)~~
|
||||
- [ ] Full client certificate UX within the client
|
||||
- *I will be waiting for some spec changes to happen before implementing this*
|
||||
- Create transient and permanent certs within the client, per domain
|
||||
- Manage and browse them
|
||||
- https://lists.orbitalfox.eu/archives/gemini/2020/001400.html
|
||||
- [ ] Subscribe to RSS and Atom feeds and display them
|
||||
- [ ] Support Markdown rendering
|
||||
|
||||
## Configuration
|
||||
The config file is written in the intuitive [TOML](https://github.com/toml-lang/toml) file format. See [default-config.toml](./default-config.toml) for details. By default this file is available at `~/.config/amfora/config.toml`.
|
||||
|
||||
On Windows, the file is in `%APPDATA%\amfora\config.toml`, which usually expands to `C:\Users\<username>\AppData\Roaming\amfora\config.toml`.
|
||||
|
||||
## Libraries
|
||||
Amfora ❤️ open source!
|
||||
|
||||
- [cview](https://gitlab.com/tslocum/cview/) for the TUI
|
||||
- It's a fork of [tview](https://github.com/rivo/tview) with PRs merged and active support
|
||||
- It uses [tcell](https://github.com/gdamore/tcell) for low level terminal operations
|
||||
- [Viper](https://github.com/spf13/viper) for configuration and TOFU storing
|
||||
- [go-gemini](https://github.com/makeworld-the-better-one/go-gemini), my forked and updated Gemini client/server library
|
||||
|
||||
## License
|
||||
This project is licensed under the GPL v3.0. See the [LICENSE](./LICENSE) file for details.
|
35
amfora.go
Normal file
35
amfora.go
Normal file
@ -0,0 +1,35 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/makeworld-the-better-one/amfora/config"
|
||||
"github.com/makeworld-the-better-one/amfora/display"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// err := logger.Init()
|
||||
// if err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
|
||||
err := config.Init()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
display.Init()
|
||||
|
||||
for _, url := range os.Args[1:] {
|
||||
display.NewTab()
|
||||
display.URL(url)
|
||||
}
|
||||
|
||||
if len(os.Args[1:]) == 0 {
|
||||
// There should always be a tab
|
||||
display.NewTab()
|
||||
}
|
||||
|
||||
if err = display.App.Run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
125
cache/cache.go
vendored
Normal file
125
cache/cache.go
vendored
Normal file
@ -0,0 +1,125 @@
|
||||
// Package cache provides an interface for a cache of strings, aka text/gemini pages.
|
||||
// It is fully thread safe.
|
||||
package cache
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"sync"
|
||||
|
||||
"github.com/makeworld-the-better-one/amfora/structs"
|
||||
)
|
||||
|
||||
var pages = make(map[string]*structs.Page) // The actual cache
|
||||
var urls = make([]string, 0) // Duplicate of the keys in the `pages` map, but in order of being added
|
||||
var maxPages = 0 // Max allowed number of pages in cache
|
||||
var maxSize = 0 // Max allowed cache size in bytes
|
||||
var lock = sync.RWMutex{}
|
||||
|
||||
// SetMaxPages sets the max number of pages the cache can hold.
|
||||
// A value <= 0 means infinite pages.
|
||||
func SetMaxPages(max int) {
|
||||
maxPages = max
|
||||
}
|
||||
|
||||
// SetMaxSize sets the max size the cache can be, in bytes.
|
||||
// A value <= 0 means infinite size.
|
||||
func SetMaxSize(max int) {
|
||||
maxSize = max
|
||||
}
|
||||
|
||||
func removeIndex(s []string, i int) []string {
|
||||
s[len(s)-1], s[i] = s[i], s[len(s)-1]
|
||||
return s[:len(s)-1]
|
||||
}
|
||||
|
||||
func removeUrl(url string) {
|
||||
for i := range urls {
|
||||
if urls[i] == url {
|
||||
urls = removeIndex(urls, i)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds a page to the cache, removing earlier pages as needed
|
||||
// to keep the cache inside its limits.
|
||||
//
|
||||
// If your page is larger than the max cache size, the provided page
|
||||
// will silently not be added to the cache.
|
||||
func Add(p *structs.Page) {
|
||||
if p.Url == "" {
|
||||
// Just in case, don't waste cache on new tab page
|
||||
return
|
||||
}
|
||||
// Never cache pages with query strings, to reduce unexpected behaviour
|
||||
parsed, err := url.Parse(p.Url)
|
||||
if err == nil && parsed.RawQuery != "" {
|
||||
return
|
||||
}
|
||||
|
||||
if p.Size() > maxSize && maxSize > 0 {
|
||||
// This page can never be added
|
||||
return
|
||||
}
|
||||
|
||||
// Remove earlier pages to make room for this one
|
||||
// There should only ever be 1 page to remove at most,
|
||||
// but this handles more just in case.
|
||||
for NumPages() >= maxPages && maxPages > 0 {
|
||||
Remove(urls[0])
|
||||
}
|
||||
// Do the same but for cache size
|
||||
for Size()+p.Size() > maxSize && maxSize > 0 {
|
||||
Remove(urls[0])
|
||||
}
|
||||
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
pages[p.Url] = p
|
||||
// Remove the URL if it was already there, then add it to the end
|
||||
removeUrl(p.Url)
|
||||
urls = append(urls, p.Url)
|
||||
}
|
||||
|
||||
// Remove will remove a page from the cache.
|
||||
// Even if the page doesn't exist there will be no error.
|
||||
func Remove(url string) {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
delete(pages, url)
|
||||
removeUrl(url)
|
||||
}
|
||||
|
||||
// Clear removes all pages from the cache.
|
||||
func Clear() {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
pages = make(map[string]*structs.Page)
|
||||
urls = make([]string, 0)
|
||||
}
|
||||
|
||||
// Size returns the approx. current size of the cache in bytes.
|
||||
func Size() int {
|
||||
lock.RLock()
|
||||
defer lock.RUnlock()
|
||||
n := 0
|
||||
for _, page := range pages {
|
||||
n += page.Size()
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func NumPages() int {
|
||||
lock.RLock()
|
||||
defer lock.RUnlock()
|
||||
return len(pages)
|
||||
}
|
||||
|
||||
// Get 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
|
||||
func Get(url string) (*structs.Page, bool) {
|
||||
lock.RLock()
|
||||
defer lock.RUnlock()
|
||||
p, ok := pages[url]
|
||||
return p, ok
|
||||
}
|
82
cache/cache_test.go
vendored
Normal file
82
cache/cache_test.go
vendored
Normal file
@ -0,0 +1,82 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/makeworld-the-better-one/amfora/structs"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var p = structs.Page{Url: "example.com"}
|
||||
var p2 = structs.Page{Url: "example.org"}
|
||||
var queryPage = structs.Page{Url: "gemini://example.com/test?query"}
|
||||
|
||||
func reset() {
|
||||
Clear()
|
||||
SetMaxPages(0)
|
||||
SetMaxSize(0)
|
||||
}
|
||||
|
||||
func TestMaxPages(t *testing.T) {
|
||||
reset()
|
||||
SetMaxPages(1)
|
||||
Add(&p)
|
||||
Add(&p2)
|
||||
assert.Equal(t, 1, NumPages(), "there should only be one page")
|
||||
}
|
||||
|
||||
func TestMaxSize(t *testing.T) {
|
||||
reset()
|
||||
assert := assert.New(t)
|
||||
SetMaxSize(p.Size())
|
||||
Add(&p)
|
||||
assert.Equal(1, NumPages(), "one page should be added")
|
||||
Add(&p2)
|
||||
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")
|
||||
}
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
reset()
|
||||
Add(&p)
|
||||
Remove(p.Url)
|
||||
assert.Equal(t, 0, NumPages(), "there shouldn't be any pages after the removal")
|
||||
}
|
||||
|
||||
func TestClearAndNumPages(t *testing.T) {
|
||||
reset()
|
||||
Add(&p)
|
||||
Clear()
|
||||
assert.Equal(t, 0, len(pages), "map should be empty")
|
||||
assert.Equal(t, 0, len(urls), "urls slice shoulde be empty")
|
||||
assert.Equal(t, 0, NumPages(), "NumPages should report empty too")
|
||||
}
|
||||
|
||||
func TestSize(t *testing.T) {
|
||||
reset()
|
||||
Add(&p)
|
||||
assert.Equal(t, p.Size(), Size(), "sizes should match")
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
reset()
|
||||
Add(&p)
|
||||
Add(&p2)
|
||||
page, ok := Get(p.Url)
|
||||
if !ok {
|
||||
t.Fatal("Get should say that the page was found")
|
||||
}
|
||||
if page.Url != p.Url {
|
||||
t.Error("page urls don't match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryString(t *testing.T) {
|
||||
// Pages with URLs with query strings don't get added
|
||||
reset()
|
||||
Add(&queryPage)
|
||||
_, ok := Get(queryPage.Url)
|
||||
if ok {
|
||||
t.Fatal("Get should not find the page, because it had query string")
|
||||
}
|
||||
}
|
22
client/client.go
Normal file
22
client/client.go
Normal file
@ -0,0 +1,22 @@
|
||||
// Package client retrieves data over Gemini and implements a TOFU system.
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/makeworld-the-better-one/go-gemini"
|
||||
)
|
||||
|
||||
// Fetch returns response data and an error.
|
||||
// The error text is human friendly and should be displayed.
|
||||
func Fetch(url string) (*gemini.Response, error) {
|
||||
resp, err := gemini.Fetch(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("URL could not be fetched: %v", err)
|
||||
}
|
||||
ok := handleTofu(resp.Cert)
|
||||
if !ok {
|
||||
return nil, ErrTofu
|
||||
}
|
||||
return resp, err
|
||||
}
|
98
client/tofu.go
Normal file
98
client/tofu.go
Normal file
@ -0,0 +1,98 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/makeworld-the-better-one/amfora/config"
|
||||
)
|
||||
|
||||
// TOFU implementation.
|
||||
// Stores cert hash and expiry for now, like Bombadillo.
|
||||
// There is ongoing TOFU discussiong on the mailing list about better
|
||||
// ways to do this, and I will update this file once those are decided on.
|
||||
|
||||
var ErrTofu = errors.New("server cert does not match TOFU database")
|
||||
|
||||
var tofuStore = config.TofuStore
|
||||
|
||||
// idKey returns the config/viper key needed to retrieve
|
||||
// a cert's ID / fingerprint.
|
||||
func idKey(domain string) string {
|
||||
return strings.ReplaceAll(domain, ".", "/")
|
||||
}
|
||||
|
||||
func expiryKey(domain string) string {
|
||||
return strings.ReplaceAll(strings.TrimSuffix(domain, "."), ".", "/") + "/expiry"
|
||||
}
|
||||
|
||||
func loadTofuEntry(domain string) (string, time.Time, error) {
|
||||
id := tofuStore.GetString(idKey(domain)) // Fingerprint
|
||||
if len(id) != 64 {
|
||||
// Not set, or invalid
|
||||
return "", time.Time{}, errors.New("not found")
|
||||
}
|
||||
|
||||
expiry := tofuStore.GetTime(expiryKey(domain))
|
||||
if expiry.IsZero() {
|
||||
// Not set
|
||||
return id, time.Time{}, errors.New("not found")
|
||||
}
|
||||
return id, expiry, nil
|
||||
}
|
||||
|
||||
// certID returns a generic string representing a cert or domain.
|
||||
func certID(cert *x509.Certificate) string {
|
||||
h := sha256.New()
|
||||
h.Write(cert.Raw)
|
||||
return fmt.Sprintf("%X", h.Sum(nil))
|
||||
|
||||
// The old way that uses the cert public key:
|
||||
// b, err := x509.MarshalPKIXPublicKey(cert.PublicKey)
|
||||
// h := sha256.New()
|
||||
// if err != nil {
|
||||
// // Unsupported key type - try to store a hash of the struct instead
|
||||
// h.Write([]byte(fmt.Sprint(cert.PublicKey)))
|
||||
// return fmt.Sprintf("%X", h.Sum(nil))
|
||||
// }
|
||||
// h.Write(b)
|
||||
// return fmt.Sprintf("%X", h.Sum(nil))
|
||||
}
|
||||
|
||||
func saveTofuEntry(cert *x509.Certificate) {
|
||||
tofuStore.Set(idKey(cert.Subject.CommonName), certID(cert))
|
||||
tofuStore.Set(expiryKey(cert.Subject.CommonName), cert.NotAfter.UTC())
|
||||
err := tofuStore.WriteConfig()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// handleTofu is the abstracted interface for taking care of TOFU.
|
||||
// A cert is provided and storage, checking, etc, are taken care of.
|
||||
// It returns a bool indicating if the cert is valid according to
|
||||
// the TOFU database.
|
||||
// If false is returned, the connection should not go ahead.
|
||||
func handleTofu(cert *x509.Certificate) bool {
|
||||
id, expiry, err := loadTofuEntry(cert.Subject.CommonName)
|
||||
if err != nil {
|
||||
// Cert isn't in database or data is malformed
|
||||
// So it can't be checked and anything is valid
|
||||
saveTofuEntry(cert)
|
||||
return true
|
||||
}
|
||||
if certID(cert) == id {
|
||||
// Save cert as the one stored
|
||||
return true
|
||||
}
|
||||
if time.Now().After(expiry) {
|
||||
// Old cert expired, so anything is valid
|
||||
saveTofuEntry(cert)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
122
config/config.go
Normal file
122
config/config.go
Normal file
@ -0,0 +1,122 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/makeworld-the-better-one/amfora/cache"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var amforaAppData string // Where amfora files are stored on Windows - cached here
|
||||
|
||||
func configDir() string {
|
||||
home, err := homedir.Dir()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
return amforaAppData
|
||||
}
|
||||
|
||||
// Unix / POSIX system
|
||||
return filepath.Join(home, ".config", "amfora")
|
||||
}
|
||||
|
||||
func configPath() string {
|
||||
return filepath.Join(configDir(), "config.toml")
|
||||
}
|
||||
|
||||
var TofuStore = viper.New()
|
||||
|
||||
func tofuDBDir() string {
|
||||
home, err := homedir.Dir()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Windows just stores it in APPDATA along with other stuff
|
||||
if runtime.GOOS == "windows" {
|
||||
return amforaAppData
|
||||
}
|
||||
// XDG cache dir on POSIX systems
|
||||
return filepath.Join(home, ".cache", "amfora")
|
||||
}
|
||||
|
||||
func tofuDBPath() string {
|
||||
return filepath.Join(tofuDBDir(), "tofu.toml")
|
||||
}
|
||||
|
||||
func Init() error {
|
||||
home, err := homedir.Dir()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
appdata, ok := os.LookupEnv("APPDATA")
|
||||
if ok {
|
||||
amforaAppData = filepath.Join(appdata, "amfora")
|
||||
} else {
|
||||
amforaAppData = filepath.Join(home, filepath.FromSlash("AppData/Roaming/amfora/"))
|
||||
}
|
||||
}
|
||||
|
||||
err = os.MkdirAll(configDir(), 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.OpenFile(configPath(), os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
|
||||
if err == nil {
|
||||
// Config file doesn't exist yet, write the default one
|
||||
_, err = f.Write(defaultConf)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
err = os.MkdirAll(tofuDBDir(), 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.OpenFile(tofuDBPath(), os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
|
||||
|
||||
TofuStore.SetConfigFile(tofuDBPath())
|
||||
TofuStore.SetConfigType("toml")
|
||||
err = TofuStore.ReadInConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
viper.SetDefault("a-general.home", "gemini.circumlunar.space")
|
||||
viper.SetDefault("a-general.http", "default")
|
||||
viper.SetDefault("a-general.search", "gus.guru/search")
|
||||
viper.SetDefault("a-general.color", true)
|
||||
viper.SetDefault("a-general.bullets", true)
|
||||
viper.SetDefault("a-general.wrap_width", 100)
|
||||
viper.SetDefault("cache.max_size", 0)
|
||||
viper.SetDefault("cache.max_pages", 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"))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetWrapWidth() int {
|
||||
i := viper.GetInt("a-general.wrap_width")
|
||||
if i <= 0 {
|
||||
return 100 // The default
|
||||
}
|
||||
return i
|
||||
}
|
33
config/default.go
Normal file
33
config/default.go
Normal file
@ -0,0 +1,33 @@
|
||||
package config
|
||||
|
||||
//go:generate ./default.sh
|
||||
var defaultConf = []byte(`# This is the default config file.
|
||||
# It also shows all the default values, if you don't create the file.
|
||||
|
||||
# All URL values may omit the scheme and/or port.
|
||||
|
||||
[a-general]
|
||||
home = "gemini://gemini.circumlunar.space"
|
||||
# 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.
|
||||
# If a command is set, than the URL will be added (in quotes) to the end of the command.
|
||||
# A space will be prepended if necessary.
|
||||
http = "default"
|
||||
|
||||
search = "gemini://gus.guru/search" # Any URL that will accept a query string can be put here
|
||||
color = true # Whether colors will be used in the terminal
|
||||
bullets = true # Whether to replace list asterisks with unicode bullets
|
||||
wrap_width = 100 # How many columns to wrap a page's text to. Preformatted blocks are not wrapped.
|
||||
|
||||
[bookmarks]
|
||||
# Make sure to quote the key names if you edit this part yourself
|
||||
# Example:
|
||||
# "CAPCOM" = "gemini://gemini.circumlunar.space/capcom/"
|
||||
|
||||
# Options for page cache - which is only for text/gemini pages
|
||||
# Increase the cache size to speed up browsing at the expense of memory
|
||||
[cache]
|
||||
# Zero values mean there is no limit
|
||||
max_size = 0 # Size in bytes
|
||||
max_pages = 30 # The maximum number of pages the cache can store
|
||||
`)
|
6
config/default.sh
Executable file
6
config/default.sh
Executable file
@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
head -n 3 default.go | tee default.go > /dev/null
|
||||
echo -n 'var defaultConf = []byte(`' >> default.go
|
||||
cat ../default-config.toml >> default.go
|
||||
echo '`)' >> default.go
|
29
default-config.toml
Normal file
29
default-config.toml
Normal file
@ -0,0 +1,29 @@
|
||||
# This is the default config file.
|
||||
# It also shows all the default values, if you don't create the file.
|
||||
|
||||
# All URL values may omit the scheme and/or port.
|
||||
|
||||
[a-general]
|
||||
home = "gemini://gemini.circumlunar.space"
|
||||
# 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.
|
||||
# If a command is set, than the URL will be added (in quotes) to the end of the command.
|
||||
# A space will be prepended if necessary.
|
||||
http = "default"
|
||||
|
||||
search = "gemini://gus.guru/search" # Any URL that will accept a query string can be put here
|
||||
color = true # Whether colors will be used in the terminal
|
||||
bullets = true # Whether to replace list asterisks with unicode bullets
|
||||
wrap_width = 100 # How many columns to wrap a page's text to. Preformatted blocks are not wrapped.
|
||||
|
||||
[bookmarks]
|
||||
# Make sure to quote the key names if you edit this part yourself
|
||||
# Example:
|
||||
# "CAPCOM" = "gemini://gemini.circumlunar.space/capcom/"
|
||||
|
||||
# Options for page cache - which is only for text/gemini pages
|
||||
# Increase the cache size to speed up browsing at the expense of memory
|
||||
[cache]
|
||||
# Zero values mean there is no limit
|
||||
max_size = 0 # Size in bytes
|
||||
max_pages = 30 # The maximum number of pages the cache can store
|
431
display/display.go
Normal file
431
display/display.go
Normal file
@ -0,0 +1,431 @@
|
||||
package display
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/makeworld-the-better-one/amfora/renderer"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/makeworld-the-better-one/amfora/cache"
|
||||
"github.com/makeworld-the-better-one/amfora/structs"
|
||||
"gitlab.com/tslocum/cview"
|
||||
//"github.com/makeworld-the-better-one/amfora/cview"
|
||||
)
|
||||
|
||||
var curTab = -1 // What number tab is currently visible, -1 means there are no tabs at all
|
||||
var tabMap = make(map[int]*structs.Page) // Map of tab number to page
|
||||
// Holds the actual tab primitives
|
||||
var tabViews = make(map[int]*cview.TextView)
|
||||
|
||||
// The user input and URL display bar at the bottom
|
||||
var bottomBar = cview.NewInputField().
|
||||
SetFieldBackgroundColor(tcell.ColorWhite).
|
||||
SetFieldTextColor(tcell.ColorBlack).
|
||||
SetLabelColor(tcell.ColorGreen)
|
||||
|
||||
var helpTable = cview.NewTable().
|
||||
SetSelectable(false, false).
|
||||
SetFixed(1, 2).
|
||||
SetBorders(true).
|
||||
SetBordersColor(tcell.ColorGray)
|
||||
|
||||
// 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".
|
||||
// The only pages that don't confine to this scheme named after the modals above,
|
||||
// which is used to draw modals on top the current tab.
|
||||
// Ex: "info", "error", "input", "yesno"
|
||||
var tabPages = cview.NewPages().
|
||||
AddPage("help", helpTable, true, false).
|
||||
AddPage("info", infoModal, false, false).
|
||||
AddPage("error", errorModal, false, false).
|
||||
AddPage("input", inputModal, false, false).
|
||||
AddPage("yesno", yesNoModal, false, false)
|
||||
|
||||
// The tabs at the top with titles
|
||||
var tabRow = cview.NewTextView().
|
||||
SetDynamicColors(true).
|
||||
SetRegions(true).
|
||||
SetScrollable(true).
|
||||
SetWrap(false).
|
||||
SetHighlightedFunc(func(added, removed, remaining []string) {
|
||||
// There will always only be one string in added - never multiple highlights
|
||||
// Remaining should always be empty
|
||||
i, _ := strconv.Atoi(added[0])
|
||||
tabPages.SwitchToPage(strconv.Itoa(i)) // Tab names are just numbers, zero-indexed
|
||||
})
|
||||
|
||||
// Root layout
|
||||
var layout = cview.NewFlex().
|
||||
SetDirection(cview.FlexRow).
|
||||
AddItem(tabRow, 1, 1, false).
|
||||
AddItem(nil, 1, 1, false). // One line of empty space above the page
|
||||
//AddItem(tabPages, 0, 1, true).
|
||||
AddItem(cview.NewFlex(). // The page text in the middle is held in another flex, to center it
|
||||
SetDirection(cview.FlexColumn).
|
||||
AddItem(nil, 0, 1, false).
|
||||
AddItem(tabPages, 0, 7, true). // The text occupies 5/6 of the screen horizontally
|
||||
AddItem(nil, 0, 1, false),
|
||||
0, 1, true).
|
||||
AddItem(nil, 1, 1, false). // One line of empty space before bottomBar
|
||||
AddItem(bottomBar, 1, 1, false)
|
||||
|
||||
var App = cview.NewApplication().EnableMouse(false).SetRoot(layout, true)
|
||||
|
||||
var renderedNewTabContent string
|
||||
var newTabLinks []string
|
||||
var newTabPage structs.Page
|
||||
|
||||
func Init() {
|
||||
tabRow.SetChangedFunc(func() {
|
||||
App.Draw()
|
||||
})
|
||||
|
||||
// Populate help table
|
||||
helpTable.SetDoneFunc(func(key tcell.Key) {
|
||||
if key == tcell.KeyEsc {
|
||||
tabPages.SwitchToPage(strconv.Itoa(curTab))
|
||||
}
|
||||
})
|
||||
rows := strings.Count(helpCells, "\n") + 1
|
||||
cells := strings.Split(
|
||||
strings.ReplaceAll(helpCells, "\n", "|"),
|
||||
"|")
|
||||
cell := 0
|
||||
for r := 0; r < rows; r++ {
|
||||
for c := 0; c < 2; c++ {
|
||||
var tableCell *cview.TableCell
|
||||
if c == 0 {
|
||||
tableCell = cview.NewTableCell(cells[cell]).
|
||||
SetAttributes(tcell.AttrBold).
|
||||
SetExpansion(1)
|
||||
} else {
|
||||
tableCell = cview.NewTableCell(cells[cell]).
|
||||
SetExpansion(2)
|
||||
}
|
||||
helpTable.SetCell(r, c, tableCell)
|
||||
cell++
|
||||
}
|
||||
}
|
||||
|
||||
bottomBar.SetBackgroundColor(tcell.ColorWhite)
|
||||
bottomBar.SetDoneFunc(func(key tcell.Key) {
|
||||
switch key {
|
||||
case tcell.KeyEnter:
|
||||
// TODO: Support search
|
||||
// Send the URL/number typed in
|
||||
|
||||
if strings.TrimSpace(bottomBar.GetText()) == "" {
|
||||
// Ignore
|
||||
bottomBar.SetLabel("")
|
||||
bottomBar.SetText(tabMap[curTab].Url)
|
||||
App.SetFocus(tabViews[curTab])
|
||||
return
|
||||
}
|
||||
|
||||
i, err := strconv.Atoi(bottomBar.GetText())
|
||||
if err != nil {
|
||||
// It's a full URL
|
||||
URL(bottomBar.GetText())
|
||||
bottomBar.SetLabel("")
|
||||
return
|
||||
}
|
||||
if i <= len(tabMap[curTab].Links) && i > 0 {
|
||||
// Valid link number
|
||||
followLink(tabMap[curTab].Url, tabMap[curTab].Links[i-1])
|
||||
bottomBar.SetLabel("")
|
||||
return
|
||||
}
|
||||
// Invalid link number
|
||||
bottomBar.SetLabel("")
|
||||
bottomBar.SetText(tabMap[curTab].Url)
|
||||
App.SetFocus(tabViews[curTab])
|
||||
|
||||
case tcell.KeyEscape:
|
||||
// Set back to what it was
|
||||
bottomBar.SetLabel("")
|
||||
bottomBar.SetText(tabMap[curTab].Url)
|
||||
App.SetFocus(tabViews[curTab])
|
||||
}
|
||||
// Other potential keys are Tab and Backtab, they are ignored
|
||||
})
|
||||
|
||||
// Render the default new tab content ONCE and store it for later
|
||||
renderedNewTabContent, newTabLinks = renderer.RenderGemini(newTabContent)
|
||||
newTabPage = structs.Page{Content: renderedNewTabContent, Links: newTabLinks}
|
||||
|
||||
modalInit()
|
||||
|
||||
// Setup map of keys to functions here
|
||||
// Changing tabs, new tab, etc
|
||||
App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
_, ok := App.GetFocus().(*cview.Button)
|
||||
if ok {
|
||||
// It's focused on a modal right now, nothing should interrupt
|
||||
return event
|
||||
}
|
||||
_, ok = App.GetFocus().(*cview.InputField)
|
||||
if ok {
|
||||
// An InputField is in focus, nothing should interrupt
|
||||
return event
|
||||
}
|
||||
|
||||
switch event.Key() {
|
||||
case tcell.KeyCtrlT:
|
||||
NewTab()
|
||||
return nil
|
||||
case tcell.KeyCtrlW:
|
||||
CloseTab()
|
||||
return nil
|
||||
case tcell.KeyCtrlR:
|
||||
Reload()
|
||||
return nil
|
||||
case tcell.KeyCtrlH:
|
||||
URL(viper.GetString("a-general.home"))
|
||||
return nil
|
||||
case tcell.KeyCtrlQ:
|
||||
Stop()
|
||||
return nil
|
||||
case tcell.KeyRune:
|
||||
// Regular key was sent
|
||||
switch string(event.Rune()) {
|
||||
case " ":
|
||||
// Space starts typing, like Bombadillo
|
||||
bottomBar.SetLabel("[::b]URL: [::-]")
|
||||
bottomBar.SetText("")
|
||||
App.SetFocus(bottomBar)
|
||||
return nil
|
||||
case "q":
|
||||
Stop()
|
||||
return nil
|
||||
case "R":
|
||||
Reload()
|
||||
return nil
|
||||
case "b":
|
||||
histBack()
|
||||
return nil
|
||||
case "f":
|
||||
histForward()
|
||||
return nil
|
||||
case "?":
|
||||
Help()
|
||||
return nil
|
||||
// Shift+NUMBER keys, for switching to a specific tab
|
||||
case "!":
|
||||
SwitchTab(0)
|
||||
return nil
|
||||
case "@":
|
||||
SwitchTab(1)
|
||||
return nil
|
||||
case "#":
|
||||
SwitchTab(2)
|
||||
return nil
|
||||
case "$":
|
||||
SwitchTab(3)
|
||||
return nil
|
||||
case "%":
|
||||
SwitchTab(4)
|
||||
return nil
|
||||
case "^":
|
||||
SwitchTab(5)
|
||||
return nil
|
||||
case "&":
|
||||
SwitchTab(6)
|
||||
return nil
|
||||
case "*":
|
||||
SwitchTab(7)
|
||||
return nil
|
||||
case "(":
|
||||
SwitchTab(8)
|
||||
return nil
|
||||
case ")": // Zero key goes to the last tab
|
||||
SwitchTab(NumTabs() - 1)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return event
|
||||
})
|
||||
}
|
||||
|
||||
// Stop stops the app gracefully.
|
||||
// In the future it will handle things like ongoing downloads, etc
|
||||
func Stop() {
|
||||
App.Stop()
|
||||
}
|
||||
|
||||
// NewTab opens a new tab and switches to it, displaying the
|
||||
// the default empty content because there's no URL.
|
||||
func NewTab() {
|
||||
// Create TextView in tabViews and change curTab
|
||||
// Set the textView options, and the changed func to App.Draw()
|
||||
// SetDoneFunc to do link highlighting
|
||||
// Add view to pages and switch to it
|
||||
|
||||
curTab = NumTabs()
|
||||
tabMap[curTab] = &newTabPage
|
||||
tabViews[curTab] = cview.NewTextView().
|
||||
SetDynamicColors(true).
|
||||
SetRegions(true).
|
||||
SetScrollable(true).
|
||||
SetWrap(false).
|
||||
SetText(renderedNewTabContent).
|
||||
SetChangedFunc(func() {
|
||||
App.Draw()
|
||||
}).
|
||||
SetDoneFunc(func(key tcell.Key) {
|
||||
// Altered from: https://gitlab.com/tslocum/cview/-/blob/master/demos/textview/main.go
|
||||
// Handles being able to select and "click" links with the enter and tab keys
|
||||
|
||||
currentSelection := tabViews[curTab].GetHighlights()
|
||||
numSelections := len(tabMap[curTab].Links)
|
||||
if key == tcell.KeyEnter {
|
||||
if len(currentSelection) > 0 && len(tabMap[curTab].Links) > 0 {
|
||||
// A link was selected, "click" it and load the page it's for
|
||||
linkN, _ := strconv.Atoi(currentSelection[0])
|
||||
followLink(tabMap[curTab].Url, tabMap[curTab].Links[linkN])
|
||||
return
|
||||
} else {
|
||||
tabViews[curTab].Highlight("0").ScrollToHighlight()
|
||||
}
|
||||
} else if len(currentSelection) > 0 {
|
||||
index, _ := strconv.Atoi(currentSelection[0])
|
||||
if key == tcell.KeyTab {
|
||||
index = (index + 1) % numSelections
|
||||
} else if key == tcell.KeyBacktab {
|
||||
index = (index - 1 + numSelections) % numSelections
|
||||
} else {
|
||||
return
|
||||
}
|
||||
tabViews[curTab].Highlight(strconv.Itoa(index)).ScrollToHighlight()
|
||||
}
|
||||
})
|
||||
|
||||
tabHist[curTab] = []string{}
|
||||
// Can't go backwards, but this isn't the first page either.
|
||||
// The first page will be the next one the user goes to.
|
||||
tabHistPos[curTab] = -1
|
||||
|
||||
tabPages.AddAndSwitchToPage(strconv.Itoa(curTab), tabViews[curTab], true)
|
||||
App.SetFocus(tabViews[curTab])
|
||||
|
||||
// Add tab number to the actual place where tabs are show on the screen
|
||||
// Tab regions are 0-indexed but text displayed on the screen starts at 1
|
||||
fmt.Fprintf(tabRow, `["%d"][darkcyan] %d [white][""]|`, curTab, curTab+1)
|
||||
tabRow.Highlight(strconv.Itoa(curTab)).ScrollToHighlight()
|
||||
|
||||
bottomBar.SetLabel("")
|
||||
bottomBar.SetText("")
|
||||
|
||||
// Force a draw, just in case
|
||||
App.Draw()
|
||||
}
|
||||
|
||||
// CloseTab closes the current tab and switches to the one to its left.
|
||||
func CloseTab() {
|
||||
// Basically the NewTab() func inverted
|
||||
|
||||
// TODO: Support closing middle tabs, by renumbering all the maps
|
||||
// So that tabs to the right of the closed tabs point to the right places
|
||||
// For now you can only close the right-most tab
|
||||
if curTab != NumTabs()-1 {
|
||||
return
|
||||
}
|
||||
|
||||
if NumTabs() <= 1 {
|
||||
// There's only one tab open, close the app instead
|
||||
Stop()
|
||||
return
|
||||
}
|
||||
|
||||
delete(tabMap, curTab)
|
||||
tabPages.RemovePage(strconv.Itoa(curTab))
|
||||
delete(tabViews, curTab)
|
||||
|
||||
delete(tabHist, curTab)
|
||||
delete(tabHistPos, curTab)
|
||||
|
||||
if curTab <= 0 {
|
||||
curTab = NumTabs() - 1
|
||||
} else {
|
||||
curTab--
|
||||
}
|
||||
|
||||
tabPages.SwitchToPage(strconv.Itoa(curTab)) // Go to previous page
|
||||
// Rewrite the tab display
|
||||
tabRow.Clear()
|
||||
for i := 0; i < NumTabs(); i++ {
|
||||
fmt.Fprintf(tabRow, `["%d"][darkcyan] %d [white][""]|`, i, i+1)
|
||||
}
|
||||
tabRow.Highlight(strconv.Itoa(curTab)).ScrollToHighlight()
|
||||
|
||||
bottomBar.SetLabel("")
|
||||
bottomBar.SetText(tabMap[curTab].Url)
|
||||
|
||||
// Just in case
|
||||
App.Draw()
|
||||
}
|
||||
|
||||
// SwitchTab switches to a specific tab, using its number, 0-indexed.
|
||||
// The tab numbers are clamped to the end, so for example numbers like -5 and 1000 are still valid.
|
||||
// This means that calling something like SwitchTab(curTab - 1) will never cause an error.
|
||||
func SwitchTab(tab int) {
|
||||
if tab < 0 {
|
||||
tab = 0
|
||||
}
|
||||
if tab > NumTabs()-1 {
|
||||
tab = NumTabs() - 1
|
||||
}
|
||||
|
||||
curTab = tab % NumTabs()
|
||||
tabPages.SwitchToPage(strconv.Itoa(curTab))
|
||||
tabRow.Highlight(strconv.Itoa(curTab)).ScrollToHighlight()
|
||||
|
||||
bottomBar.SetLabel("")
|
||||
bottomBar.SetText(tabMap[curTab].Url)
|
||||
|
||||
// Just in case
|
||||
App.Draw()
|
||||
}
|
||||
|
||||
func Reload() {
|
||||
cache.Remove(tabMap[curTab].Url)
|
||||
go handleURL(tabMap[curTab].Url)
|
||||
}
|
||||
|
||||
// URL loads and handles the provided URL for the current tab.
|
||||
// It should be an absolute URL.
|
||||
func URL(u string) {
|
||||
// Old relative URL handling stuff:
|
||||
// parsed, err := url.Parse(u)
|
||||
// if err != nil {
|
||||
// Error("Bad URL", err.Error())
|
||||
// return
|
||||
// }
|
||||
// if tabHasContent() && parsed.Host == "" {
|
||||
// // Relative link
|
||||
// followLink(tabMap[curTab].Url, u)
|
||||
// return
|
||||
// }
|
||||
|
||||
go func() {
|
||||
final, displayed := handleURL(u)
|
||||
if displayed {
|
||||
addToHist(final)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func NumTabs() int {
|
||||
return len(tabViews)
|
||||
}
|
||||
|
||||
// Help displays the help and keybindings.
|
||||
func Help() {
|
||||
helpTable.ScrollToBeginning()
|
||||
tabPages.SwitchToPage("help")
|
||||
App.Draw()
|
||||
}
|
24
display/help.go
Normal file
24
display/help.go
Normal file
@ -0,0 +1,24 @@
|
||||
package display
|
||||
|
||||
import "strings"
|
||||
|
||||
var helpCells = strings.TrimSpace(`
|
||||
?|Bring up this help.
|
||||
Esc|Leave the help
|
||||
Arrow keys, h/j/k/l|Scroll and move a page.
|
||||
Tab|Navigate to the next item in a popup.
|
||||
Shift-Tab|Navigate to the previous item in a popup.
|
||||
Ctrl-H|Go home
|
||||
Ctrl-T|New tab
|
||||
Ctrl-W|Close tab. For now, only the right-most tab can be closed.
|
||||
b|Go back a page
|
||||
f|Go forward a page
|
||||
g|Go to top of document
|
||||
G|Go to bottom of document
|
||||
spacebar|Open bar at the bottom - type a URL or link number
|
||||
Enter|On a page this will start link highlighting. Press Tab and Shift-Tab to pick different links. Press enter again to go to one.
|
||||
Ctrl-R|Reload a page. This also clears the cache.
|
||||
q, Ctrl-Q|Quit
|
||||
Shift-NUMBER|Go to a specific tab.
|
||||
Shift-0, )|Go to the last tab.
|
||||
`)
|
44
display/history.go
Normal file
44
display/history.go
Normal file
@ -0,0 +1,44 @@
|
||||
package display
|
||||
|
||||
// Tab number mapped to list of URLs ordered from first to most recent.
|
||||
var tabHist = make(map[int][]string)
|
||||
|
||||
// Tab number mapped to where in its history you are.
|
||||
// The value is a valid index of the string slice above.
|
||||
var tabHistPos = make(map[int]int)
|
||||
|
||||
// addToHist adds the given URL to history.
|
||||
// It assumes the URL is currently being loaded and displayed on the page.
|
||||
func addToHist(u string) {
|
||||
if tabHistPos[curTab] < len(tabHist[curTab])-1 {
|
||||
// We're somewhere in the middle of the history instead, with URLs ahead and behind.
|
||||
// The URLs ahead need to be removed so this new URL is the most recent item in the history
|
||||
tabHist[curTab] = tabHist[curTab][:tabHistPos[curTab]+1]
|
||||
}
|
||||
tabHist[curTab] = append(tabHist[curTab], u)
|
||||
tabHistPos[curTab]++
|
||||
}
|
||||
|
||||
func histForward() {
|
||||
if tabHistPos[curTab] >= len(tabHist[curTab])-1 {
|
||||
// Already on the most recent URL in the history
|
||||
return
|
||||
}
|
||||
tabHistPos[curTab]++
|
||||
go func() {
|
||||
handleURL(tabHist[curTab][tabHistPos[curTab]])
|
||||
applyScroll()
|
||||
}()
|
||||
}
|
||||
|
||||
func histBack() {
|
||||
if tabHistPos[curTab] <= 0 {
|
||||
// First tab in history
|
||||
return
|
||||
}
|
||||
tabHistPos[curTab]--
|
||||
go func() {
|
||||
handleURL(tabHist[curTab][tabHistPos[curTab]])
|
||||
applyScroll()
|
||||
}()
|
||||
}
|
156
display/modals.go
Normal file
156
display/modals.go
Normal file
@ -0,0 +1,156 @@
|
||||
package display
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
"gitlab.com/tslocum/cview"
|
||||
)
|
||||
|
||||
// This file contains code for all the popups / modals used in the display
|
||||
|
||||
var infoModal = cview.NewModal().
|
||||
SetBackgroundColor(tcell.ColorGray).
|
||||
SetButtonBackgroundColor(tcell.ColorNavy).
|
||||
SetButtonTextColor(tcell.ColorWhite).
|
||||
SetTextColor(tcell.ColorWhite).
|
||||
AddButtons([]string{"Ok"})
|
||||
|
||||
var errorModal = cview.NewModal().
|
||||
SetBackgroundColor(tcell.ColorMaroon).
|
||||
SetButtonBackgroundColor(tcell.ColorNavy).
|
||||
SetButtonTextColor(tcell.ColorWhite).
|
||||
SetTextColor(tcell.ColorWhite).
|
||||
AddButtons([]string{"Ok"})
|
||||
|
||||
// TODO: Support input
|
||||
var inputModal = cview.NewModal().
|
||||
SetBackgroundColor(tcell.ColorGreen).
|
||||
SetButtonBackgroundColor(tcell.ColorNavy).
|
||||
SetButtonTextColor(tcell.ColorWhite).
|
||||
SetTextColor(tcell.ColorWhite).
|
||||
AddButtons([]string{"Send", "Cancel"})
|
||||
|
||||
var inputCh = make(chan string)
|
||||
var inputModalText string // The current text of the input field in the modal
|
||||
|
||||
var yesNoModal = cview.NewModal().
|
||||
SetBackgroundColor(tcell.ColorPurple).
|
||||
SetButtonBackgroundColor(tcell.ColorNavy).
|
||||
SetButtonTextColor(tcell.ColorWhite).
|
||||
SetTextColor(tcell.ColorWhite).
|
||||
AddButtons([]string{"Yes", "No"})
|
||||
|
||||
// Channel to recieve yesNo answer on
|
||||
var yesNoCh = make(chan bool)
|
||||
|
||||
func modalInit() {
|
||||
// Modal functions that can't be added up above, because they return the wrong type
|
||||
infoModal.SetBorder(true)
|
||||
infoModal.SetBorderColor(tcell.ColorWhite)
|
||||
infoModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||||
tabPages.SwitchToPage(strconv.Itoa(curTab))
|
||||
})
|
||||
|
||||
errorModal.SetBorder(true)
|
||||
errorModal.SetBorderColor(tcell.ColorWhite)
|
||||
errorModal.SetTitleColor(tcell.ColorWhite)
|
||||
errorModal.SetTitleAlign(cview.AlignCenter)
|
||||
errorModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||||
tabPages.SwitchToPage(strconv.Itoa(curTab))
|
||||
})
|
||||
|
||||
inputModal.SetBorder(true)
|
||||
inputModal.SetBorderColor(tcell.ColorWhite)
|
||||
inputModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||||
if buttonLabel == "Send" {
|
||||
inputCh <- inputModalText
|
||||
return
|
||||
}
|
||||
// Empty string indicates no input
|
||||
inputCh <- ""
|
||||
|
||||
//tabPages.SwitchToPage(strconv.Itoa(curTab)) - handled in Input()
|
||||
})
|
||||
|
||||
yesNoModal.SetBorder(true)
|
||||
yesNoModal.SetBorderColor(tcell.ColorWhite)
|
||||
yesNoModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||||
if buttonLabel == "Yes" {
|
||||
yesNoCh <- true
|
||||
return
|
||||
}
|
||||
yesNoCh <- false
|
||||
|
||||
//tabPages.SwitchToPage(strconv.Itoa(curTab)) - Handled in YesNo()
|
||||
})
|
||||
}
|
||||
|
||||
// Error displays an error on the screen in a modal.
|
||||
func Error(title, text string) {
|
||||
// Capitalize and add period if necessary - because most errors don't do that
|
||||
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
|
||||
title = " " + strings.TrimSpace(title) + " "
|
||||
|
||||
errorModal.SetTitle(title)
|
||||
errorModal.SetText(text)
|
||||
tabPages.ShowPage("error")
|
||||
tabPages.SendToFront("error")
|
||||
App.SetFocus(errorModal)
|
||||
App.Draw()
|
||||
}
|
||||
|
||||
// Info displays some info on the screen in a modal.
|
||||
func Info(s string) {
|
||||
infoModal.SetText(s)
|
||||
tabPages.ShowPage("info")
|
||||
tabPages.SendToFront("info")
|
||||
App.SetFocus(infoModal)
|
||||
App.Draw()
|
||||
}
|
||||
|
||||
// Input pulls up a modal that asks for input, and returns the user's input.
|
||||
// It returns an bool indicating if the user chose to send input or not.
|
||||
func Input(prompt string) (string, bool) {
|
||||
// Remove and re-add input field - to clear the old text
|
||||
if inputModal.GetForm().GetFormItemCount() > 0 {
|
||||
inputModal.GetForm().RemoveFormItem(0)
|
||||
}
|
||||
inputModalText = ""
|
||||
inputModal.GetForm().AddInputField("", "", 0, nil,
|
||||
func(text string) {
|
||||
// Store for use later
|
||||
inputModalText = text
|
||||
})
|
||||
|
||||
inputModal.SetText(prompt)
|
||||
tabPages.ShowPage("input")
|
||||
tabPages.SendToFront("input")
|
||||
App.SetFocus(inputModal)
|
||||
App.Draw()
|
||||
|
||||
resp := <-inputCh
|
||||
tabPages.SwitchToPage(strconv.Itoa(curTab))
|
||||
if resp == "" {
|
||||
return "", false
|
||||
}
|
||||
return resp, true
|
||||
}
|
||||
|
||||
// YesNo displays a modal asking a yes-or-no question.
|
||||
func YesNo(prompt string) bool {
|
||||
yesNoModal.SetText(prompt)
|
||||
tabPages.ShowPage("yesno")
|
||||
tabPages.SendToFront("yesno")
|
||||
App.SetFocus(yesNoModal)
|
||||
App.Draw()
|
||||
|
||||
resp := <-yesNoCh
|
||||
tabPages.SwitchToPage(strconv.Itoa(curTab))
|
||||
return resp
|
||||
}
|
13
display/newtab.go
Normal file
13
display/newtab.go
Normal file
@ -0,0 +1,13 @@
|
||||
package display
|
||||
|
||||
var newTabContent = `# New Tab
|
||||
|
||||
You've opened a new tab. Use the bar at the bottom to browse around. You can start typing in it by pressing the space key, or clicking the bottom bar.
|
||||
|
||||
Press the ? key at any time to bring up the help, and see other keybindings. Most are what you expect, you can use h/j/k/l and the arrow keys to move around text, as well as scrolling with the mouse. Common browsers shortcuts like Ctrl-T, Ctrl-W, and Ctrl-F are also supported.
|
||||
|
||||
Happy browsing!
|
||||
|
||||
=> //gemini.circumlunar.space Gemini homepage
|
||||
=> https://github.com/makeworld-the-better-one/amfora Amfora homepage [HTTPS]
|
||||
`
|
281
display/private.go
Normal file
281
display/private.go
Normal file
@ -0,0 +1,281 @@
|
||||
package display
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/makeworld-the-better-one/amfora/cache"
|
||||
"github.com/makeworld-the-better-one/amfora/client"
|
||||
"github.com/makeworld-the-better-one/amfora/renderer"
|
||||
"github.com/makeworld-the-better-one/amfora/structs"
|
||||
"github.com/makeworld-the-better-one/amfora/webbrowser"
|
||||
"github.com/makeworld-the-better-one/go-gemini"
|
||||
"github.com/spf13/viper"
|
||||
"gitlab.com/tslocum/cview"
|
||||
//"github.com/makeworld-the-better-one/amfora/cview"
|
||||
)
|
||||
|
||||
// This file contains the functions that aren't part of the public API.
|
||||
|
||||
// tabHasContent returns true when the current tab has a page being displayed.
|
||||
// The most likely situation where false would be returned is when the default
|
||||
// new tab content is being displayed.
|
||||
func tabHasContent() bool {
|
||||
if curTab < 0 {
|
||||
return false
|
||||
}
|
||||
if len(tabViews) < curTab {
|
||||
// There isn't a TextView for the current tab number
|
||||
return false
|
||||
}
|
||||
if tabMap[curTab].Url == "" {
|
||||
// Likely the default content page
|
||||
return false
|
||||
}
|
||||
_, ok := tabMap[curTab]
|
||||
return ok // If there's a page, return true
|
||||
}
|
||||
|
||||
// saveScroll saves where in the page the user was.
|
||||
// It should be used whenever moving from one page to another.
|
||||
func saveScroll() {
|
||||
// It will also be saved in the cache because the cache uses the same pointer
|
||||
row, col := tabViews[curTab].GetScrollOffset()
|
||||
tabMap[curTab].Row = row
|
||||
tabMap[curTab].Column = col
|
||||
}
|
||||
|
||||
// applyScroll applies the saved scroll values to the current page and tab.
|
||||
// It should only be used when going backward and forward, not when
|
||||
// loading a new page (that might have scroll vals cached anyway).
|
||||
func applyScroll() {
|
||||
tabViews[curTab].ScrollTo(tabMap[curTab].Row, tabMap[curTab].Column)
|
||||
}
|
||||
|
||||
// followLink should be used when the user "clicks" a link on a page.
|
||||
// Not when a URL is opened on a new tab for the first time.
|
||||
func followLink(prev, next string) {
|
||||
saveScroll() // Likely called later on anyway, here just in case
|
||||
prevParsed, _ := url.Parse(prev)
|
||||
nextParsed, err := url.Parse(next)
|
||||
if err != nil {
|
||||
Error("Error", "Link URL could not be parsed")
|
||||
return
|
||||
}
|
||||
nextURL := prevParsed.ResolveReference(nextParsed).String()
|
||||
go func() {
|
||||
final, displayed := handleURL(nextURL)
|
||||
if displayed {
|
||||
addToHist(final)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// setPage displays a Page on the current tab.
|
||||
func setPage(p *structs.Page) {
|
||||
saveScroll() // Save the scroll of the previous page
|
||||
|
||||
// Change page
|
||||
tabMap[curTab] = p
|
||||
tabViews[curTab].SetText(p.Content)
|
||||
tabViews[curTab].Highlight("") // Turn off highlights
|
||||
tabViews[curTab].ScrollToBeginning()
|
||||
|
||||
// Setup display
|
||||
App.SetFocus(tabViews[curTab])
|
||||
bottomBar.SetLabel("")
|
||||
bottomBar.SetText(p.Url)
|
||||
}
|
||||
|
||||
// handleURL displays whatever action is needed for the provided URL,
|
||||
// and applies it to the current tab.
|
||||
// It loads documents, handles errors, brings up a download prompt, etc.
|
||||
//
|
||||
// The string returned is the final URL, if redirects were involved.
|
||||
// In most cases it will be the same as the passed URL.
|
||||
// If there is some error, it will return "".
|
||||
// The second returned item is a bool indicating if page content was displayed.
|
||||
// It returns false for Errors, other protocols, etc.
|
||||
func handleURL(u string) (string, bool) {
|
||||
defer App.Draw() // Make sure modals get displayed
|
||||
|
||||
//logger.Log.Printf("Sent: %s", u)
|
||||
|
||||
u = normalizeURL(u)
|
||||
|
||||
//logger.Log.Printf("Normalized: %s", u)
|
||||
|
||||
parsed, err := url.Parse(u)
|
||||
if err != nil {
|
||||
Error("URL Error", err.Error())
|
||||
return "", false
|
||||
}
|
||||
|
||||
if strings.HasPrefix(u, "http") {
|
||||
switch strings.TrimSpace(viper.GetString("a-general.http")) {
|
||||
case "", "off":
|
||||
Error("Error", "Opening HTTP URLs is turned off.")
|
||||
case "default":
|
||||
s, err := webbrowser.Open(u)
|
||||
if err != nil {
|
||||
Error("Error", err.Error())
|
||||
} else {
|
||||
Info(s)
|
||||
}
|
||||
default:
|
||||
// The config has a custom command to execute for HTTP URLs
|
||||
fields := strings.Fields(viper.GetString("a-general.http"))
|
||||
err := exec.Command(fields[0], append(fields[1:], u)...).Start()
|
||||
if err != nil {
|
||||
Error("Error", err.Error())
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
if !strings.HasPrefix(u, "gemini") {
|
||||
// TODO: Replace it with with displaying the URL, once modal titles work
|
||||
Error("Error", "Unsupported protocol, only [::b]gemini[::-] and [::b]http[::-] are supported.")
|
||||
return "", false
|
||||
}
|
||||
// Gemini URL
|
||||
|
||||
// Load page from cache if possible
|
||||
page, ok := cache.Get(u)
|
||||
if ok {
|
||||
setPage(page)
|
||||
return u, true
|
||||
}
|
||||
// Otherwise download it
|
||||
bottomBar.SetText("Loading...")
|
||||
App.Draw()
|
||||
|
||||
res, err := client.Fetch(u)
|
||||
if err != nil {
|
||||
Error("Error", err.Error())
|
||||
// Set the bar back to original URL
|
||||
bottomBar.SetText(tabMap[curTab].Url)
|
||||
return "", false
|
||||
}
|
||||
if renderer.CanDisplay(res) {
|
||||
page, err := renderer.MakePage(u, res)
|
||||
if err != nil {
|
||||
Error("Error", "Issuing creating page: "+err.Error())
|
||||
// Set the bar back to original URL
|
||||
bottomBar.SetText(tabMap[curTab].Url)
|
||||
return "", false
|
||||
}
|
||||
cache.Add(page)
|
||||
setPage(page)
|
||||
return u, true
|
||||
}
|
||||
// Not displayable
|
||||
// Could be a non 20 (or 21) status code, or a different kind of document
|
||||
|
||||
// Set the bar back to original URL
|
||||
bottomBar.SetText(tabMap[curTab].Url)
|
||||
App.Draw()
|
||||
|
||||
// Handle each status code
|
||||
switch gemini.SimplifyStatus(res.Status) {
|
||||
case 10:
|
||||
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 = strings.ReplaceAll(url.PathEscape(userInput), "+", "%2B")
|
||||
return handleURL(parsed.String())
|
||||
}
|
||||
return "", false
|
||||
case 30:
|
||||
parsedMeta, err := url.Parse(res.Meta)
|
||||
if err != nil {
|
||||
Error("Redirect Error", "Invalid URL: "+err.Error())
|
||||
return "", false
|
||||
}
|
||||
redir := parsed.ResolveReference(parsedMeta).String()
|
||||
|
||||
if YesNo("Follow redirect?\n" + redir) {
|
||||
return handleURL(redir)
|
||||
}
|
||||
return "", false
|
||||
case 40:
|
||||
Error("Temporary Failure", cview.Escape(res.Meta)) // Escaped just in case, to not allow malicious meta strings
|
||||
return "", false
|
||||
case 50:
|
||||
Error("Permanent Failure", cview.Escape(res.Meta))
|
||||
return "", false
|
||||
case 60:
|
||||
Info("The server requested a certificate. Cert handling is coming to Amfora soon!")
|
||||
return "", false
|
||||
}
|
||||
// Status code 20, but not a document that can be displayed
|
||||
yes := YesNo("This type of file can't be displayed. Downloading will be implemented soon. Would like to open the file in a HTTPS proxy for now?")
|
||||
if yes {
|
||||
// Open in mozz's proxy
|
||||
portalURL := u
|
||||
if parsed.RawQuery != "" {
|
||||
// Remove query and add encoded version on the end
|
||||
query := parsed.RawQuery
|
||||
parsed.RawQuery = ""
|
||||
portalURL = parsed.String() + "%3F" + query
|
||||
}
|
||||
portalURL = strings.TrimPrefix(portalURL, "gemini://") + "?raw=1"
|
||||
|
||||
s, err := webbrowser.Open("https://portal.mozz.us/gemini/" + portalURL)
|
||||
if err != nil {
|
||||
Error("Error", err.Error())
|
||||
} else {
|
||||
Info(s)
|
||||
}
|
||||
App.Draw()
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// normalizeURL attempts to make URLs that are different strings
|
||||
// but point to the same place all look the same.
|
||||
//
|
||||
// Example: gemini://gus.guru:1965/ and //gus.guru/.
|
||||
// This function will take both output the same URL each time.
|
||||
//
|
||||
// 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)
|
||||
if err != nil {
|
||||
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 == "" {
|
||||
// Always add scheme
|
||||
parsed.Scheme = "gemini"
|
||||
} else if parsed.Scheme != "gemini" {
|
||||
// Not a gemini URL, nothing to do
|
||||
return u
|
||||
}
|
||||
|
||||
parsed.User = nil // No passwords in Gemini
|
||||
parsed.Fragment = "" // No fragments either
|
||||
if parsed.Port() == "1965" {
|
||||
// Always remove default port
|
||||
parsed.Host = parsed.Hostname()
|
||||
}
|
||||
|
||||
// Add slash to the end of a URL with just a domain
|
||||
// gemini://example.com -> gemini://example.com/
|
||||
if parsed.Path == "" {
|
||||
parsed.Path = "/"
|
||||
}
|
||||
|
||||
return parsed.String()
|
||||
}
|
21
go.mod
Normal file
21
go.mod
Normal file
@ -0,0 +1,21 @@
|
||||
module github.com/makeworld-the-better-one/amfora
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
||||
github.com/gdamore/tcell v1.3.1-0.20200608133353-cb1e5d6fa606
|
||||
github.com/makeworld-the-better-one/go-gemini v0.5.0
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/mitchellh/mapstructure v1.3.1 // indirect
|
||||
github.com/pelletier/go-toml v1.8.0 // indirect
|
||||
github.com/spf13/afero v1.2.2 // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.7.0
|
||||
github.com/stretchr/testify v1.6.0
|
||||
gitlab.com/tslocum/cview v1.4.8-0.20200614211415-f477be8ba472
|
||||
gopkg.in/ini.v1 v1.57.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20200603094226-e3079894b1e8 // indirect
|
||||
)
|
389
go.sum
Normal file
389
go.sum
Normal file
@ -0,0 +1,389 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell v1.3.0 h1:r35w0JBADPZCVQijYebl6YMWWtHRqVEGt7kL2eBADRM=
|
||||
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
|
||||
github.com/gdamore/tcell v1.3.1-0.20200608133353-cb1e5d6fa606 h1:Y00kKKKYVyn7InlCMRcnZbwcjHFIsgkjU0Bn1F5re4o=
|
||||
github.com/gdamore/tcell v1.3.1-0.20200608133353-cb1e5d6fa606/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/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.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/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-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
|
||||
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
||||
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.3.1 h1:Vx1+loZ2MfWPC0qd+vEFaIcxmWJoagz+rFxX5uZxeBw=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.3.1/go.mod h1:P7/FbZ+IEIbA/d+A0Y3w2GNgD8SA2AcNv7aDGJbaWG4=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.3.2-0.20200614183147-32dd6eceb1d0 h1:Kz6VOurTGz4lDLBy3Vd5Gak+e0khPEwSDDj4x0SegvI=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.3.2-0.20200614183147-32dd6eceb1d0/go.mod h1:P7/FbZ+IEIbA/d+A0Y3w2GNgD8SA2AcNv7aDGJbaWG4=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.4.0 h1:UYGqvCMedgV74Qro8pLVGYJ3LN7gK6uC6GNhw1bqRiM=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.4.0/go.mod h1:P7/FbZ+IEIbA/d+A0Y3w2GNgD8SA2AcNv7aDGJbaWG4=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.4.1-0.20200616003826-382c5f0c0ef3 h1:H2kArUN0jFSQ/3RI3KhE10tOIQAkKtLrI4kMK3sPccM=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.4.1-0.20200616003826-382c5f0c0ef3/go.mod h1:P7/FbZ+IEIbA/d+A0Y3w2GNgD8SA2AcNv7aDGJbaWG4=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.4.1-0.20200616230531-9d7ac4323c2b h1:JmlaXeOwDaV0S5eBGaUjt647H+9KT274EruXuZ00ceM=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.4.1-0.20200616230531-9d7ac4323c2b/go.mod h1:P7/FbZ+IEIbA/d+A0Y3w2GNgD8SA2AcNv7aDGJbaWG4=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.4.1-0.20200616232915-ed61139d1eee h1:a17hCyK0fDzsXetLcoNRJpITbDewevIMziE9AEdiqC0=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.4.1-0.20200616232915-ed61139d1eee/go.mod h1:P7/FbZ+IEIbA/d+A0Y3w2GNgD8SA2AcNv7aDGJbaWG4=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.4.1-0.20200616233000-90c00ecba439 h1:lGpF/Y98k54eewpHabpFiXNNUyLZXhzKNf8ENIPE9XE=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.4.1-0.20200616233000-90c00ecba439/go.mod h1:P7/FbZ+IEIbA/d+A0Y3w2GNgD8SA2AcNv7aDGJbaWG4=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.4.1-0.20200617040701-61e0b380d100 h1:QBeer60NxocMspaK15+DWZCIzIJ7qjyRAcCIZ629bVc=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.4.1-0.20200617040701-61e0b380d100/go.mod h1:P7/FbZ+IEIbA/d+A0Y3w2GNgD8SA2AcNv7aDGJbaWG4=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.4.1-0.20200617041401-3154e68a3755 h1:A19eOhFAZ/RvVmSTkpghF34vsvUGDIENkFKjcfQ0e8w=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.4.1-0.20200617041401-3154e68a3755/go.mod h1:P7/FbZ+IEIbA/d+A0Y3w2GNgD8SA2AcNv7aDGJbaWG4=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.4.1 h1:JZPllyrKY0IE0Ts88rMEpvqmnjAb77xhlYnafbunTH0=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.4.1/go.mod h1:P7/FbZ+IEIbA/d+A0Y3w2GNgD8SA2AcNv7aDGJbaWG4=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.4.2-0.20200617144014-0b132b84d585 h1:qvr3wgkbgcbxStfNb49B+CDFtiTiUCBLGvyuWHfTSQ0=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.4.2-0.20200617144014-0b132b84d585/go.mod h1:P7/FbZ+IEIbA/d+A0Y3w2GNgD8SA2AcNv7aDGJbaWG4=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.5.0 h1:M/Adz5Xf7rsL59tgD7+uFIpLvVspuEhwgVyr2EmhrxQ=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.5.0/go.mod h1:P7/FbZ+IEIbA/d+A0Y3w2GNgD8SA2AcNv7aDGJbaWG4=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.5.1-0.20200618180334-440c780d8473 h1:7zgnlrKbhVUE06053XzA72zO1/rE9hBlXMAg48D8p90=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.5.1-0.20200618180334-440c780d8473/go.mod h1:P7/FbZ+IEIbA/d+A0Y3w2GNgD8SA2AcNv7aDGJbaWG4=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.5.1-0.20200618180814-9209e8a23cf3 h1:+JZQRDQeNT5yOFAemYmNeck8a32gBKA9S/iYVqD611M=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.5.1-0.20200618180814-9209e8a23cf3/go.mod h1:P7/FbZ+IEIbA/d+A0Y3w2GNgD8SA2AcNv7aDGJbaWG4=
|
||||
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-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=
|
||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.3.1 h1:cCBH2gTD2K0OtLlv/Y5H01VQCqmlDxz30kS5Y5bqfLA=
|
||||
github.com/mitchellh/mapstructure v1.3.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.8.0 h1:Keo9qb7iRJs2voHvunFtuuYFsbWeOBh8/P9v/kVMFtw=
|
||||
github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
|
||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM=
|
||||
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho=
|
||||
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
gitlab.com/tslocum/cbind v0.1.1 h1:JXXtxMWHgWLvoF+QkrvcNvOQ59juy7OE1RhT7hZfdt0=
|
||||
gitlab.com/tslocum/cbind v0.1.1/go.mod h1:rX7vkl0pUSg/yy427MmD1FZAf99S7WwpUlxF/qTpPqk=
|
||||
gitlab.com/tslocum/cview v1.4.8-0.20200609225128-e0fafcdb01b7 h1:gHN0ESC32n+Qd1gs0pwHeNCB8o4ETHTd06zt2wh/wyo=
|
||||
gitlab.com/tslocum/cview v1.4.8-0.20200609225128-e0fafcdb01b7/go.mod h1:eHzQzCnul21ODOS/wTnYJs6d3lkVYAv2/KRo+5wPsh0=
|
||||
gitlab.com/tslocum/cview v1.4.8-0.20200614211415-f477be8ba472 h1:lvLn/TWWgqG1gJAd1a8DOSPgkrQEWaMg+AQhM5/PdzY=
|
||||
gitlab.com/tslocum/cview v1.4.8-0.20200614211415-f477be8ba472/go.mod h1:QctoEJaR2AqZTy0KKo12P1ZjHgQJyVkAXaeDanBBhlE=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
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/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=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
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=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc=
|
||||
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-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 h1:OjiUf46hAmXblsZdnoSXsEUSKU8r1UEzcL5RVZ4gO9Y=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/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/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 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
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=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
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.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
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/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
|
||||
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/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200603094226-e3079894b1e8 h1:jL/vaozO53FMfZLySWM+4nulF3gQEC6q5jH90LPomDo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200603094226-e3079894b1e8/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
20
logger/logger.go
Normal file
20
logger/logger.go
Normal file
@ -0,0 +1,20 @@
|
||||
package logger
|
||||
|
||||
// For debugging
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
var Log *log.Logger
|
||||
|
||||
func Init() error {
|
||||
f, err := os.Create("debug.log")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
Log = log.New(f, "", log.LstdFlags)
|
||||
Log.Println("Started Log")
|
||||
return nil
|
||||
}
|
238
renderer/renderer.go
Normal file
238
renderer/renderer.go
Normal file
@ -0,0 +1,238 @@
|
||||
// Package renderer provides functions to convert various data into a cview primitive.
|
||||
// Example objects include a Gemini response, and an error.
|
||||
//
|
||||
// Rendered lines always end with \r\n, in an effort to be Window compatible.
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
urlPkg "net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/makeworld-the-better-one/amfora/config"
|
||||
"github.com/makeworld-the-better-one/amfora/structs"
|
||||
"github.com/makeworld-the-better-one/go-gemini"
|
||||
"github.com/spf13/viper"
|
||||
"gitlab.com/tslocum/cview"
|
||||
)
|
||||
|
||||
// CanDisplay returns true if the response is supported by Amfora
|
||||
// for displaying on the screen.
|
||||
// It also doubles as a function to detect whether something can be stored in a Page struct.
|
||||
func CanDisplay(res *gemini.Response) bool {
|
||||
if gemini.SimplifyStatus(res.Status) != 20 {
|
||||
// No content
|
||||
return false
|
||||
}
|
||||
mediatype, params, err := mime.ParseMediaType(res.Meta)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if strings.ToLower(params["charset"]) != "utf-8" && strings.ToLower(params["charset"]) != "us-ascii" && params["charset"] != "" {
|
||||
// Amfora doesn't support other charsets
|
||||
return false
|
||||
}
|
||||
if mediatype != "text/gemini" && mediatype != "text/plain" {
|
||||
// Amfora doesn't support other filetypes
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// convertRegularGemini converts non-preformatted blocks of text/gemini
|
||||
// into a cview-compatible format.
|
||||
// It also returns a slice of link URLs.
|
||||
// numLinks is the number of links that exist so far.
|
||||
//
|
||||
// Since this only works on non-preformatted blocks, renderGemini
|
||||
// should always be used instead.
|
||||
//
|
||||
// TODO: Style cross-protocol links differently
|
||||
//
|
||||
func convertRegularGemini(s string, numLinks int) (string, []string) {
|
||||
links := make([]string, 0)
|
||||
lines := strings.Split(s, "\n")
|
||||
wrappedLines := make([]string, 0) // Final result
|
||||
|
||||
for i := range lines {
|
||||
lines[i] = strings.TrimRight(lines[i], " \r\t\n")
|
||||
|
||||
if strings.HasPrefix(lines[i], "#") && viper.GetBool("a-general.color") {
|
||||
|
||||
// Headings
|
||||
if strings.HasPrefix(lines[i], "###") {
|
||||
lines[i] = "[fuchsia::b]" + lines[i] + "[-::-]"
|
||||
}
|
||||
if strings.HasPrefix(lines[i], "##") {
|
||||
lines[i] = "[lime::b]" + lines[i] + "[-::-]"
|
||||
}
|
||||
if strings.HasPrefix(lines[i], "#") {
|
||||
lines[i] = "[red::b]" + lines[i] + "[-::-]"
|
||||
}
|
||||
|
||||
// Links
|
||||
} else if strings.HasPrefix(lines[i], "=>") && len([]rune(lines[i])) >= 3 {
|
||||
// Trim whitespace and separate link from link text
|
||||
|
||||
lines[i] = strings.Trim(lines[i][2:], " \t") // Remove `=>` part too
|
||||
delim := strings.IndexAny(lines[i], " \t") // Whitespace between link and link text
|
||||
|
||||
var url string
|
||||
var linkText string
|
||||
if delim == -1 {
|
||||
// No link text
|
||||
url = lines[i]
|
||||
linkText = url
|
||||
} else {
|
||||
// There is link text
|
||||
url = lines[i][:delim]
|
||||
linkText = strings.Trim(lines[i][delim:], " \t")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(lines[i]) == "" || strings.TrimSpace(url) == "" {
|
||||
// Link was just whitespace, reset it and move on
|
||||
lines[i] = "=>"
|
||||
wrappedLines = append(wrappedLines, lines[i])
|
||||
continue
|
||||
}
|
||||
|
||||
links = append(links, url)
|
||||
|
||||
if viper.GetBool("a-general.color") {
|
||||
if pU, _ := urlPkg.Parse(url); pU.Scheme == "" || pU.Scheme == "gemini" {
|
||||
// A gemini link
|
||||
// Add the link text in blue (in a region), and a gray link number to the left of it
|
||||
lines[i] = `[silver::b][` + strconv.Itoa(numLinks+len(links)) + "[]" + "[-::-] " +
|
||||
`[dodgerblue]["` + strconv.Itoa(numLinks+len(links)-1) + `"]` + linkText + `[""][-]`
|
||||
} else {
|
||||
// Not a gemini link, use purple instead
|
||||
lines[i] = `[silver::b][` + strconv.Itoa(numLinks+len(links)) + "[]" + "[-::-] " +
|
||||
`[#8700d7]["` + strconv.Itoa(numLinks+len(links)-1) + `"]` + linkText + `[""][-]`
|
||||
}
|
||||
} else {
|
||||
// No colours allowed
|
||||
lines[i] = `[::b][` + strconv.Itoa(numLinks+len(links)) + "[] " +
|
||||
`["` + strconv.Itoa(numLinks+len(links)-1) + `"]` + linkText + `[""][-]`
|
||||
}
|
||||
|
||||
// Lists
|
||||
} else if strings.HasPrefix(lines[i], "* ") {
|
||||
if viper.GetBool("a-general.bullets") {
|
||||
lines[i] = " 🞄" + lines[i][1:]
|
||||
}
|
||||
// Optionally list lines could be colored here too, if color is enabled
|
||||
}
|
||||
|
||||
// Final processing of each line: wrapping
|
||||
|
||||
if strings.TrimSpace(lines[i]) == "" {
|
||||
// Just add empty line without processing
|
||||
wrappedLines = append(wrappedLines, "")
|
||||
} else {
|
||||
if (strings.HasPrefix(lines[i], "[silver::b]") && viper.GetBool("a-general.color")) || strings.HasPrefix(lines[i], "[::b]") {
|
||||
// It's a link line, so don't wrap it
|
||||
wrappedLines = append(wrappedLines, lines[i])
|
||||
} else if strings.HasPrefix(lines[i], ">") {
|
||||
// It's a quote line, add extra quote symbols 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], " ")
|
||||
|
||||
temp := cview.WordWrap(lines[i], config.GetWrapWidth())
|
||||
for i := range temp {
|
||||
temp[i] = "> " + temp[i]
|
||||
}
|
||||
wrappedLines = append(wrappedLines, temp...)
|
||||
} else {
|
||||
wrappedLines = append(wrappedLines, cview.WordWrap(lines[i], config.GetWrapWidth())...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(wrappedLines, "\r\n"), links
|
||||
}
|
||||
|
||||
// renderGemini converts text/gemini into a cview displayable format.
|
||||
// It also returns a slice of link URLs.
|
||||
func RenderGemini(s string) (string, []string) {
|
||||
s = cview.Escape(s)
|
||||
if viper.GetBool("a-general.color") {
|
||||
s = cview.TranslateANSI(s)
|
||||
}
|
||||
lines := strings.Split(s, "\n")
|
||||
|
||||
links := make([]string, 0)
|
||||
|
||||
// Process and wrap non preformatted lines
|
||||
rendered := "" // Final result
|
||||
pre := false
|
||||
buf := "" // Block of regular or preformatted lines
|
||||
for i := range lines {
|
||||
if strings.HasPrefix(lines[i], "```") {
|
||||
if pre {
|
||||
// In a preformatted block, so add the text as is
|
||||
// Don't add the current line with backticks
|
||||
rendered += buf
|
||||
} else {
|
||||
// Not preformatted, regular text
|
||||
ren, lks := convertRegularGemini(buf, len(links))
|
||||
links = append(links, lks...)
|
||||
rendered += ren
|
||||
}
|
||||
buf = "" // Clear buffer for next block
|
||||
pre = !pre
|
||||
continue
|
||||
}
|
||||
// Lines always end with \r\n for Windows compatibility
|
||||
buf += strings.TrimSuffix(lines[i], "\r") + "\r\n"
|
||||
}
|
||||
// Gone through all the lines, but there still is likely a block in the buffer
|
||||
if pre {
|
||||
// File ended without closing the preformatted block
|
||||
rendered += buf
|
||||
} else {
|
||||
// Not preformatted, regular text
|
||||
// Same code as in the loop above
|
||||
ren, lks := convertRegularGemini(buf, len(links))
|
||||
links = append(links, lks...)
|
||||
rendered += ren
|
||||
}
|
||||
|
||||
return rendered, links
|
||||
}
|
||||
|
||||
func MakePage(url string, res *gemini.Response) (*structs.Page, error) {
|
||||
if !CanDisplay(res) {
|
||||
return nil, errors.New("not valid content for a Page")
|
||||
}
|
||||
|
||||
content, err := ioutil.ReadAll(res.Body) // TODO: Don't use all memory on large pages
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.Body.Close()
|
||||
|
||||
mediatype, _, _ := mime.ParseMediaType(res.Meta)
|
||||
|
||||
if mediatype == "text/plain" {
|
||||
return &structs.Page{
|
||||
Url: url,
|
||||
Content: string(content),
|
||||
Links: []string{}, // Plaintext has no links
|
||||
}, nil
|
||||
}
|
||||
if mediatype == "text/gemini" {
|
||||
rendered, links := RenderGemini(string(content))
|
||||
return &structs.Page{
|
||||
Url: url,
|
||||
Content: rendered,
|
||||
Links: links,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("displayable mediatype is not handled in the code, implementation error")
|
||||
}
|
19
structs/structs.go
Normal file
19
structs/structs.go
Normal file
@ -0,0 +1,19 @@
|
||||
package structs
|
||||
|
||||
// Page is for storing UTF-8 text/gemini pages, as well as text/plain pages.
|
||||
type Page struct {
|
||||
Url string
|
||||
Content string // The processed content, NOT raw. Uses cview colour tags. All link/link texts must have region tags.
|
||||
Links []string // URLs, for each region in the content.
|
||||
Row int // Scroll position
|
||||
Column int
|
||||
}
|
||||
|
||||
// Size returns an approx. size of a Page in bytes.
|
||||
func (p *Page) Size() int {
|
||||
b := len(p.Content) + len(p.Url)
|
||||
for i := range p.Links {
|
||||
b += len(p.Links[i])
|
||||
}
|
||||
return b
|
||||
}
|
16
structs/structs_test.go
Normal file
16
structs/structs_test.go
Normal file
@ -0,0 +1,16 @@
|
||||
package structs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSize(t *testing.T) {
|
||||
p := Page{
|
||||
Url: "12345",
|
||||
Content: "12345",
|
||||
Links: []string{"1", "2", "3", "4", "5"},
|
||||
}
|
||||
assert.Equal(t, 15, p.Size(), "sizes should be equal")
|
||||
}
|
5
webbrowser/README.md
Normal file
5
webbrowser/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# `package webbrowser`
|
||||
|
||||
The code in this folder is adapted from Bombadillo, you can see the original [here](https://tildegit.org/sloum/bombadillo/src/branch/master/http). Many thanks to Sloum and the rest of the team!
|
||||
|
||||
The code is simple, and I have changed it, but in any case there should be no licensing issues because both repos are under GPL v3.
|
13
webbrowser/open_browser_darwin.go
Normal file
13
webbrowser/open_browser_darwin.go
Normal file
@ -0,0 +1,13 @@
|
||||
// +build darwin
|
||||
|
||||
package webbrowser
|
||||
|
||||
import "os/exec"
|
||||
|
||||
func Open(url string) (string, error) {
|
||||
err := exec.Command("open", url).Start()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "Opened in system default web browser", nil
|
||||
}
|
9
webbrowser/open_browser_other.go
Normal file
9
webbrowser/open_browser_other.go
Normal file
@ -0,0 +1,9 @@
|
||||
// +build !linux,!darwin,!windows,!freebsd,!netbsd,!openbsd
|
||||
|
||||
package webbrowser
|
||||
|
||||
import "fmt"
|
||||
|
||||
func Open(url string) (string, error) {
|
||||
return "", fmt.Errorf("unsupported os for default HTTP handling. Set a command in the config")
|
||||
}
|
34
webbrowser/open_browser_unix.go
Normal file
34
webbrowser/open_browser_unix.go
Normal file
@ -0,0 +1,34 @@
|
||||
// +build linux freebsd netbsd openbsd
|
||||
|
||||
package webbrowser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// OpenInBrowser checks for the presence of a display server
|
||||
// and environment variables indicating a gui is present. If found
|
||||
// then xdg-open is called on a url to open said url in the default
|
||||
// gui web browser for the system
|
||||
func Open(url string) (string, error) {
|
||||
disp := os.Getenv("DISPLAY")
|
||||
wayland := os.Getenv("WAYLAND_DISPLAY")
|
||||
_, err := exec.LookPath("Xorg")
|
||||
if disp == "" && wayland == "" && err != nil {
|
||||
return "", fmt.Errorf("no gui is available")
|
||||
}
|
||||
|
||||
_, err = exec.LookPath("xdg-open")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("xdg-open command not found, cannot open in web browser")
|
||||
}
|
||||
// Use start rather than run or output in order
|
||||
// to release the process and not block
|
||||
err = exec.Command("xdg-open", url).Start()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "Opened in system default web browser", nil
|
||||
}
|
14
webbrowser/open_browser_windows.go
Normal file
14
webbrowser/open_browser_windows.go
Normal file
@ -0,0 +1,14 @@
|
||||
// +build windows
|
||||
// +build !linux !darwin !freebsd !netbsd !openbsd
|
||||
|
||||
package webbrowser
|
||||
|
||||
import "os/exec"
|
||||
|
||||
func Open(url string) (string, error) {
|
||||
err := exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "Opened in system default web browser", nil
|
||||
}
|
Loading…
Reference in New Issue
Block a user