amfora-1.10.0/ 0000755 0001750 0001750 00000000000 14575704331 012375 5 ustar nilesh nilesh amfora-1.10.0/display/ 0000755 0001750 0001750 00000000000 14575704331 014042 5 ustar nilesh nilesh amfora-1.10.0/display/handlers.go 0000644 0001750 0001750 00000034644 14575704331 016204 0 ustar nilesh nilesh package display
import (
"errors"
"fmt"
"mime"
"net"
"net/url"
"os/exec"
"path"
"strings"
"github.com/makeworld-the-better-one/amfora/cache"
"github.com/makeworld-the-better-one/amfora/client"
"github.com/makeworld-the-better-one/amfora/config"
"github.com/makeworld-the-better-one/amfora/renderer"
"github.com/makeworld-the-better-one/amfora/structs"
"github.com/makeworld-the-better-one/amfora/subscriptions"
"github.com/makeworld-the-better-one/amfora/sysopen"
"github.com/makeworld-the-better-one/amfora/webbrowser"
"github.com/makeworld-the-better-one/go-gemini"
"github.com/makeworld-the-better-one/rr"
"github.com/spf13/viper"
)
// handleHTTP is used by handleURL.
// It opens HTTP links and displays Info and Error modals.
// Returns false if there was an error.
func handleHTTP(u string, showInfo bool) bool {
if len(config.HTTPCommand) == 1 {
// Possibly a non-command
switch strings.TrimSpace(config.HTTPCommand[0]) {
case "", "off":
Error("HTTP Error", "Opening HTTP URLs is turned off.")
return false
case "default":
s, err := webbrowser.Open(u)
if err != nil {
Error("Webbrowser Error", err.Error())
return false
}
if showInfo {
Info(s)
}
return true
}
}
// Custom command
var proc *exec.Cmd
if len(config.HTTPCommand) > 1 {
proc = exec.Command(config.HTTPCommand[0], append(config.HTTPCommand[1:], u)...)
} else {
proc = exec.Command(config.HTTPCommand[0], u)
}
err := proc.Start()
if err != nil {
Error("HTTP Error", "Error executing custom browser command: "+err.Error())
return false
}
//nolint:errcheck
go proc.Wait() // Prevent zombies, see #219
Info("Opened with: " + config.HTTPCommand[0])
App.Draw()
return true
}
// handleOther is used by handleURL.
// It opens links other than Gemini and HTTP and displays Error modals.
func handleOther(u string) {
// The URL should have a scheme due to a previous call to normalizeURL
parsed, _ := url.Parse(u)
// Search for a handler for the URL scheme
handler := viper.GetStringSlice("url-handlers." + parsed.Scheme)
if len(handler) == 0 {
// A string and not a list of strings, use old method of parsing
// #214
handler = strings.Fields(viper.GetString("url-handlers." + parsed.Scheme))
if len(handler) == 0 {
handler = viper.GetStringSlice("url-handlers.other")
if len(handler) == 0 {
handler = strings.Fields(viper.GetString("url-handlers.other"))
}
}
}
if len(handler) == 1 {
// Maybe special key
switch strings.TrimSpace(handler[0]) {
case "", "off":
Error("URL Error", "Opening "+parsed.Scheme+" URLs is turned off.")
return
case "default":
_, err := sysopen.Open(u)
if err != nil {
Error("Application Error", err.Error())
return
}
Info("Opened in default application")
return
}
}
// Custom application command
var proc *exec.Cmd
if len(handler) > 1 {
proc = exec.Command(handler[0], append(handler[1:], u)...)
} else {
proc = exec.Command(handler[0], u)
}
err := proc.Start()
if err != nil {
Error("URL Error", "Error executing custom command: "+err.Error())
}
//nolint:errcheck
go proc.Wait() // Prevent zombies, see #219
Info("Opened with: " + handler[0])
App.Draw()
}
// handleAbout can be called to deal with any URLs that start with
// 'about:'. It will display errors if the URL is not recognized,
// but not display anything if an 'about:' URL is not passed.
//
// It does not add the displayed page to history.
//
// It returns the URL displayed, and a bool indicating if the provided
// URL could be handled. The string returned will always be empty
// if the bool is false.
func handleAbout(t *tab, u string) (string, bool) {
if !strings.HasPrefix(u, "about:") {
return "", false
}
switch u {
case "about:bookmarks":
Bookmarks(t)
return u, true
case "about:newtab":
temp := newTabPage // Copy
setPage(t, &temp)
t.applyBottomBar()
return u, true
case "about:version":
temp := versionPage
setPage(t, &temp)
t.applyBottomBar()
return u, true
case "about:license":
temp := licensePage
setPage(t, &temp)
t.applyBottomBar()
return u, true
case "about:thanks":
temp := thanksPage
setPage(t, &temp)
t.applyBottomBar()
return u, true
case "about:about":
temp := aboutPage
setPage(t, &temp)
t.applyBottomBar()
return u, true
}
if u == "about:subscriptions" || (len(u) > 20 && u[:20] == "about:subscriptions?") {
// about:subscriptions?2 views page 2
return Subscriptions(t, u), true
}
if u == "about:manage-subscriptions" || (len(u) > 27 && u[:27] == "about:manage-subscriptions?") {
ManageSubscriptions(t, u)
// Don't count remove command in history
if u == "about:manage-subscriptions" {
return u, true
}
return "", false
}
Error("Error", "Not a valid 'about:' URL.")
return "", false
}
// 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.
//
// The bottomBar is not actually changed in this func, except during loading.
// The func that calls this one should apply the bottomBar values if necessary.
//
// numRedirects is the number of redirects that resulted in the provided URL.
// It should typically be 0.
func handleURL(t *tab, u string, numRedirects int) (string, bool) {
defer App.Draw() // Just in case
// Save for resetting on error
oldLable := t.barLabel
oldText := t.barText
// Custom return function
ret := func(s string, b bool) (string, bool) {
if !b {
// Reset bottomBar if page wasn't loaded
t.barLabel = oldLable
t.barText = oldText
}
t.mode = tabModeDone
t.preferURLHandler = false
go func(p *structs.Page) {
if b && t.hasContent() && !t.isAnAboutPage() && viper.GetBool("subscriptions.popup") {
// The current page might be an untracked feed, and the user wants
// to be notified in such cases.
feed, isFeed := getFeedFromPage(p)
if isFeed && isValidTab(t) && t.page == p {
// After parsing and track-checking time, the page is still being displayed
addFeedDirect(p.URL, feed, subscriptions.IsSubscribed(p.URL))
}
}
}(t.page)
return s, b
}
t.barLabel = ""
bottomBar.SetLabel("")
App.SetFocus(t.view)
if strings.HasPrefix(u, "about:") {
return ret(handleAbout(t, u))
}
u = client.NormalizeURL(u)
u = cache.Redirect(u)
parsed, err := url.Parse(u)
if err != nil {
Error("URL Error", err.Error())
return ret("", false)
}
// check if a prompt is needed to handle this url
prompt := viper.GetBool("url-prompts.other")
if viper.IsSet("url-prompts." + parsed.Scheme) {
prompt = viper.GetBool("url-prompts." + parsed.Scheme)
}
if prompt && !(YesNo("Follow URL?\n" + u)) {
return ret("", false)
}
proxy := strings.TrimSpace(viper.GetString("proxies." + parsed.Scheme))
usingProxy := false
proxyHostname, proxyPort, err := net.SplitHostPort(proxy)
if err != nil {
// Error likely means there's no port in the host
proxyHostname = proxy
proxyPort = "1965"
}
if strings.HasPrefix(u, "http") {
if proxy == "" || proxy == "off" || t.preferURLHandler {
// No proxy available
handleHTTP(u, true)
return ret("", false)
}
usingProxy = true
}
if strings.HasPrefix(u, "file") {
page, ok := handleFile(u)
if !ok {
return ret("", false)
}
setPage(t, page)
return ret(u, true)
}
if !strings.HasPrefix(u, "http") && !strings.HasPrefix(u, "gemini") && !strings.HasPrefix(u, "file") {
// Not a Gemini URL
if proxy == "" || proxy == "off" || t.preferURLHandler {
// No proxy available
handleOther(u)
return ret("", false)
}
usingProxy = true
}
// Gemini URL, or one with a Gemini proxy available
// Load page from cache if it exists,
// and this isn't a page that was redirected to by the server (indicates dynamic content)
if numRedirects == 0 {
page, ok := cache.GetPage(u)
if ok {
setPage(t, page)
return ret(u, true)
}
}
// Otherwise download it
bottomBar.SetText("Loading...")
t.barText = "Loading..." // Save it too, in case the tab switches during loading
t.mode = tabModeLoading
App.Draw()
var res *gemini.Response
if usingProxy {
res, err = client.FetchWithProxy(proxyHostname, proxyPort, u)
} else {
res, err = client.Fetch(u)
}
// Loading may have taken a while, make sure tab is still valid
if !isValidTab(t) {
return ret("", false)
}
if errors.Is(err, client.ErrTofu) {
if usingProxy {
// They are using a proxy
if Tofu(proxy, client.GetExpiry(proxyHostname, proxyPort)) {
// They want to continue anyway
client.ResetTofuEntry(proxyHostname, proxyPort, res.Cert)
// Response can be used further down, no need to reload
} else {
// They don't want to continue
return ret("", false)
}
} else {
if Tofu(parsed.Host, client.GetExpiry(parsed.Hostname(), parsed.Port())) {
// They want to continue anyway
client.ResetTofuEntry(parsed.Hostname(), parsed.Port(), res.Cert)
// Response can be used further down, no need to reload
} else {
// They don't want to continue
return ret("", false)
}
}
} else if err != nil {
Error("URL Fetch Error", err.Error())
return ret("", false)
}
// Fetch happened successfully, use RestartReader to buffer read data
res.Body = rr.NewRestartReader(res.Body)
if renderer.CanDisplay(res) {
page, err := renderer.MakePage(u, res, textWidth(), usingProxy)
// Rendering may have taken a while, make sure tab is still valid
if !isValidTab(t) {
return ret("", false)
}
if errors.Is(err, renderer.ErrTooLarge) {
// Downloading now
// Disable read timeout and go back to start
res.SetReadTimeout(0) //nolint: errcheck
res.Body.(*rr.RestartReader).Restart()
dlChoice("That page is too large. What would you like to do?", u, res)
return ret("", false)
}
if errors.Is(err, renderer.ErrTimedOut) {
// Downloading now
// Disable read timeout and go back to start
res.SetReadTimeout(0) //nolint: errcheck
res.Body.(*rr.RestartReader).Restart()
dlChoice("Loading that page timed out. What would you like to do?", u, res)
return ret("", false)
}
if err != nil {
Error("Page Error", "Issuing creating page: "+err.Error())
return ret("", false)
}
page.TermWidth = termW
if !client.HasClientCert(parsed.Host, parsed.Path) {
// Don't cache pages with client certs
go cache.AddPage(page)
}
setPage(t, page)
return ret(u, true)
}
// Not displayable
// Could be a non 20 status code, or a different kind of document
// Handle each status code
// Except 20, that's handled after the switch
status := gemini.CleanStatus(res.Status)
switch status {
case 10, 11:
var userInput string
var ok bool
if status == 10 {
// Regular input
userInput, ok = Input(res.Meta, false)
} else {
// Sensitive input
userInput, ok = Input(res.Meta, true)
}
if ok {
// Make another request with the query string added
parsed.RawQuery = gemini.QueryEscape(userInput)
if len(parsed.String()) > gemini.URLMaxLength {
Error("Input Error", "URL for that input would be too long.")
return ret("", false)
}
return ret(handleURL(t, parsed.String(), 0))
}
return ret("", false)
case 30, 31:
parsedMeta, err := url.Parse(res.Meta)
if err != nil {
Error("Redirect Error", "Invalid URL: "+err.Error())
return ret("", false)
}
redir := parsed.ResolveReference(parsedMeta).String()
justAddsSlash := (redir == u+"/")
// Prompt before redirecting to non-Gemini protocol
redirect := false
if !justAddsSlash && !strings.HasPrefix(redir, "gemini") {
if YesNo("Follow redirect to non-Gemini URL?\n" + redir) {
redirect = true
} else {
return ret("", false)
}
}
// Prompt before redirecting
autoRedirect := justAddsSlash || viper.GetBool("a-general.auto_redirect")
if redirect || (autoRedirect && numRedirects < 5) || YesNo("Follow redirect?\n"+redir) {
if status == gemini.StatusRedirectPermanent {
go cache.AddRedir(u, redir)
}
return ret(handleURL(t, redir, numRedirects+1))
}
return ret("", false)
case 40:
Error("Temporary Failure", escapeMeta(res.Meta))
return ret("", false)
case 41:
Error("Server Unavailable", escapeMeta(res.Meta))
return ret("", false)
case 42:
Error("CGI Error", escapeMeta(res.Meta))
return ret("", false)
case 43:
Error("Proxy Failure", escapeMeta(res.Meta))
return ret("", false)
case 44:
Error("Slow Down", "You should wait "+escapeMeta(res.Meta)+" seconds before making another request.")
return ret("", false)
case 50:
Error("Permanent Failure", escapeMeta(res.Meta))
return ret("", false)
case 51:
Error("Not Found", escapeMeta(res.Meta))
return ret("", false)
case 52:
Error("Gone", escapeMeta(res.Meta))
return ret("", false)
case 53:
Error("Proxy Request Refused", escapeMeta(res.Meta))
return ret("", false)
case 59:
Error("Bad Request", escapeMeta(res.Meta))
return ret("", false)
case 60:
Error("Client Certificate Required", escapeMeta(res.Meta))
return ret("", false)
case 61:
Error("Certificate Not Authorised", escapeMeta(res.Meta))
return ret("", false)
case 62:
Error("Certificate Not Valid", escapeMeta(res.Meta))
return ret("", false)
default:
if !gemini.StatusInRange(status) {
// Status code not in a valid range
Error("Status Code Error", fmt.Sprintf("Out of range status code: %d", status))
return ret("", false)
}
}
// Status code 20, but not a document that can be displayed
// First see if it's a feed, and ask the user about adding it if it is
filename := path.Base(parsed.Path)
mediatype, _, _ := mime.ParseMediaType(res.Meta)
feed, ok := subscriptions.GetFeed(mediatype, filename, res.Body)
if ok {
go func() {
added := addFeedDirect(u, feed, subscriptions.IsSubscribed(u))
if !added {
// Otherwise offer download choices
// Disable read timeout and go back to start
res.SetReadTimeout(0) //nolint: errcheck
res.Body.(*rr.RestartReader).Restart()
dlChoice("That file could not be displayed. What would you like to do?", u, res)
}
}()
return ret("", false)
}
// Otherwise offer download choices
// Disable read timeout and go back to start
res.SetReadTimeout(0) //nolint: errcheck
res.Body.(*rr.RestartReader).Restart()
dlChoice("That file could not be displayed. What would you like to do?", u, res)
return ret("", false)
}
amfora-1.10.0/display/thanks.sh 0000755 0001750 0001750 00000000322 14575704331 015666 0 ustar nilesh nilesh #!/usr/bin/env sh
cat > thanks.go <<-EOF
//nolint
package display
//go:generate ./thanks.sh
EOF
echo -n 'var thanks = []byte(`' >> thanks.go
cat ../THANKS.md | tr '`' "'" >> thanks.go
echo '`)' >> thanks.go
amfora-1.10.0/display/about.go 0000644 0001750 0001750 00000002210 14575704331 015476 0 ustar nilesh nilesh package display
import (
"fmt"
"github.com/makeworld-the-better-one/amfora/renderer"
"github.com/makeworld-the-better-one/amfora/structs"
)
var aboutPage structs.Page
var versionPage structs.Page
var licensePage structs.Page
var thanksPage structs.Page
func aboutInit(version, commit, builtBy string) {
aboutPage = createAboutPage("about:about", `# Internal Pages
=> about:bookmarks
=> about:subscriptions
=> about:manage-subscriptions
=> about:newtab
=> about:version
=> about:license
=> about:thanks
`)
versionPage = createAboutPage("about:version",
fmt.Sprintf(
"# Amfora Version Info\n\nAmfora: %s\nCommit: %s\nBuilt by: %s",
version, commit, builtBy,
),
)
licensePage = createAboutPage("about:license", string(license))
thanksPage = createAboutPage("about:thanks", string(thanks))
}
func createAboutPage(url string, content string) structs.Page {
renderContent, links := renderer.RenderGemini(content, textWidth(), false)
return structs.Page{
Raw: content,
Content: renderContent,
Links: links,
URL: url,
TermWidth: -1, // Force reformatting on first display
Mediatype: structs.TextGemini,
}
}
amfora-1.10.0/display/help.go 0000644 0001750 0001750 00000010716 14575704331 015326 0 ustar nilesh nilesh package display
import (
"fmt"
"strings"
"text/tabwriter"
"code.rocketnine.space/tslocum/cview"
"github.com/gdamore/tcell/v2"
"github.com/makeworld-the-better-one/amfora/config"
)
var helpCells = strings.TrimSpace(
"?\tBring up this help. You can scroll!\n" +
"Esc\tLeave the help\n" +
"Arrow keys, %s(left)/%s(down)/%s(up)/%s(right)\tScroll and move a page.\n" +
"%s\tGo up a page in document\n" +
"%s\tGo down a page in document\n" +
"%s\tGo to top of document\n" +
"%s\tGo to bottom of document\n" +
"Tab\tNavigate to the next item in a popup.\n" +
"Shift-Tab\tNavigate to the previous item in a popup.\n" +
"%s\tGo back in the history\n" +
"%s\tGo forward in the history\n" +
"%s\tOpen bar at the bottom - type a URL, link number, search term.\n" +
"\tYou can also type two dots (..) to go up a directory in the URL.\n" +
"\tTyping new:N will open link number N in a new tab\n" +
"\tinstead of the current one.\n" +
"%s\tGo to links 1-10 respectively.\n" +
"%s\tEdit current URL\n" +
"%s\tCopy current page URL\n" +
"%s\tCopy current selected URL\n" +
"Enter, Tab\tOn a page this will start link highlighting.\n" +
"\tPress Tab and Shift-Tab to pick different links.\n" +
"\tPress Enter again to go to one, or Esc to stop.\n" +
"%s\tOpen the highlighted URL with a URL handler instead of the configured proxy\n" +
"%s\tGo to a specific tab. (Default: Shift-NUMBER)\n" +
"%s\tGo to the last tab.\n" +
"%s\tPrevious tab\n" +
"%s\tNext tab\n" +
"%s\tGo home\n" +
"%s\tNew tab, or if a link is selected,\n" +
"\tthis will open the link in a new tab.\n" +
"%s\tClose tab. For now, only the right-most tab can be closed.\n" +
"%s\tReload a page, discarding the cached version.\n" +
"\tThis can also be used if you resize your terminal.\n" +
"%s\tView bookmarks\n" +
"%s\tAdd, change, or remove a bookmark for the current page.\n" +
"%s\tSave the current page to your downloads.\n" +
"%s\tView subscriptions\n" +
"%s\tAdd or update a subscription\n" +
"%s\tQuit\n")
var helpTable = cview.NewTextView()
// Help displays the help and keybindings.
func Help() {
helpTable.ScrollToBeginning()
panels.ShowPanel(PanelHelp)
panels.SendToFront(PanelHelp)
App.SetFocus(helpTable)
}
func helpInit() {
// Populate help table
helpTable.SetBackgroundColor(config.GetColor("bg"))
helpTable.SetTextColor(config.GetColor("regular_text"))
helpTable.SetPadding(0, 0, 1, 1)
helpTable.SetDoneFunc(func(key tcell.Key) {
if key == tcell.KeyEsc || key == tcell.KeyEnter {
panels.HidePanel(PanelHelp)
App.SetFocus(tabs[curTab].view)
App.Draw()
}
})
helpTable.SetScrollBarColor(config.GetColor("scrollbar"))
tabKeys := fmt.Sprintf("%s to %s", strings.Split(config.GetKeyBinding(config.CmdTab1), ",")[0],
strings.Split(config.GetKeyBinding(config.CmdTab9), ",")[0])
linkKeys := fmt.Sprintf("%s to %s", strings.Split(config.GetKeyBinding(config.CmdLink1), ",")[0],
strings.Split(config.GetKeyBinding(config.CmdLink0), ",")[0])
helpCells = fmt.Sprintf(helpCells,
config.GetKeyBinding(config.CmdMoveLeft),
config.GetKeyBinding(config.CmdMoveDown),
config.GetKeyBinding(config.CmdMoveUp),
config.GetKeyBinding(config.CmdMoveRight),
config.GetKeyBinding(config.CmdPgup),
config.GetKeyBinding(config.CmdPgdn),
config.GetKeyBinding(config.CmdBeginning),
config.GetKeyBinding(config.CmdEnd),
config.GetKeyBinding(config.CmdBack),
config.GetKeyBinding(config.CmdForward),
config.GetKeyBinding(config.CmdBottom),
linkKeys,
config.GetKeyBinding(config.CmdEdit),
config.GetKeyBinding(config.CmdCopyPageURL),
config.GetKeyBinding(config.CmdCopyTargetURL),
config.GetKeyBinding(config.CmdURLHandlerOpen),
tabKeys,
config.GetKeyBinding(config.CmdTab0),
config.GetKeyBinding(config.CmdPrevTab),
config.GetKeyBinding(config.CmdNextTab),
config.GetKeyBinding(config.CmdHome),
config.GetKeyBinding(config.CmdNewTab),
config.GetKeyBinding(config.CmdCloseTab),
config.GetKeyBinding(config.CmdReload),
config.GetKeyBinding(config.CmdBookmarks),
config.GetKeyBinding(config.CmdAddBookmark),
config.GetKeyBinding(config.CmdSave),
config.GetKeyBinding(config.CmdSub),
config.GetKeyBinding(config.CmdAddSub),
config.GetKeyBinding(config.CmdQuit),
)
lines := strings.Split(helpCells, "\n")
w := tabwriter.NewWriter(helpTable, 0, 8, 2, ' ', 0)
for i, line := range lines {
if i > 0 && line[0] != '\t' {
fmt.Fprintln(w, "\t")
}
fmt.Fprintln(w, line)
}
w.Flush()
panels.AddPanel(PanelHelp, helpTable, true, false)
}
amfora-1.10.0/display/modals.go 0000644 0001750 0001750 00000024143 14575704331 015654 0 ustar nilesh nilesh package display
import (
"fmt"
"strings"
"time"
"code.rocketnine.space/tslocum/cview"
humanize "github.com/dustin/go-humanize"
"github.com/gdamore/tcell/v2"
"github.com/makeworld-the-better-one/amfora/config"
"github.com/spf13/viper"
)
// This file contains code for the popups / modals used in the display.
// The bookmark modal is in bookmarks.go
var infoModal = cview.NewModal()
var errorModal = cview.NewModal()
var inputModal = cview.NewModal()
var yesNoModal = cview.NewModal()
var inputCh = make(chan string)
var yesNoCh = make(chan bool)
var inputModalText string // The current text of the input field in the modal
// Internal channel used to know when a modal has been dismissed
var modalDone = make(chan struct{})
func modalInit() {
infoModal.AddButtons([]string{"Ok"})
errorModal.AddButtons([]string{"Ok"})
yesNoModal.AddButtons([]string{"Yes", "No"})
panels.AddPanel(PanelInfoModal, infoModal, false, false)
panels.AddPanel(PanelErrorModal, errorModal, false, false)
panels.AddPanel(PanelInputModal, inputModal, false, false)
panels.AddPanel(PanelYesNoModal, yesNoModal, false, false)
// Color setup
if viper.GetBool("a-general.color") {
m := infoModal
m.SetBackgroundColor(config.GetColor("info_modal_bg"))
m.SetButtonBackgroundColor(config.GetColor("btn_bg"))
m.SetButtonTextColor(config.GetColor("btn_text"))
m.SetTextColor(config.GetColor("info_modal_text"))
form := m.GetForm()
form.SetButtonBackgroundColorFocused(config.GetColor("btn_text"))
form.SetButtonTextColorFocused(config.GetTextColor("btn_bg", "btn_text"))
frame := m.GetFrame()
frame.SetBorderColor(config.GetColor("info_modal_text"))
frame.SetTitleColor(config.GetColor("info_modal_text"))
m = errorModal
m.SetBackgroundColor(config.GetColor("error_modal_bg"))
m.SetButtonBackgroundColor(config.GetColor("btn_bg"))
m.SetButtonTextColor(config.GetColor("btn_text"))
m.SetTextColor(config.GetColor("error_modal_text"))
form = m.GetForm()
form.SetButtonBackgroundColorFocused(config.GetColor("btn_text"))
form.SetButtonTextColorFocused(config.GetTextColor("btn_bg", "btn_text"))
frame = errorModal.GetFrame()
frame.SetBorderColor(config.GetColor("error_modal_text"))
frame.SetTitleColor(config.GetColor("error_modal_text"))
m = inputModal
m.SetBackgroundColor(config.GetColor("input_modal_bg"))
m.SetButtonBackgroundColor(config.GetColor("btn_bg"))
m.SetButtonTextColor(config.GetColor("btn_text"))
m.SetTextColor(config.GetColor("input_modal_text"))
frame = inputModal.GetFrame()
frame.SetBorderColor(config.GetColor("input_modal_text"))
frame.SetTitleColor(config.GetColor("input_modal_text"))
form = inputModal.GetForm()
form.SetFieldBackgroundColor(config.GetColor("input_modal_field_bg"))
form.SetFieldTextColor(config.GetColor("input_modal_field_text"))
form.SetButtonBackgroundColorFocused(config.GetColor("btn_text"))
form.SetButtonTextColorFocused(config.GetTextColor("btn_bg", "btn_text"))
m = yesNoModal
m.SetButtonBackgroundColor(config.GetColor("btn_bg"))
m.SetButtonTextColor(config.GetColor("btn_text"))
form = m.GetForm()
form.SetButtonBackgroundColorFocused(config.GetColor("btn_text"))
form.SetButtonTextColorFocused(config.GetTextColor("btn_bg", "btn_text"))
} else {
m := infoModal
m.SetBackgroundColor(tcell.ColorBlack)
m.SetButtonBackgroundColor(tcell.ColorWhite)
m.SetButtonTextColor(tcell.ColorBlack)
m.SetTextColor(tcell.ColorWhite)
form := m.GetForm()
form.SetButtonBackgroundColorFocused(tcell.ColorBlack)
form.SetButtonTextColorFocused(tcell.ColorWhite)
frame := infoModal.GetFrame()
frame.SetBorderColor(tcell.ColorWhite)
frame.SetTitleColor(tcell.ColorWhite)
m = errorModal
m.SetBackgroundColor(tcell.ColorBlack)
m.SetButtonBackgroundColor(tcell.ColorWhite)
m.SetButtonTextColor(tcell.ColorBlack)
m.SetTextColor(tcell.ColorWhite)
form = m.GetForm()
form.SetButtonBackgroundColorFocused(tcell.ColorBlack)
form.SetButtonTextColorFocused(tcell.ColorWhite)
frame = errorModal.GetFrame()
frame.SetBorderColor(tcell.ColorWhite)
frame.SetTitleColor(tcell.ColorWhite)
m = inputModal
m.SetBackgroundColor(tcell.ColorBlack)
m.SetButtonBackgroundColor(tcell.ColorWhite)
m.SetButtonTextColor(tcell.ColorBlack)
m.SetTextColor(tcell.ColorWhite)
frame = inputModal.GetFrame()
frame.SetBorderColor(tcell.ColorWhite)
frame.SetTitleColor(tcell.ColorWhite)
form = inputModal.GetForm()
form.SetFieldBackgroundColor(tcell.ColorWhite)
form.SetFieldTextColor(tcell.ColorBlack)
form.SetButtonBackgroundColorFocused(tcell.ColorBlack)
form.SetButtonTextColorFocused(tcell.ColorWhite)
// YesNo background color is changed in funcs
m = yesNoModal
m.SetButtonBackgroundColor(tcell.ColorWhite)
m.SetButtonTextColor(tcell.ColorBlack)
form = m.GetForm()
form.SetButtonBackgroundColorFocused(tcell.ColorBlack)
form.SetButtonTextColorFocused(tcell.ColorWhite)
}
// Modal functions that can't be added up above, because they return the wrong type
infoModal.SetBorder(true)
frame := infoModal.GetFrame()
frame.SetTitleAlign(cview.AlignCenter)
frame.SetTitle(" Info ")
infoModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
panels.HidePanel(PanelInfoModal)
App.SetFocus(tabs[curTab].view)
App.Draw()
modalDone <- struct{}{}
})
errorModal.SetBorder(true)
errorModal.GetFrame().SetTitleAlign(cview.AlignCenter)
errorModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
panels.HidePanel(PanelErrorModal)
App.SetFocus(tabs[curTab].view)
App.Draw()
modalDone <- struct{}{}
})
inputModal.SetBorder(true)
frame = inputModal.GetFrame()
frame.SetTitleAlign(cview.AlignCenter)
frame.SetTitle(" Input ")
inputModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
if buttonLabel == "Send" {
inputCh <- inputModalText
return
}
// Empty string indicates no input
inputCh <- ""
})
yesNoModal.SetBorder(true)
yesNoModal.GetFrame().SetTitleAlign(cview.AlignCenter)
yesNoModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
if buttonLabel == "Yes" {
yesNoCh <- true
return
}
yesNoCh <- false
})
bkmkInit()
dlInit()
}
// Error displays an error on the screen in a modal, and blocks until dismissed by the user.
func Error(title, text string) {
if text == "" {
text = "No additional information."
} else {
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.GetFrame().SetTitle(title)
errorModal.SetText(text)
panels.ShowPanel(PanelErrorModal)
panels.SendToFront(PanelErrorModal)
App.SetFocus(errorModal)
App.Draw()
<-modalDone
}
// Info displays some info on the screen in a modal, and blocks until dismissed by the user.
func Info(s string) {
infoModal.SetText(s)
panels.ShowPanel(PanelInfoModal)
panels.SendToFront(PanelInfoModal)
App.SetFocus(infoModal)
App.Draw()
<-modalDone
}
// Input pulls up a modal that asks for input, waits for that input, and returns it.
// It returns an bool indicating if the user chose to send input or not.
func Input(prompt string, sensitive bool) (string, bool) {
// Remove elements and re-add them - to clear input text and keep input in focus
inputModal.ClearButtons()
inputModal.GetForm().Clear(false)
inputModal.AddButtons([]string{"Send", "Cancel"})
inputModalText = ""
if sensitive {
// TODO use bullet characters if user wants it once bug is fixed - see NOTES.md
inputModal.GetForm().AddPasswordField("", "", 0, '*',
func(text string) {
// Store for use later
inputModalText = text
})
} else {
inputModal.GetForm().AddInputField("", "", 0, nil,
func(text string) {
inputModalText = text
})
}
inputModal.SetText(prompt + " ")
panels.ShowPanel(PanelInputModal)
panels.SendToFront(PanelInputModal)
App.SetFocus(inputModal)
App.Draw()
resp := <-inputCh
panels.HidePanel(PanelInputModal)
App.SetFocus(tabs[curTab].view)
App.Draw()
if resp == "" {
return "", false
}
return resp, true
}
// YesNo displays a modal asking a yes-or-no question, waits for an answer, then returns it as a bool.
func YesNo(prompt string) bool {
if viper.GetBool("a-general.color") {
m := yesNoModal
m.SetBackgroundColor(config.GetColor("yesno_modal_bg"))
m.SetTextColor(config.GetColor("yesno_modal_text"))
frame := yesNoModal.GetFrame()
frame.SetBorderColor(config.GetColor("yesno_modal_text"))
frame.SetTitleColor(config.GetColor("yesno_modal_text"))
} else {
m := yesNoModal
m.SetBackgroundColor(tcell.ColorBlack)
m.SetTextColor(tcell.ColorWhite)
frame := yesNoModal.GetFrame()
frame.SetBorderColor(tcell.ColorWhite)
frame.SetTitleColor(tcell.ColorWhite)
}
yesNoModal.GetFrame().SetTitle("")
yesNoModal.SetText(prompt)
panels.ShowPanel(PanelYesNoModal)
panels.SendToFront(PanelYesNoModal)
App.SetFocus(yesNoModal)
App.Draw()
resp := <-yesNoCh
panels.HidePanel(PanelYesNoModal)
App.SetFocus(tabs[curTab].view)
App.Draw()
return resp
}
// Tofu displays the TOFU warning modal.
// It blocks then returns a bool indicating whether the user wants to continue.
func Tofu(host string, expiry time.Time) bool {
// Reuses yesNoModal, with error color
m := yesNoModal
frame := yesNoModal.GetFrame()
if viper.GetBool("a-general.color") {
m.SetBackgroundColor(config.GetColor("tofu_modal_bg"))
m.SetTextColor(config.GetColor("tofu_modal_text"))
frame.SetBorderColor(config.GetColor("tofu_modal_text"))
frame.SetTitleColor(config.GetColor("tofu_modal_text"))
} else {
m.SetBackgroundColor(tcell.ColorBlack)
m.SetTextColor(tcell.ColorWhite)
m.SetBorderColor(tcell.ColorWhite)
m.SetTitleColor(tcell.ColorWhite)
}
frame.SetTitle(" TOFU ")
m.SetText(
//nolint:lll
fmt.Sprintf("%s's certificate has changed, possibly indicating a security issue. The certificate would have expired %s. Are you sure you want to continue? ",
host,
humanize.Time(expiry),
),
)
panels.ShowPanel(PanelYesNoModal)
panels.SendToFront(PanelYesNoModal)
App.SetFocus(yesNoModal)
App.Draw()
resp := <-yesNoCh
panels.HidePanel(PanelYesNoModal)
App.SetFocus(tabs[curTab].view)
App.Draw()
return resp
}
amfora-1.10.0/display/panels.go 0000644 0001750 0001750 00000000473 14575704331 015657 0 ustar nilesh nilesh package display
const (
PanelBrowser = "browser"
PanelBookmarks = "bkmk"
PanelDownload = "dl"
PanelDownloadChoiceModal = "dlChoice"
PanelHelp = "help"
PanelYesNoModal = "yesno"
PanelInfoModal = "info"
PanelErrorModal = "error"
PanelInputModal = "input"
)
amfora-1.10.0/display/thanks.go 0000644 0001750 0001750 00000001770 14575704331 015666 0 ustar nilesh nilesh //nolint
package display
//go:generate ./thanks.sh
var thanks = []byte(`# THANKS
Thank you to the following contributors, who have helped make Amfora great. FOSS projects are a community effort, and we would be worse off without you.
* Sotiris Papatheodorou (@sotpapathe)
* Chloe Kudryavtsev (@CosmicToast)
* Adrian Hesketh (@a-h)
* Jansen Price (@sumpygump)
* Alex Wennerberg (@alexwennerberg)
* Timur Ismagilov (@bouncepaw)
* Matt Caroll (@ohiolab)
* Patryk Niedźwiedziński (@pniedzwiedzinski)
* Trevor Slocum (@tsclocum)
* Mattias Jadelius (@jedthehumanoid)
* Lokesh Krishna (@lokesh-krishna)
* Jeff (@phaedrus-jaf)
* Stephen Robinson (@sudobash1)
* Peter Steinberg (@objectliteral)
* Thomas Adam (@ThomasAdam)
* @lostleonardo
* Himanshu (@singalhimanshu)
* @regr4
* Anas Mohamed (@amohamed11)
* David Jimenez (@dvejmz)
* Michael McDonagh (@m-mcdonagh)
* mooff (@awfulcooking)
* Josias (@justjosias)
* mntn (@mntn-xyz)
* Maxime Bouillot (@Arkaeriit)
* Emily (@emily-is-my-username)
* Autumn! (@autumnull)
`)
amfora-1.10.0/display/private.go 0000644 0001750 0001750 00000010276 14575704331 016051 0 ustar nilesh nilesh package display
// This file contains the functions that aren't part of the public API.
// The funcs are for network and displaying.
import (
"net/url"
"strconv"
"strings"
"github.com/makeworld-the-better-one/amfora/renderer"
"github.com/makeworld-the-better-one/amfora/structs"
)
// followLink should be used when the user "clicks" a link on a page,
// but not when a URL is opened on a new tab for the first time.
//
// It will handle updating the bottomBar.
//
// It should be called with the `go` keyword to spawn a new goroutine if
// it would otherwise block the UI loop, such as when called from an input
// handler.
//
// It blocks until navigation is finished, and we've completed any user
// interaction related to loading the URL (such as info, error modals)
func followLink(t *tab, prev, next string) {
if strings.HasPrefix(next, "about:") {
goURL(t, next)
return
}
if t.hasContent() && !t.isAnAboutPage() {
nextURL, err := resolveRelLink(t, prev, next)
if err != nil {
Error("URL Error", err.Error())
return
}
goURL(t, nextURL)
return
}
// No content on current tab, so the "prev" URL is not valid.
// An example is the about:newtab page
_, err := url.Parse(next)
if err != nil {
Error("URL Error", "Link URL could not be parsed")
return
}
goURL(t, next)
}
// reformatPage will take the raw page content and reformat it according to the current terminal dimensions.
// It should be called when the terminal size changes.
// It will not waste resources if the passed page is already fitted to the current terminal width, and can be
// called safely even when the page might be already formatted properly.
func reformatPage(p *structs.Page) {
if p.TermWidth == termW {
// No changes to make
return
}
// TODO: Setup a renderer.RenderFromMediatype func so this isn't needed
var rendered string
switch p.Mediatype {
case structs.TextGemini:
// Links are not recorded because they won't change
proxied := true
if strings.HasPrefix(p.URL, "gemini") ||
strings.HasPrefix(p.URL, "about") ||
strings.HasPrefix(p.URL, "file") {
proxied = false
}
rendered, _ = renderer.RenderGemini(p.Raw, textWidth(), proxied)
case structs.TextPlain:
rendered = renderer.RenderPlainText(p.Raw)
case structs.TextAnsi:
rendered = renderer.RenderANSI(p.Raw)
default:
// Rendering this type is not implemented
return
}
p.Content = rendered
p.TermWidth = termW
}
// reformatPageAndSetView is for reformatting a page that is already being displayed.
// setPage should be used when a page is being loaded for the first time.
func reformatPageAndSetView(t *tab, p *structs.Page) {
if p.TermWidth == termW {
// No changes to make
return
}
reformatPage(p)
t.view.SetText(p.Content)
t.applyScroll() // Go back to where you were, roughly
App.Draw()
}
// setPage displays a Page on the passed tab number.
// The bottomBar is not actually changed in this func
func setPage(t *tab, p *structs.Page) {
if !isValidTab(t) {
// Don't waste time reformatting an invalid tab
return
}
// Make sure the page content is fitted to the terminal every time it's displayed
reformatPage(p)
t.page = p
// Change page on screen
t.view.SetText(p.Content)
t.view.Highlight("") // Turn off highlights, other funcs may restore if necessary
t.view.ScrollToBeginning()
// Reset page left margin
tabNum := tabNumber(t)
browser.AddTab(
strconv.Itoa(tabNum),
t.label(),
makeContentLayout(t.view, leftMargin()),
)
App.Draw()
// Setup display
App.SetFocus(t.view)
// Save bottom bar for the tab - other funcs will apply/display it
t.barLabel = ""
t.barText = p.URL
}
// goURL is like handleURL, but takes care of history and the bottomBar.
// It should be preferred over handleURL in most cases.
// It has no return values to be processed.
//
// It should be called in a goroutine.
func goURL(t *tab, u string) {
// Update page cache in history for #122
t.historyCachePage()
final, displayed := handleURL(t, u, 0)
if displayed {
t.addToHistory(final)
} else if t.page.URL == "" {
// The tab is showing interstitial or no content. Let's go to about:newtab.
handleAbout(t, "about:newtab")
}
if t == tabs[curTab] {
// Display the bottomBar state that handleURL set
t.applyBottomBar()
}
}
amfora-1.10.0/display/license.sh 0000755 0001750 0001750 00000000315 14575704331 016022 0 ustar nilesh nilesh #!/usr/bin/env sh
cat > license.go <<-EOF
package display
//go:generate ./license.sh
EOF
echo -n 'var license = []byte(`' >> license.go
cat ../LICENSE | tr '`' "'" >> license.go
echo '`)' >> license.go
amfora-1.10.0/display/newtab.go 0000644 0001750 0001750 00000002200 14575704331 015643 0 ustar nilesh nilesh package display
import (
"io/ioutil"
"github.com/makeworld-the-better-one/amfora/config"
)
//nolint
var defaultNewTabContent = `# 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.
Press the ? key at any time to bring up the help, and see other keybindings. Most are what you expect.
You can customize this page by creating a gemtext file called newtab.gmi, in Amfora's configuration folder.
Happy browsing!
## Internal Pages
=> about:bookmarks Bookmarks
=> about:subscriptions Subscriptions
=> about:about All internal pages
## Learn more about Amfora!
=> https://github.com/makeworld-the-better-one/amfora Amfora homepage
=> https://github.com/makeworld-the-better-one/amfora/wiki Amfora Wiki [GitHub]
=> gemini://makeworld.space/amfora-wiki/ Amfora Wiki [On Gemini!]
=> gemini://geminiprotocol.net Project Gemini
`
// Read the new tab content from a file if it exists or fallback to a default page.
func getNewTabContent() string {
data, err := ioutil.ReadFile(config.NewTabPath)
if err == nil {
return string(data)
}
return defaultNewTabContent
}
amfora-1.10.0/display/history.go 0000644 0001750 0001750 00000001730 14575704331 016073 0 ustar nilesh nilesh package display
// applyHist is a history.go internal function, to load a URL in the history.
func applyHist(t *tab) {
handleURL(t, t.history.urls[t.history.pos], 0) // Load that position in history
// Set page's scroll and link info from history cache, in case it didn't have it in the page already
// Like for non-cached pages like about: pages
// This fixes #122
pg := t.history.pageCache[t.history.pos]
p := t.page
p.Row = pg.row
p.Column = pg.column
p.Selected = pg.selected
p.SelectedID = pg.selectedID
p.Mode = pg.mode
t.applyAll()
}
func histForward(t *tab) {
if t.history.pos >= len(t.history.urls)-1 {
// Already on the most recent URL in the history
return
}
// Update page cache in history for #122
t.historyCachePage()
t.history.pos++
go applyHist(t)
}
func histBack(t *tab) {
if t.history.pos <= 0 {
// First tab in history
return
}
// Update page cache in history for #122
t.historyCachePage()
t.history.pos--
go applyHist(t)
}
amfora-1.10.0/display/util.go 0000644 0001750 0001750 00000005417 14575704331 015355 0 ustar nilesh nilesh package display
import (
"errors"
"net/url"
"strings"
"code.rocketnine.space/tslocum/cview"
"github.com/spf13/viper"
)
// This file contains funcs that are small, self-contained utilities.
// makeContentLayout returns a flex that contains the given TextView
// along with the provided left margin, as well as a single empty
// line at the top, for a top margin.
func makeContentLayout(tv *cview.TextView, leftMargin int) *cview.Flex {
// Create horizontal flex with the left margin as an empty space
horiz := cview.NewFlex()
horiz.SetDirection(cview.FlexColumn)
if leftMargin > 0 {
horiz.AddItem(nil, leftMargin, 0, false)
}
horiz.AddItem(tv, 0, 1, true)
// Create a vertical flex with the other one and a top margin
vert := cview.NewFlex()
vert.SetDirection(cview.FlexRow)
vert.AddItem(nil, 1, 0, false)
vert.AddItem(horiz, 0, 1, true)
return vert
}
// tabNumber gets the index of the tab in the tabs slice. It returns -1
// if the tab is not in that slice.
func tabNumber(t *tab) int {
tempTabs := tabs
for i := range tempTabs {
if tempTabs[i] == t {
return i
}
}
return -1
}
// escapeMeta santizes a META string for use within a cview modal.
func escapeMeta(meta string) string {
return cview.Escape(strings.ReplaceAll(meta, "\n", ""))
}
// isValidTab indicates whether the passed tab is still being used, even if it's not currently displayed.
func isValidTab(t *tab) bool {
return tabNumber(t) != -1
}
func leftMargin() int {
// Return the left margin size that centers the text, assuming it's the max width
// https://github.com/makeworld-the-better-one/amfora/issues/233
lm := (termW - viper.GetInt("a-general.max_width")) / 2
if lm < 0 {
return 0
}
return lm
}
func textWidth() int {
if termW <= 0 {
// This prevent a flash of 1-column text on startup, when the terminal
// width hasn't been initialized.
return viper.GetInt("a-general.max_width")
}
// Subtract left and right margin from total width to get text width
// Left and right margin are equal because text is automatically centered, see:
// https://github.com/makeworld-the-better-one/amfora/issues/233
max := termW - leftMargin()*2
if max < viper.GetInt("a-general.max_width") {
return max
}
return viper.GetInt("a-general.max_width")
}
// resolveRelLink returns an absolute link for the given absolute link and relative one.
// It also returns an error if it could not resolve the links, which should be displayed
// to the user.
func resolveRelLink(t *tab, prev, next string) (string, error) {
if !t.hasContent() || t.isAnAboutPage() {
return next, nil
}
prevParsed, _ := url.Parse(prev)
nextParsed, err := url.Parse(next)
if err != nil {
return "", errors.New("link URL could not be parsed") //nolint:goerr113
}
return prevParsed.ResolveReference(nextParsed).String(), nil
}
amfora-1.10.0/display/download.go 0000644 0001750 0001750 00000025437 14575704331 016213 0 ustar nilesh nilesh package display
import (
"fmt"
"io"
"io/ioutil"
"mime"
"net/url"
"os"
"os/exec"
"path"
"path/filepath"
"strconv"
"strings"
"time"
"code.rocketnine.space/tslocum/cview"
"github.com/gdamore/tcell/v2"
"github.com/makeworld-the-better-one/amfora/config"
"github.com/makeworld-the-better-one/amfora/structs"
"github.com/makeworld-the-better-one/amfora/sysopen"
"github.com/makeworld-the-better-one/go-gemini"
"github.com/schollz/progressbar/v3"
"github.com/spf13/viper"
)
// For choosing between download and the portal - copy of YesNo basically
var dlChoiceModal = cview.NewModal()
// Channel to indicate what choice they made using the button text
var dlChoiceCh = make(chan string)
var dlModal = cview.NewModal()
func dlInit() {
panels.AddPanel(PanelDownload, dlModal, false, false)
panels.AddPanel(PanelDownloadChoiceModal, dlChoiceModal, false, false)
dlm := dlModal
chm := dlChoiceModal
if viper.GetBool("a-general.color") {
chm.SetButtonBackgroundColor(config.GetColor("btn_bg"))
chm.SetButtonTextColor(config.GetColor("btn_text"))
chm.SetBackgroundColor(config.GetColor("dl_choice_modal_bg"))
chm.SetTextColor(config.GetColor("dl_choice_modal_text"))
form := chm.GetForm()
form.SetButtonBackgroundColorFocused(config.GetColor("btn_text"))
form.SetButtonTextColorFocused(config.GetTextColor("btn_bg", "btn_text"))
frame := chm.GetFrame()
frame.SetBorderColor(config.GetColor("dl_choice_modal_text"))
frame.SetTitleColor(config.GetColor("dl_choice_modal_text"))
dlm.SetButtonBackgroundColor(config.GetColor("btn_bg"))
dlm.SetButtonTextColor(config.GetColor("btn_text"))
dlm.SetBackgroundColor(config.GetColor("dl_modal_bg"))
dlm.SetTextColor(config.GetColor("dl_modal_text"))
form = dlm.GetForm()
form.SetButtonBackgroundColorFocused(config.GetColor("btn_text"))
form.SetButtonTextColorFocused(config.GetTextColor("btn_bg", "btn_text"))
frame = dlm.GetFrame()
frame.SetBorderColor(config.GetColor("dl_modal_text"))
frame.SetTitleColor(config.GetColor("dl_modal_text"))
} else {
chm.SetButtonBackgroundColor(tcell.ColorWhite)
chm.SetButtonTextColor(tcell.ColorBlack)
chm.SetBackgroundColor(tcell.ColorBlack)
chm.SetTextColor(tcell.ColorWhite)
chm.SetBorderColor(tcell.ColorWhite)
chm.GetFrame().SetTitleColor(tcell.ColorWhite)
form := chm.GetForm()
form.SetButtonBackgroundColorFocused(tcell.ColorBlack)
form.SetButtonTextColorFocused(tcell.ColorWhite)
dlm.SetButtonBackgroundColor(tcell.ColorWhite)
dlm.SetButtonTextColor(tcell.ColorBlack)
dlm.SetBackgroundColor(tcell.ColorBlack)
dlm.SetTextColor(tcell.ColorWhite)
form = dlm.GetForm()
form.SetButtonBackgroundColorFocused(tcell.ColorBlack)
form.SetButtonTextColorFocused(tcell.ColorWhite)
frame := dlm.GetFrame()
frame.SetBorderColor(tcell.ColorWhite)
frame.SetTitleColor(tcell.ColorWhite)
}
chm.AddButtons([]string{"Open", "Download", "Cancel"})
chm.SetBorder(true)
chm.GetFrame().SetTitleAlign(cview.AlignCenter)
chm.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
dlChoiceCh <- buttonLabel
})
dlm.SetBorder(true)
frame := dlm.GetFrame()
frame.SetTitleAlign(cview.AlignCenter)
frame.SetTitle(" Download ")
dlm.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
if buttonLabel == "Ok" {
panels.HidePanel(PanelDownload)
App.SetFocus(tabs[curTab].view)
App.Draw()
}
})
}
func getMediaHandler(resp *gemini.Response) config.MediaHandler {
def := config.MediaHandler{
Cmd: nil,
NoPrompt: false,
Stream: false,
}
mediatype, _, err := mime.ParseMediaType(resp.Meta)
if err != nil {
return def
}
if ret, ok := config.MediaHandlers[mediatype]; ok {
return ret
}
splitType := strings.Split(mediatype, "/")[0]
if ret, ok := config.MediaHandlers[splitType]; ok {
return ret
}
if ret, ok := config.MediaHandlers["*"]; ok {
return ret
}
return def
}
// dlChoice displays the download choice modal and acts on the user's choice.
// It should run in a goroutine.
func dlChoice(text, u string, resp *gemini.Response) {
mediaHandler := getMediaHandler(resp)
var choice string
if mediaHandler.NoPrompt {
choice = "Open"
} else {
dlChoiceModal.SetText(text)
panels.ShowPanel(PanelDownloadChoiceModal)
panels.SendToFront(PanelDownloadChoiceModal)
App.SetFocus(dlChoiceModal)
App.Draw()
choice = <-dlChoiceCh
}
if choice == "Download" {
panels.HidePanel(PanelDownloadChoiceModal)
App.Draw()
downloadURL(config.DownloadsDir, u, resp)
resp.Body.Close() // Only close when the file is downloaded
return
}
if choice == "Open" {
panels.HidePanel(PanelDownloadChoiceModal)
App.Draw()
open(u, resp)
return
}
// They chose the "Cancel" button
panels.HidePanel(PanelDownloadChoiceModal)
App.SetFocus(tabs[curTab].view)
App.Draw()
}
// open performs the same actions as downloadURL except it also opens the file.
// If there is no system viewer configured for the particular mediatype, it opens it
// with the default system viewer.
func open(u string, resp *gemini.Response) {
mediaHandler := getMediaHandler(resp)
if mediaHandler.Stream {
// Run command with downloaded data from stdin
cmd := mediaHandler.Cmd
var proc *exec.Cmd
if len(cmd) == 1 {
proc = exec.Command(cmd[0])
} else {
proc = exec.Command(cmd[0], cmd[1:]...)
}
proc.Stdin = resp.Body
err := proc.Start()
if err != nil {
Error("File Opening Error", "Error executing custom command: "+err.Error())
return
}
//nolint:errcheck
go proc.Wait() // Prevent zombies, see #219
Info("Opened with " + cmd[0])
return
}
path := downloadURL(config.TempDownloadsDir, u, resp)
if path == "" {
return
}
panels.HidePanel(PanelDownload)
App.SetFocus(tabs[curTab].view)
App.Draw()
if mediaHandler.Cmd == nil {
// Open with system default viewer
_, err := sysopen.Open(path)
if err != nil {
Error("System Viewer Error", err.Error())
return
}
Info("Opened in default system viewer")
} else {
cmd := mediaHandler.Cmd
proc := exec.Command(cmd[0], append(cmd[1:], path)...)
err := proc.Start()
if err != nil {
Error("File Opening Error", "Error executing custom command: "+err.Error())
return
}
//nolint:errcheck
go proc.Wait() // Prevent zombies, see #219
Info("Opened with " + cmd[0])
}
App.Draw()
}
// downloadURL pulls up a modal to show download progress and saves the URL content.
// downloadPage should be used for Page content.
// Returns location downloaded to or an empty string on error.
func downloadURL(dir, u string, resp *gemini.Response) string {
_, _, width, _ := dlModal.GetInnerRect()
// Copy of progressbar.DefaultBytesSilent with custom width
bar := progressbar.NewOptions64(
-1,
progressbar.OptionSetWidth(width),
progressbar.OptionSetWriter(ioutil.Discard),
progressbar.OptionShowBytes(true),
progressbar.OptionThrottle(65*time.Millisecond),
progressbar.OptionShowCount(),
progressbar.OptionSpinnerType(14),
)
bar.RenderBlank() //nolint:errcheck
savePath, err := downloadNameFromURL(dir, u, "")
if err != nil {
Error("Download Error", "Error deciding on file name: "+err.Error())
return ""
}
f, err := os.OpenFile(savePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
Error("Download Error", "Error creating download file: "+err.Error())
return ""
}
defer f.Close()
done := false
go func(isDone *bool) {
// Update the bar display
for !*isDone {
dlModal.SetText(bar.String())
App.Draw()
time.Sleep(100 * time.Millisecond)
}
}(&done)
// Display
dlModal.ClearButtons()
dlModal.AddButtons([]string{"Downloading..."})
panels.ShowPanel(PanelDownload)
panels.SendToFront(PanelDownload)
App.SetFocus(dlModal)
App.Draw()
_, err = io.Copy(io.MultiWriter(f, bar), resp.Body)
done = true
if err != nil {
panels.HidePanel(PanelDownload)
Error("Download Error", err.Error())
f.Close()
os.Remove(savePath) // Remove partial file
return ""
}
dlModal.SetText(fmt.Sprintf("Download complete! File saved to %s.", savePath))
dlModal.ClearButtons()
dlModal.AddButtons([]string{"Ok"})
dlModal.GetForm().SetFocus(100)
App.SetFocus(dlModal)
App.Draw()
return savePath
}
// downloadPage saves the passed Page to a file.
// It returns the saved path and an error.
// It always cleans up, so if an error is returned there is no file saved
func downloadPage(p *structs.Page) (string, error) {
var savePath string
var err error
if p.Mediatype == structs.TextGemini {
savePath, err = downloadNameFromURL(config.DownloadsDir, p.URL, ".gmi")
} else {
savePath, err = downloadNameFromURL(config.DownloadsDir, p.URL, ".txt")
}
if err != nil {
return "", err
}
err = ioutil.WriteFile(savePath, []byte(p.Raw), 0644)
if err != nil {
// Just in case
os.Remove(savePath)
return "", err
}
return savePath, err
}
// downloadNameFromURL takes a URl and returns a safe download path that will not overwrite any existing file.
// ext is an extension that will be added if the file has no extension, and for domain only URLs.
// It should include the dot.
func downloadNameFromURL(dir, u, ext string) (string, error) {
var name string
var err error
parsed, _ := url.Parse(u)
if strings.HasPrefix(u, "about:") {
name, err = getSafeDownloadName(dir, parsed.Opaque+ext, true, 0)
if err != nil {
return "", err
}
} else if parsed.Path == "" || path.Base(parsed.Path) == "/" {
// No file, just the root domain
name, err = getSafeDownloadName(dir, parsed.Hostname()+ext, true, 0)
if err != nil {
return "", err
}
} else {
// There's a specific file
name = path.Base(parsed.Path)
if !strings.Contains(name, ".") {
// No extension
name += ext
}
name, err = getSafeDownloadName(dir, name, false, 0)
if err != nil {
return "", err
}
}
return filepath.Join(dir, name), nil
}
// getSafeDownloadName is used by downloads.go only.
// It returns a modified name that is unique for the specified folder.
// This way duplicate saved files will not overwrite each other.
//
// lastDot should be set to true if the number added to the name should come before
// the last dot in the filename instead of the first.
//
// n should be set to 0, it is used for recursiveness.
func getSafeDownloadName(dir, name string, lastDot bool, n int) (string, error) {
// newName("test.txt", 3) -> "test(3).txt"
newName := func() string {
if n <= 0 {
return name
}
if lastDot {
ext := filepath.Ext(name)
return strings.TrimSuffix(name, ext) + "(" + strconv.Itoa(n) + ")" + ext
}
idx := strings.Index(name, ".")
if idx == -1 {
return name + "(" + strconv.Itoa(n) + ")"
}
return name[:idx] + "(" + strconv.Itoa(n) + ")" + name[idx:]
}
d, err := os.Open(dir)
if err != nil {
return "", err
}
files, err := d.Readdirnames(-1)
if err != nil {
d.Close()
return "", err
}
nn := newName()
for i := range files {
if nn == files[i] {
d.Close()
return getSafeDownloadName(dir, name, lastDot, n+1)
}
}
d.Close()
return nn, nil // Name doesn't exist already
}
amfora-1.10.0/display/file.go 0000644 0001750 0001750 00000006063 14575704331 015315 0 ustar nilesh nilesh package display
import (
"fmt"
"io/ioutil"
"mime"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/makeworld-the-better-one/amfora/renderer"
"github.com/makeworld-the-better-one/amfora/structs"
"github.com/spf13/viper"
)
// handleFile handles urls using file:// protocol
func handleFile(u string) (*structs.Page, bool) {
page := &structs.Page{}
uri, err := url.ParseRequestURI(u)
if err != nil {
Error("File Error", "Cannot parse URI: "+err.Error())
return page, false
}
fi, err := os.Stat(uri.Path)
if err != nil {
Error("File Error", "Cannot open local file: "+err.Error())
return page, false
}
switch mode := fi.Mode(); {
case mode.IsDir():
// Must end in slash
if u[len(u)-1] != '/' {
u += "/"
}
for _, index := range []string{"index.gmi", "index.gemini"} {
m, err := os.Stat(uri.Path + "/" + index)
if err == nil && !m.IsDir() {
return handleFile(u + index)
}
}
return createDirectoryListing(u)
case mode.IsRegular():
if fi.Size() > viper.GetInt64("a-general.page_max_size") {
Error("File Error", "Cannot open local file, exceeds page max size")
return page, false
}
mimetype := mime.TypeByExtension(filepath.Ext(uri.Path))
if strings.HasSuffix(u, ".gmi") || strings.HasSuffix(u, ".gemini") {
mimetype = "text/gemini"
}
if !strings.HasPrefix(mimetype, "text/") {
Error("File Error", "Cannot open file, not recognized as text.")
return page, false
}
content, err := ioutil.ReadFile(uri.Path)
if err != nil {
Error("File Error", "Cannot open local file: "+err.Error())
return page, false
}
if mimetype == "text/gemini" {
rendered, links := renderer.RenderGemini(string(content), textWidth(), false)
page = &structs.Page{
Mediatype: structs.TextGemini,
URL: u,
Raw: string(content),
Content: rendered,
Links: links,
TermWidth: termW,
}
} else {
page = &structs.Page{
Mediatype: structs.TextPlain,
URL: u,
Raw: string(content),
Content: renderer.RenderPlainText(string(content)),
Links: []string{},
TermWidth: termW,
}
}
}
return page, true
}
// createDirectoryListing creates a text/gemini page for a directory
// that lists all the files as links.
func createDirectoryListing(u string) (*structs.Page, bool) {
page := &structs.Page{}
uri, err := url.ParseRequestURI(u)
if err != nil {
Error("Directory Error", "Cannot parse URI: "+err.Error())
}
files, err := ioutil.ReadDir(uri.Path)
if err != nil {
Error("Directory error", "Cannot open local directory: "+err.Error())
return page, false
}
content := "Index of " + uri.Path + "\n"
content += "=> ../ ../\n"
for _, f := range files {
separator := ""
if f.IsDir() {
separator = "/"
}
content += fmt.Sprintf("=> %s%s %s%s\n", f.Name(), separator, f.Name(), separator)
}
rendered, links := renderer.RenderGemini(content, textWidth(), false)
page = &structs.Page{
Mediatype: structs.TextGemini,
URL: u,
Raw: content,
Content: rendered,
Links: links,
TermWidth: termW,
}
return page, true
}
amfora-1.10.0/display/display.go 0000644 0001750 0001750 00000037503 14575704331 016046 0 ustar nilesh nilesh package display
import (
"fmt"
"net/url"
"regexp"
"strconv"
"strings"
"code.rocketnine.space/tslocum/cview"
"github.com/gdamore/tcell/v2"
"github.com/makeworld-the-better-one/amfora/cache"
"github.com/makeworld-the-better-one/amfora/client"
"github.com/makeworld-the-better-one/amfora/config"
"github.com/makeworld-the-better-one/amfora/renderer"
"github.com/makeworld-the-better-one/amfora/structs"
"github.com/makeworld-the-better-one/go-gemini"
"github.com/muesli/termenv"
"github.com/spf13/viper"
)
var tabs []*tab // Slice of all the current browser tabs
var curTab = -1 // What tab is currently visible - index for the tabs slice (-1 means there are no tabs)
// Terminal dimensions
var termW int
var termH int
// The user input and URL display bar at the bottom
var bottomBar = cview.NewInputField()
// When the bottom bar string has a space, this regex decides whether it's
// a non-encoded URL or a search string.
// See this comment for details:
// https://github.com/makeworld-the-better-one/amfora/issues/138#issuecomment-740961292
var hasSpaceisURL = regexp.MustCompile(`[^ ]+\.[^ ].*/.`)
// Viewer for the tab primitives
// Pages are named as strings of tab numbers - so the textview for the first tab
// is held in the page named "0".
// The only pages that don't confine to this scheme are those named after modals,
// which are used to draw modals on top the current tab.
// Ex: "info", "error", "input", "yesno"
var panels = cview.NewPanels()
// Tabbed viewer for primitives
// Panels are named as strings of tab numbers - so the textview for the first tab
// is held in the page named "0".
var browser = cview.NewTabbedPanels()
// Root layout
var layout = cview.NewFlex()
var newTabPage structs.Page
var App = cview.NewApplication()
func Init(version, commit, builtBy string) {
aboutInit(version, commit, builtBy)
// Detect terminal colors for syntax highlighting
switch termenv.ColorProfile() {
case termenv.TrueColor:
renderer.TermColor = "terminal16m"
case termenv.ANSI256:
renderer.TermColor = "terminal256"
case termenv.ANSI:
renderer.TermColor = "terminal16"
case termenv.Ascii:
renderer.TermColor = ""
}
App.EnableMouse(false)
App.SetRoot(layout, true)
App.SetAfterResizeFunc(func(width int, height int) {
// Store for calculations
termW = width
termH = height
// Make sure the current tab content is reformatted when the terminal size changes
for i := range tabs {
// Overwrite all tabs with a new, differently sized, left margin
browser.AddTab(
strconv.Itoa(i),
tabs[i].label(),
makeContentLayout(tabs[i].view, leftMargin()),
)
if tabs[i] == tabs[curTab] {
// Reformat page ASAP, in the middle of loop
reformatPageAndSetView(tabs[curTab], tabs[curTab].page)
}
}
})
panels.AddPanel(PanelBrowser, browser, true, true)
helpInit()
layout.SetDirection(cview.FlexRow)
layout.AddItem(panels, 0, 1, true)
layout.AddItem(bottomBar, 1, 1, false)
if viper.GetBool("a-general.color") {
bottomBar.SetBackgroundColor(config.GetColor("bottombar_bg"))
bottomBar.SetLabelColor(config.GetColor("bottombar_label"))
bottomBar.SetFieldBackgroundColor(config.GetColor("bottombar_bg"))
bottomBar.SetFieldTextColor(config.GetColor("bottombar_text"))
browser.SetTabBackgroundColor(config.GetColor("bg"))
browser.SetTabBackgroundColorFocused(config.GetColor("tab_num"))
browser.SetTabTextColor(config.GetColor("tab_num"))
browser.SetTabTextColorFocused(config.GetColor("ColorBg"))
browser.SetTabSwitcherDivider(
"",
fmt.Sprintf("[%s:%s]|[-]", config.GetColorString("tab_divider"), config.GetColorString("bg")),
fmt.Sprintf("[%s:%s]|[-]", config.GetColorString("tab_divider"), config.GetColorString("bg")),
)
browser.Switcher.SetBackgroundColor(config.GetColor("bg"))
} else {
bottomBar.SetBackgroundColor(tcell.ColorWhite)
bottomBar.SetLabelColor(tcell.ColorBlack)
bottomBar.SetFieldBackgroundColor(tcell.ColorWhite)
bottomBar.SetFieldTextColor(tcell.ColorBlack)
browser.SetTabBackgroundColor(tcell.ColorBlack)
browser.SetTabBackgroundColorFocused(tcell.ColorWhite)
browser.SetTabTextColor(tcell.ColorWhite)
browser.SetTabTextColorFocused(tcell.ColorBlack)
browser.SetTabSwitcherDivider(
"",
"[#ffffff:#000000]|[-]",
"[#ffffff:#000000]|[-]",
)
}
bottomBar.SetDoneFunc(func(key tcell.Key) {
tab := curTab
// Reset func to set the bottomBar back to what it was before
// Use for errors.
reset := func() {
bottomBar.SetLabel("")
tabs[tab].applyAll()
App.SetFocus(tabs[tab].view)
}
//nolint:exhaustive
switch key {
case tcell.KeyEnter:
// Figure out whether it's a URL, link number, or search
// And send out a request
query := bottomBar.GetText()
if strings.TrimSpace(query) == "" {
// Ignore
reset()
return
}
if query[0] == '.' && tabs[tab].hasContent() && !tabs[tab].isAnAboutPage() {
// Relative url
current, err := url.Parse(tabs[tab].page.URL)
if err != nil {
// This shouldn't occur
return
}
if query == ".." && tabs[tab].page.URL[len(tabs[tab].page.URL)-1] != '/' {
// Support what ".." used to work like
// If on /dir/doc.gmi, got to /dir/
query = "./"
}
target, err := current.Parse(query)
if err != nil {
// Invalid relative url
return
}
URL(target.String())
return
}
i, err := strconv.Atoi(query)
if err != nil {
if strings.HasPrefix(query, "new:") && len(query) > 4 {
// They're trying to open a link number in a new tab
i, err = strconv.Atoi(query[4:])
if err != nil {
reset()
return
}
if i <= len(tabs[tab].page.Links) && i > 0 {
// Open new tab and load link
oldTab := tab
// Resolve and follow link manually
nextParsed, err := url.Parse(tabs[oldTab].page.Links[i-1])
if err != nil {
Error("URL Error", "link URL could not be parsed")
reset()
return
}
if tabs[oldTab].hasContent() && !tabs[oldTab].isAnAboutPage() {
prevParsed, _ := url.Parse(tabs[oldTab].page.URL)
NewTabWithURL(prevParsed.ResolveReference(nextParsed).String())
} else {
NewTabWithURL(nextParsed.String())
}
return
}
} else {
// It's a full URL or search term
// Detect if it's a search or URL
// Remove whitespace from the string.
// We don't want to convert legitimate
// :// links to search terms.
query := strings.TrimSpace(query)
if ((strings.Contains(query, " ") && !hasSpaceisURL.MatchString(query)) ||
(!strings.HasPrefix(query, "//") && !strings.Contains(query, "://") &&
!strings.Contains(query, ".")) && !strings.HasPrefix(query, "about:")) &&
!(query == "localhost" || strings.HasPrefix(query, "localhost/") || strings.HasPrefix(query, "localhost:")) {
// Has a space and follows regex, OR
// doesn't start with "//", contain "://", and doesn't have a dot either.
// Then it's a search
u := viper.GetString("a-general.search") + "?" + gemini.QueryEscape(query)
// Don't use the cached version of the search
cache.RemovePage(client.NormalizeURL(u))
URL(u)
} else {
// Full URL
// Don't use cached version for manually entered URL
cache.RemovePage(client.NormalizeURL(client.FixUserURL(query)))
URL(query)
}
return
}
}
if i <= len(tabs[tab].page.Links) && i > 0 {
// It's a valid link number
go followLink(tabs[tab], tabs[tab].page.URL, tabs[tab].page.Links[i-1])
return
}
// Invalid link number, don't do anything
reset()
return
case tcell.KeyEsc:
// Set back to what it was
reset()
return
}
// Other potential keys are Tab and Backtab, they are ignored
})
// Render the default new tab content ONCE and store it for later
// This code is repeated in Reload()
newTabContent := getNewTabContent()
renderedNewTabContent, newTabLinks := renderer.RenderGemini(newTabContent, textWidth(), false)
newTabPage = structs.Page{
Raw: newTabContent,
Content: renderedNewTabContent,
Links: newTabLinks,
URL: "about:newtab",
TermWidth: -1, // Force reformatting on first display
Mediatype: structs.TextGemini,
}
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
}
_, ok = App.GetFocus().(*cview.Modal)
if ok {
// It's focused on a modal right now, nothing should interrupt
return event
}
frontPanelName, _ := panels.GetFrontPanel()
if frontPanelName == PanelHelp {
// It's focused on help right now
if config.TranslateKeyEvent(event) == config.CmdQuit {
// Allow quit key to work, but nothing else
Stop()
return nil
}
// Pass everything else directly, inhibiting other keybindings
// like for editing the URL
return event
}
// To add a configurable global key command, you'll need to update one of
// the two switch statements here. You'll also need to add an enum entry in
// config/keybindings.go, update KeyInit() in config/keybindings.go, add a default
// keybinding in config/config.go and update the help panel in display/help.go
cmd := config.TranslateKeyEvent(event)
if tabs[curTab].mode == tabModeDone {
// All the keys and operations that can only work while NOT loading
//nolint:exhaustive
switch cmd {
case config.CmdReload:
Reload()
return nil
case config.CmdHome:
URL(viper.GetString("a-general.home"))
return nil
case config.CmdBottom:
// Space starts typing, like Bombadillo
bottomBar.SetLabel("[::b]URL/Num./Search: [::-]")
bottomBar.SetText("")
// Don't save bottom bar, so that whenever you switch tabs, it's not in that mode
App.SetFocus(bottomBar)
return nil
case config.CmdEdit:
// Letter e allows to edit current URL
bottomBar.SetLabel("[::b]Edit URL: [::-]")
bottomBar.SetText(tabs[curTab].page.URL)
App.SetFocus(bottomBar)
return nil
case config.CmdAddSub:
go addSubscription()
return nil
}
}
// All the keys and operations that can work while a tab IS loading
//nolint:exhaustive
switch cmd {
case config.CmdNewTab:
if tabs[curTab].page.Mode == structs.ModeLinkSelect {
next, err := resolveRelLink(tabs[curTab], tabs[curTab].page.URL, tabs[curTab].page.Selected)
if err != nil {
Error("URL Error", err.Error())
return nil
}
NewTabWithURL(next)
} else {
NewTab()
}
return nil
case config.CmdCloseTab:
CloseTab()
return nil
case config.CmdQuit:
Stop()
return nil
case config.CmdPrevTab:
// Wrap around, allow for modulo with negative numbers
n := NumTabs()
SwitchTab((((curTab - 1) % n) + n) % n)
return nil
case config.CmdNextTab:
SwitchTab((curTab + 1) % NumTabs())
return nil
case config.CmdHelp:
Help()
return nil
}
if cmd >= config.CmdTab1 && cmd <= config.CmdTab0 {
if cmd == config.CmdTab0 {
// Zero key goes to the last tab
SwitchTab(NumTabs() - 1)
} else {
SwitchTab(int(cmd - config.CmdTab1))
}
return nil
}
// Let another element handle the event, it's not a special global key
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() {
NewTabWithURL("about:newtab")
bottomBar.SetLabel("")
bottomBar.SetText("")
tabs[NumTabs()-1].saveBottomBar()
}
// NewTabWithURL opens a new tab and switches to it, displaying the
// the URL provided.
func NewTabWithURL(url string) {
// Create TextView 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
// Process current tab before making a new one
if curTab > -1 {
// Turn off link selecting mode in the current tab
tabs[curTab].view.Highlight("")
// Save bottomBar state
tabs[curTab].saveBottomBar()
}
curTab = NumTabs()
tabs = append(tabs, makeNewTab())
var interstitial string
if !strings.HasPrefix(url, "about:") {
interstitial = "Loading " + url + "..."
}
setPage(tabs[curTab], renderPageFromString(interstitial))
// Regardless of the starting URL, about:newtab will
// be the history root.
tabs[curTab].addToHistory("about:newtab")
tabs[curTab].history.pos = 0 // Manually set as first page
browser.AddTab(
strconv.Itoa(curTab),
tabs[curTab].label(),
makeContentLayout(tabs[curTab].view, leftMargin()),
)
browser.SetCurrentTab(strconv.Itoa(curTab))
App.SetFocus(tabs[curTab].view)
URL(url)
// 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
}
tabs = tabs[:len(tabs)-1]
browser.RemoveTab(strconv.Itoa(curTab))
if curTab <= 0 {
curTab = NumTabs() - 1
} else {
curTab--
}
browser.SetCurrentTab(strconv.Itoa(curTab)) // Go to previous page
// Restore previous tab's state
tabs[curTab].applyAll()
App.SetFocus(tabs[curTab].view)
// 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
}
// Save current tab attributes
if curTab > -1 {
// Save bottomBar state
tabs[curTab].saveBottomBar()
}
curTab = tab % NumTabs()
// Display tab
reformatPageAndSetView(tabs[curTab], tabs[curTab].page)
browser.SetCurrentTab(strconv.Itoa(curTab))
tabs[curTab].applyAll()
App.SetFocus(tabs[curTab].view)
// Just in case
App.Draw()
}
func Reload() {
if tabs[curTab].page.URL == "about:newtab" && config.CustomNewTab {
// Re-render new tab, similar to Init()
newTabContent := getNewTabContent()
tmpTermW := termW
renderedNewTabContent, newTabLinks := renderer.RenderGemini(newTabContent, textWidth(), false)
newTabPage = structs.Page{
Raw: newTabContent,
Content: renderedNewTabContent,
Links: newTabLinks,
URL: "about:newtab",
TermWidth: tmpTermW,
Mediatype: structs.TextGemini,
}
temp := newTabPage // Copy
setPage(tabs[curTab], &temp)
return
}
if !tabs[curTab].hasContent() {
return
}
go func(t *tab) {
cache.RemovePage(tabs[curTab].page.URL)
handleURL(t, t.page.URL, 0) // goURL is not used bc history shouldn't be added to
if t == tabs[curTab] {
// Display the bottomBar state that handleURL set
t.applyBottomBar()
}
}(tabs[curTab])
}
// URL loads and handles the provided URL for the current tab.
// It should be an absolute URL.
func URL(u string) {
t := tabs[curTab]
if strings.HasPrefix(u, "about:") {
go goURL(t, u)
} else {
go goURL(t, client.FixUserURL(u))
}
}
func RenderFromString(str string) {
t := tabs[curTab]
page := renderPageFromString(str)
setPage(t, page)
}
func renderPageFromString(str string) *structs.Page {
rendered, links := renderer.RenderGemini(str, textWidth(), false)
page := &structs.Page{
Mediatype: structs.TextGemini,
Raw: str,
Content: rendered,
Links: links,
TermWidth: termW,
}
return page
}
func NumTabs() int {
return len(tabs)
}
amfora-1.10.0/display/bookmarks.go 0000644 0001750 0001750 00000013133 14575704331 016362 0 ustar nilesh nilesh package display
import (
"fmt"
"regexp"
"strings"
"code.rocketnine.space/tslocum/cview"
"github.com/gdamore/tcell/v2"
"github.com/makeworld-the-better-one/amfora/bookmarks"
"github.com/makeworld-the-better-one/amfora/config"
"github.com/makeworld-the-better-one/amfora/renderer"
"github.com/makeworld-the-better-one/amfora/structs"
"github.com/spf13/viper"
)
// For adding and removing bookmarks, basically a clone of the input modal.
var bkmkModal = cview.NewModal()
type bkmkAction int
const (
add bkmkAction = iota
change
cancel
remove
)
// bkmkCh is for the user action
var bkmkCh = make(chan bkmkAction)
var bkmkModalText string // The current text of the input field in the modal
// Regex for extracting top level 1 heading. The title will extracted from the 1st submatch.
var topHeadingRegex = regexp.MustCompile(`(?m)^#[^#][\t ]*[^\s].*$`)
func bkmkInit() {
panels.AddPanel(PanelBookmarks, bkmkModal, false, false)
m := bkmkModal
if viper.GetBool("a-general.color") {
m.SetBackgroundColor(config.GetColor("bkmk_modal_bg"))
m.SetButtonBackgroundColor(config.GetColor("btn_bg"))
m.SetButtonTextColor(config.GetColor("btn_text"))
m.SetTextColor(config.GetColor("bkmk_modal_text"))
form := m.GetForm()
form.SetLabelColor(config.GetColor("bkmk_modal_label"))
form.SetFieldBackgroundColor(config.GetColor("bkmk_modal_field_bg"))
form.SetFieldTextColor(config.GetColor("bkmk_modal_field_text"))
form.SetFieldBackgroundColorFocused(config.GetColor("bkmk_modal_field_text"))
form.SetFieldTextColorFocused(config.GetTextColor("bkmk_modal_field_bg", "bkmk_modal_field_text"))
form.SetButtonBackgroundColorFocused(config.GetColor("btn_text"))
form.SetButtonTextColorFocused(config.GetTextColor("btn_bg", "btn_text"))
frame := m.GetFrame()
frame.SetBorderColor(config.GetColor("bkmk_modal_text"))
frame.SetTitleColor(config.GetColor("bkmk_modal_text"))
} else {
m.SetBackgroundColor(tcell.ColorBlack)
m.SetButtonBackgroundColor(tcell.ColorWhite)
m.SetButtonTextColor(tcell.ColorBlack)
m.SetTextColor(tcell.ColorWhite)
form := m.GetForm()
form.SetLabelColor(tcell.ColorWhite)
form.SetFieldBackgroundColor(tcell.ColorWhite)
form.SetFieldTextColor(tcell.ColorBlack)
form.SetButtonBackgroundColorFocused(tcell.ColorBlack)
form.SetButtonTextColorFocused(tcell.ColorWhite)
frame := m.GetFrame()
frame.SetBorderColor(tcell.ColorWhite)
frame.SetTitleColor(tcell.ColorWhite)
}
m.SetBorder(true)
frame := m.GetFrame()
frame.SetTitleAlign(cview.AlignCenter)
frame.SetTitle(" Add Bookmark ")
m.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
switch buttonLabel {
case "Add":
bkmkCh <- add
case "Change":
bkmkCh <- change
case "Remove":
bkmkCh <- remove
case "Cancel":
bkmkCh <- cancel
case "":
bkmkCh <- cancel
}
})
}
// Bkmk displays the "Add a bookmark" modal.
// It accepts the default value for the bookmark name that will be displayed, but can be changed by the user.
// It also accepts a bool indicating whether this page already has a bookmark.
// It returns the bookmark name and the bookmark action.
func openBkmkModal(name string, exists bool) (string, bkmkAction) {
// Basically a copy of Input()
// Reset buttons before input field, to make sure the input is in focus
bkmkModal.ClearButtons()
if exists {
bkmkModal.SetText("Change or remove the bookmark for the current page?")
bkmkModal.AddButtons([]string{"Change", "Remove", "Cancel"})
} else {
bkmkModal.SetText("Create a bookmark for the current page?")
bkmkModal.AddButtons([]string{"Add", "Cancel"})
}
// Remove and re-add input field - to clear the old text
bkmkModal.GetForm().Clear(false)
bkmkModalText = name
bkmkModal.GetForm().AddInputField("Name: ", name, 0, nil,
func(text string) {
// Store for use later
bkmkModalText = text
})
panels.ShowPanel(PanelBookmarks)
panels.SendToFront(PanelBookmarks)
App.SetFocus(bkmkModal)
App.Draw()
action := <-bkmkCh
panels.HidePanel(PanelBookmarks)
App.SetFocus(tabs[curTab].view)
App.Draw()
return bkmkModalText, action
}
// Bookmarks displays the bookmarks page on the current tab.
func Bookmarks(t *tab) {
bkmkPageRaw := "# Bookmarks\r\n\r\n"
// Gather bookmarks
names, urls := bookmarks.All()
for i := range names {
bkmkPageRaw += fmt.Sprintf("=> %s %s\r\n", urls[i], names[i])
}
// Render and display
content, links := renderer.RenderGemini(bkmkPageRaw, textWidth(), false)
page := structs.Page{
Raw: bkmkPageRaw,
Content: content,
Links: links,
URL: "about:bookmarks",
TermWidth: termW,
Mediatype: structs.TextGemini,
}
setPage(t, &page)
t.applyBottomBar()
}
// addBookmark goes through the process of adding a bookmark for the current page.
// It is the high-level way of doing it. It should be called in a goroutine.
// It can also be called to edit an existing bookmark.
func addBookmark() {
t := tabs[curTab]
p := t.page
if !t.hasContent() || t.isAnAboutPage() {
// It's an about: page, or a malformed one
return
}
name, exists := bookmarks.Get(p.URL)
// Retrieve & use top level 1 heading for name if bookmark does not already exist.
if !exists {
match := topHeadingRegex.FindString(p.Raw)
if match != "" {
name = strings.TrimSpace(match[1:])
}
}
// Open a bookmark modal with the current name of the bookmark, if it exists
// otherwise use the top level 1 heading as a suggested name
newName, action := openBkmkModal(name, exists)
//nolint:exhaustive
switch action {
case add:
bookmarks.Add(p.URL, newName)
case change:
bookmarks.Change(p.URL, newName)
case remove:
bookmarks.Remove(p.URL)
}
// Other case is action == cancel, so nothing needs to happen
}
amfora-1.10.0/display/license.go 0000644 0001750 0001750 00000104622 14575704331 016020 0 ustar nilesh nilesh package display
//go:generate ./license.sh
var license = []byte(` GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Copyright (C)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Copyright (C)
This program comes with ABSOLUTELY NO WARRANTY; for details type 'show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type 'show c' for details.
The hypothetical commands 'show w' and 'show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
.
`)
amfora-1.10.0/display/subscriptions.go 0000644 0001750 0001750 00000023516 14575704331 017307 0 ustar nilesh nilesh package display
import (
"fmt"
"net/url"
"path"
"sort"
"strconv"
"strings"
"time"
"github.com/gdamore/tcell/v2"
"github.com/makeworld-the-better-one/amfora/cache"
"github.com/makeworld-the-better-one/amfora/config"
"github.com/makeworld-the-better-one/amfora/renderer"
"github.com/makeworld-the-better-one/amfora/structs"
"github.com/makeworld-the-better-one/amfora/subscriptions"
"github.com/makeworld-the-better-one/go-gemini"
"github.com/mmcdole/gofeed"
"github.com/spf13/viper"
)
// Map page number (zero-indexed) to the time it was made at.
// This allows for caching the pages until there's an update.
var subscriptionPageUpdated = make(map[int]time.Time)
// toLocalDay truncates the provided time to a date only,
// but converts to the local time first.
func toLocalDay(t time.Time) time.Time {
t = t.Local()
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
}
// Subscriptions displays the subscriptions page on the current tab.
func Subscriptions(t *tab, u string) string {
pageN := 0 // Pages are zero-indexed internally
// Correct URL if query string exists
// The only valid query string is an int above 1.
// Anything "redirects" to the first page, with no query string.
// This is done over just serving the first page content for
// invalid query strings so that there won't be duplicate caches.
correctURL := func(u2 string) string {
if len(u2) > 20 && u2[:20] == "about:subscriptions?" {
query, err := gemini.QueryUnescape(u2[20:])
if err != nil {
return "about:subscriptions"
}
// Valid query string
i, err := strconv.Atoi(query)
if err != nil {
// Not an int
return "about:subscriptions"
}
if i < 2 {
return "about:subscriptions"
}
// Valid int above 1
pageN = i - 1 // Pages are zero-indexed internally
return u2
}
return u2
}
u = correctURL(u)
// Retrieve cached version if there hasn't been any updates
p, ok := cache.GetPage(u)
if subscriptionPageUpdated[pageN].After(subscriptions.LastUpdated) && ok {
setPage(t, p)
t.applyBottomBar()
return u
}
pe := subscriptions.GetPageEntries()
// Figure out where the entries for this page start, if at all.
epp := viper.GetInt("subscriptions.entries_per_page")
if epp <= 0 {
epp = 1
}
start := pageN * epp // Index of the first page entry to be displayed
end := start + epp
if end > len(pe.Entries) {
end = len(pe.Entries)
}
var rawPage string
if pageN == 0 {
rawPage = "# Subscriptions\n\n" + rawPage
} else {
rawPage = fmt.Sprintf("# Subscriptions (page %d)\n\n", pageN+1) + rawPage
}
if start > len(pe.Entries)-1 && len(pe.Entries) != 0 {
// The page is out of range, doesn't exist
rawPage += "This page does not exist.\n\n=> about:subscriptions Subscriptions\n"
} else {
// Render page
if viper.GetBool("subscriptions.header") {
rawPage += "You can use Ctrl-X to subscribe to a page, or to an Atom/RSS/JSON feed." +
"See the online wiki for more.\n" +
"If you just opened Amfora then updates may appear incrementally. Reload the page to see them.\n\n"
}
rawPage += "=> about:manage-subscriptions Manage subscriptions\n\n"
// curDay represents what day of posts the loop is on.
// It only goes backwards in time.
// Its initial setting means:
// Only display posts older than 26 hours in the future, nothing further in the future.
//
// 26 hours was chosen because it is the largest timezone difference
// currently in the world. Posts may be dated in the future
// due to software bugs, where the local user's date is used, but
// the UTC timezone is specified. Gemfeed does this at the time of
// writing, but will not after #3 gets merged on its repo. Still,
// the older version will be used for a while.
curDay := toLocalDay(time.Now()).Add(26 * time.Hour)
for _, entry := range pe.Entries[start:end] { // From new to old
// Convert to local time, remove sub-day info
pub := toLocalDay(entry.Published)
if pub.Before(curDay) {
// This post is on a new day, add a day header
curDay = pub
rawPage += fmt.Sprintf("\n## %s\n\n", curDay.Format("Jan 02, 2006"))
}
if entry.Title == "" || entry.Title == "/" {
// Just put author/title
// Mainly used for when you're tracking the root domain of a site
rawPage += fmt.Sprintf("=>%s %s\n", entry.URL, entry.Prefix)
} else {
// Include title and dash
rawPage += fmt.Sprintf("=>%s %s - %s\n", entry.URL, entry.Prefix, entry.Title)
}
}
if pageN == 0 && len(pe.Entries) > epp {
// First page, and there's more than can fit
rawPage += "\n\n=> about:subscriptions?2 Next Page\n"
} else if pageN > 0 {
// A later page
rawPage += fmt.Sprintf(
"\n\n=> about:subscriptions?%d Previous Page\n",
pageN, // pageN is zero-indexed but the query string is one-indexed
)
if end != len(pe.Entries) {
// There's more
rawPage += fmt.Sprintf("=> about:subscriptions?%d Next Page\n", pageN+2)
}
}
}
content, links := renderer.RenderGemini(rawPage, textWidth(), false)
page := structs.Page{
Raw: rawPage,
Content: content,
Links: links,
URL: u,
TermWidth: termW,
Mediatype: structs.TextGemini,
}
go cache.AddPage(&page)
setPage(t, &page)
t.applyBottomBar()
subscriptionPageUpdated[pageN] = time.Now()
return u
}
// ManageSubscriptions displays the subscription managing page in
// the current tab. `u` is the URL entered by the user.
func ManageSubscriptions(t *tab, u string) {
if len(u) > 27 && u[:27] == "about:manage-subscriptions?" {
// There's a query string, aka a URL to unsubscribe from
manageSubscriptionQuery(t, u)
return
}
rawPage := "# Manage Subscriptions\n\n" +
"Below is list of URLs you are subscribed to, both feeds and pages. " +
"Navigate to the link to unsubscribe from that feed or page.\n\n"
urls := subscriptions.AllURLS()
sort.Strings(urls)
for _, u2 := range urls {
rawPage += fmt.Sprintf(
"=>%s %s\n",
"about:manage-subscriptions?"+gemini.QueryEscape(u2),
u2,
)
}
content, links := renderer.RenderGemini(rawPage, textWidth(), false)
page := structs.Page{
Raw: rawPage,
Content: content,
Links: links,
URL: "about:manage-subscriptions",
TermWidth: termW,
Mediatype: structs.TextGemini,
}
go cache.AddPage(&page)
setPage(t, &page)
t.applyBottomBar()
}
func manageSubscriptionQuery(t *tab, u string) {
sub, err := gemini.QueryUnescape(u[27:])
if err != nil {
Error("URL Error", "Invalid query string: "+err.Error())
return
}
err = subscriptions.Remove(sub)
if err != nil {
ManageSubscriptions(t, "about:manage-subscriptions") // Reload
Error("Save Error", "Error saving the unsubscription to disk: "+err.Error())
return
}
ManageSubscriptions(t, "about:manage-subscriptions") // Reload
Info("Unsubscribed from " + sub)
}
// openSubscriptionModal displays the "Add subscription" modal
// It returns whether the user wanted to subscribe to feed/page.
// The subscribed arg specifies whether this feed/page is already
// subscribed to.
func openSubscriptionModal(validFeed, subscribed bool) bool {
// Reuses yesNoModal
if viper.GetBool("a-general.color") {
m := yesNoModal
m.SetBackgroundColor(config.GetColor("subscription_modal_bg"))
m.SetTextColor(config.GetColor("subscription_modal_text"))
frame := yesNoModal.GetFrame()
frame.SetBorderColor(config.GetColor("subscription_modal_text"))
frame.SetTitleColor(config.GetColor("subscription_modal_text"))
} else {
m := yesNoModal
m.SetBackgroundColor(tcell.ColorBlack)
m.SetTextColor(tcell.ColorWhite)
frame := yesNoModal.GetFrame()
frame.SetBorderColor(tcell.ColorWhite)
frame.SetTitleColor(tcell.ColorWhite)
}
if validFeed {
yesNoModal.GetFrame().SetTitle("Feed Subscription")
if subscribed {
yesNoModal.SetText("You are already subscribed to this feed. Would you like to manually update it?")
} else {
yesNoModal.SetText("Would you like to subscribe to this feed?")
}
} else {
yesNoModal.GetFrame().SetTitle("Page Subscription")
if subscribed {
yesNoModal.SetText("You are already subscribed to this page. Would you like to manually update it?")
} else {
yesNoModal.SetText("Would you like to subscribe to this page?")
}
}
panels.ShowPanel(PanelYesNoModal)
panels.SendToFront(PanelYesNoModal)
App.SetFocus(yesNoModal)
App.Draw()
resp := <-yesNoCh
panels.HidePanel(PanelYesNoModal)
App.SetFocus(tabs[curTab].view)
App.Draw()
return resp
}
// getFeedFromPage is like subscriptions.GetFeed but takes a structs.Page as input.
func getFeedFromPage(p *structs.Page) (*gofeed.Feed, bool) {
parsed, _ := url.Parse(p.URL)
filename := path.Base(parsed.Path)
r := strings.NewReader(p.Raw)
return subscriptions.GetFeed(p.RawMediatype, filename, r)
}
// addFeedDirect is only for adding feeds, not pages.
// It's for when you already have a feed and know if it's tracked.
// Used mainly by handleURL because it already did a lot of the work.
// It returns a bool indicating whether the user actually wanted to
// add the feed or not.
//
// Like addFeed, it should be called in a goroutine.
func addFeedDirect(u string, feed *gofeed.Feed, tracked bool) bool {
if openSubscriptionModal(true, tracked) {
err := subscriptions.AddFeed(u, feed)
if err != nil {
Error("Feed Error", err.Error())
}
return true
}
return false
}
// addFeed goes through the process of subscribing to the current page/feed.
// It is the high-level way of doing it. It should be called in a goroutine.
func addSubscription() {
t := tabs[curTab]
p := t.page
if !t.hasContent() || t.isAnAboutPage() {
// It's an about: page, or a malformed one
return
}
feed, isFeed := getFeedFromPage(p)
tracked := subscriptions.IsSubscribed(p.URL)
if openSubscriptionModal(isFeed, tracked) {
var err error
if isFeed {
err = subscriptions.AddFeed(p.URL, feed)
} else {
err = subscriptions.AddPage(p.URL, strings.NewReader(p.Raw))
}
if err != nil {
Error("Feed/Page Error", err.Error())
}
}
}
amfora-1.10.0/display/tab.go 0000644 0001750 0001750 00000036153 14575704331 015147 0 ustar nilesh nilesh package display
import (
"fmt"
"net/url"
"path"
"strconv"
"strings"
"code.rocketnine.space/tslocum/cview"
"github.com/atotto/clipboard"
"github.com/gdamore/tcell/v2"
"github.com/makeworld-the-better-one/amfora/config"
"github.com/makeworld-the-better-one/amfora/structs"
)
type tabMode int
const (
tabModeDone tabMode = iota
tabModeLoading
)
// tabHistoryPageCache is fields from the Page struct, cached here to solve #122
// See structs/structs.go for an explanation of the fields.
type tabHistoryPageCache struct {
row int
column int
selected string
selectedID string
mode structs.PageMode
}
type tabHistory struct {
urls []string
pos int // Position: where in the list of URLs we are
pageCache []*tabHistoryPageCache
}
// tab hold the information needed for each browser tab.
type tab struct {
page *structs.Page
view *cview.TextView
history *tabHistory
mode tabMode
barLabel string // The bottomBar label for the tab
barText string // The bottomBar text for the tab
preferURLHandler bool // For #143, use URL handler over proxy
}
// makeNewTab initializes an tab struct with no content.
func makeNewTab() *tab {
t := tab{
page: &structs.Page{Mode: structs.ModeOff},
view: cview.NewTextView(),
history: &tabHistory{},
mode: tabModeDone,
}
t.view.SetDynamicColors(true)
t.view.SetRegions(true)
t.view.SetScrollable(true)
t.view.SetWrap(false)
t.view.SetScrollBarVisibility(config.ScrollBar)
t.view.SetScrollBarColor(config.GetColor("scrollbar"))
t.view.SetChangedFunc(func() {
App.Draw()
})
t.view.SetDoneFunc(func(key tcell.Key) {
// Altered from:
// https://gitlab.com/tslocum/cview/-/blob/1f765c8695c3f4b35dae57f469d3aee0b1adbde7/demos/textview/main.go
// Handles being able to select and "click" links with the enter and tab keys
tab := curTab // Don't let it change in the middle of the code
if tabs[tab].mode != tabModeDone {
return
}
if key == tcell.KeyEsc {
// Stop highlighting
bottomBar.SetLabel("")
bottomBar.SetText(tabs[tab].page.URL)
tabs[tab].clearSelected()
tabs[tab].saveBottomBar()
return
}
if len(tabs[tab].page.Links) == 0 {
// No links on page
return
}
currentSelection := tabs[tab].view.GetHighlights()
numSelections := len(tabs[tab].page.Links)
if key == tcell.KeyEnter && len(currentSelection) > 0 {
// A link is selected and enter was pressed: "click" it and load the page it's for
bottomBar.SetLabel("")
linkN, _ := strconv.Atoi(currentSelection[0])
tabs[tab].page.Selected = tabs[tab].page.Links[linkN]
tabs[tab].page.SelectedID = currentSelection[0]
tabs[tab].preferURLHandler = false // Reset in case
go followLink(tabs[tab], tabs[tab].page.URL, tabs[tab].page.Links[linkN])
return
}
if len(currentSelection) == 0 && (key == tcell.KeyEnter || key == tcell.KeyTab) {
// They've started link highlighting
tabs[tab].page.Mode = structs.ModeLinkSelect
tabs[tab].view.Highlight("0")
tabs[tab].scrollToHighlight()
// Display link URL in bottomBar
bottomBar.SetLabel("[::b]Link: [::-]")
bottomBar.SetText(tabs[tab].page.Links[0])
tabs[tab].saveBottomBar()
tabs[tab].page.Selected = tabs[tab].page.Links[0]
tabs[tab].page.SelectedID = "0"
}
if len(currentSelection) > 0 {
// There's still a selection, but a different key was pressed, not Enter
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
}
tabs[tab].view.Highlight(strconv.Itoa(index))
tabs[tab].scrollToHighlight()
// Display link URL in bottomBar
bottomBar.SetLabel("[::b]Link: [::-]")
bottomBar.SetText(tabs[tab].page.Links[index])
tabs[tab].saveBottomBar()
tabs[tab].page.Selected = tabs[tab].page.Links[index]
tabs[tab].page.SelectedID = strconv.Itoa(index)
}
})
t.view.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
// Capture scrolling and change the left margin size accordingly, see #197
// This was also touched by #222
// This also captures any tab-specific events now
if t.mode != tabModeDone {
// Any events that should be caught when the tab is loading is handled in display.go
return nil
}
cmd := config.TranslateKeyEvent(event)
// Cmds that aren't single row/column scrolling
//nolint:exhaustive
switch cmd {
case config.CmdBookmarks:
Bookmarks(&t)
t.addToHistory("about:bookmarks")
return nil
case config.CmdAddBookmark:
go addBookmark()
return nil
case config.CmdPgup:
t.pageUp()
return nil
case config.CmdPgdn:
t.pageDown()
return nil
case config.CmdSave:
if t.hasContent() {
savePath, err := downloadPage(t.page)
if err != nil {
go Error("Download Error", fmt.Sprintf("Error saving page content: %v", err))
} else {
go Info(fmt.Sprintf("Page content saved to %s. ", savePath))
}
} else {
go Info("The current page has no content, so it couldn't be downloaded.")
}
return nil
case config.CmdBack:
histBack(&t)
return nil
case config.CmdForward:
histForward(&t)
return nil
case config.CmdSub:
Subscriptions(&t, "about:subscriptions")
tabs[curTab].addToHistory("about:subscriptions")
return nil
case config.CmdCopyPageURL:
currentURL := tabs[curTab].page.URL
err := clipboard.WriteAll(currentURL)
if err != nil {
go Error("Copy Error", err.Error())
return nil
}
return nil
case config.CmdCopyTargetURL:
currentURL := t.page.URL
selectedURL := t.highlightedURL()
if selectedURL == "" {
return nil
}
u, _ := url.Parse(currentURL)
copiedURL, err := u.Parse(selectedURL)
if err != nil {
err := clipboard.WriteAll(selectedURL)
if err != nil {
go Error("Copy Error", err.Error())
return nil
}
return nil
}
err = clipboard.WriteAll(copiedURL.String())
if err != nil {
go Error("Copy Error", err.Error())
return nil
}
return nil
case config.CmdURLHandlerOpen:
currentSelection := t.view.GetHighlights()
t.preferURLHandler = true
// Copied code from when enter key is pressed
if len(currentSelection) > 0 {
bottomBar.SetLabel("")
linkN, _ := strconv.Atoi(currentSelection[0])
t.page.Selected = t.page.Links[linkN]
t.page.SelectedID = currentSelection[0]
go followLink(&t, t.page.URL, t.page.Links[linkN])
}
return nil
}
// Number key: 1-9, 0, LINK1-LINK10
if cmd >= config.CmdLink1 && cmd <= config.CmdLink0 {
if int(cmd) <= len(t.page.Links) {
// It's a valid link number
t.preferURLHandler = false // Reset in case
go followLink(&t, t.page.URL, t.page.Links[cmd-1])
return nil
}
}
// Scrolling stuff
// Copied in scrollTo
key := event.Key()
mod := event.Modifiers()
height, width := t.view.GetBufferSize()
_, _, boxW, boxH := t.view.GetInnerRect()
// Make boxW accurate by subtracting one if a scrollbar is covering the last
// column of text
if config.ScrollBar == cview.ScrollBarAlways ||
(config.ScrollBar == cview.ScrollBarAuto && height > boxH) {
boxW--
}
if cmd == config.CmdMoveRight || (key == tcell.KeyRight && mod == tcell.ModNone) {
// Scrolling to the right
if t.page.Column >= leftMargin() {
// Scrolled right far enought that no left margin is needed
if (t.page.Column-leftMargin())+boxW >= width {
// And scrolled as far as possible to the right
return nil
}
} else {
// Left margin still exists
if boxW-(leftMargin()-t.page.Column) >= width {
// But still scrolled as far as possible
return nil
}
}
t.page.Column++
} else if cmd == config.CmdMoveLeft || (key == tcell.KeyLeft && mod == tcell.ModNone) {
// Scrolling to the left
if t.page.Column == 0 {
// Can't scroll to the left anymore
return nil
}
t.page.Column--
} else if cmd == config.CmdMoveUp || (key == tcell.KeyUp && mod == tcell.ModNone) {
// Scrolling up
if t.page.Row > 0 {
t.page.Row--
}
return event
} else if cmd == config.CmdMoveDown || (key == tcell.KeyDown && mod == tcell.ModNone) {
// Scrolling down
if t.page.Row < height {
t.page.Row++
}
return event
} else if cmd == config.CmdBeginning {
t.page.Row = 0
// This is required because cview will also set the column (incorrectly)
// if it handles this event itself
t.applyScroll()
App.Draw()
return nil
} else if cmd == config.CmdEnd {
t.page.Row = height
t.applyScroll()
App.Draw()
return nil
} else {
// Some other key, stop processing it
return event
}
t.applyHorizontalScroll()
App.Draw()
return nil
})
return &t
}
// historyCachePage caches certain info about the current page in the tab's history,
// see #122 for details.
func (t *tab) historyCachePage() {
if t.page == nil || t.page.URL == "" || t.history.pageCache == nil || len(t.history.pageCache) == 0 {
return
}
t.history.pageCache[t.history.pos] = &tabHistoryPageCache{
row: t.page.Row,
column: t.page.Column,
selected: t.page.Selected,
selectedID: t.page.SelectedID,
mode: t.page.Mode,
}
}
// addToHistory adds the given URL to history.
// It assumes the URL is currently being loaded and displayed on the page.
func (t *tab) addToHistory(u string) {
if t.history.pos < len(t.history.urls)-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
t.history.urls = t.history.urls[:t.history.pos+1]
// Same for page cache
t.history.pageCache = t.history.pageCache[:t.history.pos+1]
}
t.history.urls = append(t.history.urls, u)
t.history.pos++
// Cache page info for #122
t.history.pageCache = append(t.history.pageCache, &tabHistoryPageCache{}) // Add new spot
t.historyCachePage() // Fill it with data
}
// pageUp scrolls up 75% of the height of the terminal, like Bombadillo.
func (t *tab) pageUp() {
t.page.Row -= termH / 2
if t.page.Row < 0 {
t.page.Row = 0
}
t.applyScroll()
}
// pageDown scrolls down 75% of the height of the terminal, like Bombadillo.
func (t *tab) pageDown() {
height, _ := t.view.GetBufferSize()
t.page.Row += termH / 2
if t.page.Row > height {
t.page.Row = height
}
t.applyScroll()
}
// hasContent returns false when the tab's page is malformed,
// has no content or URL.
func (t *tab) hasContent() bool {
if t.page == nil || t.view == nil {
return false
}
if t.page.URL == "" {
return false
}
if t.page.Content == "" {
return false
}
return true
}
// isAnAboutPage returns true when the tab's page is an about page
func (t *tab) isAnAboutPage() bool {
return strings.HasPrefix(t.page.URL, "about:")
}
// applyHorizontalScroll handles horizontal scroll logic including left margin resizing,
// see #197 for details. Use applyScroll instead.
//
// In certain cases it will still use and apply the saved Row.
func (t *tab) applyHorizontalScroll() {
i := tabNumber(t)
if i == -1 {
// Tab is not actually being used and should not be (re)added to the browser
return
}
if t.page.Column >= leftMargin() {
// Scrolled to the right far enough that no left margin is needed
browser.AddTab(
strconv.Itoa(i),
t.label(),
makeContentLayout(t.view, 0),
)
t.view.ScrollTo(t.page.Row, t.page.Column-leftMargin())
} else {
// Left margin is still needed, but is not necessarily at the right size by default
browser.AddTab(
strconv.Itoa(i),
t.label(),
makeContentLayout(t.view, leftMargin()-t.page.Column),
)
}
}
// applyScroll applies the saved scroll values to the page and tab.
// It should only be used when going backward and forward.
func (t *tab) applyScroll() {
t.view.ScrollTo(t.page.Row, 0)
t.applyHorizontalScroll()
}
// scrollTo scrolls the current tab to specified position. Like
// cview.TextView.ScrollTo but using the custom scrolling logic required by #196.
func (t *tab) scrollTo(row, col int) {
height, width := t.view.GetBufferSize()
// Keep row and col within limits
if row < 0 {
row = 0
} else if row > height {
row = height
}
if col < 0 {
col = 0
} else if col > width {
col = width
}
t.page.Row = row
t.page.Column = col
t.applyScroll()
App.Draw()
}
// scrollToHighlight scrolls the current tab to specified position. Like
// cview.TextView.ScrollToHighlight but using the custom scrolling logic
// required by #196.
func (t *tab) scrollToHighlight() {
t.view.ScrollToHighlight()
App.Draw()
t.scrollTo(t.view.GetScrollOffset())
}
// saveBottomBar saves the current bottomBar values in the tab.
func (t *tab) saveBottomBar() {
t.barLabel = bottomBar.GetLabel()
t.barText = bottomBar.GetText()
}
// applyBottomBar sets the bottomBar using the stored tab values
func (t *tab) applyBottomBar() {
bottomBar.SetLabel(t.barLabel)
bottomBar.SetText(t.barText)
}
// clearSelected turns off any selection that was going on.
// It does not affect the bottomBar.
func (t *tab) clearSelected() {
t.page.Mode = structs.ModeOff
t.page.Selected = ""
t.page.SelectedID = ""
t.view.Highlight("")
}
// applySelected selects whatever is stored as the selected element in the struct,
// and sets the mode accordingly.
// It is safe to call if nothing was selected previously.
//
// applyBottomBar should be called after, as this func might set some bottomBar values.
func (t *tab) applySelected() {
if t.page.Mode == structs.ModeOff {
// Just in case
t.page.Selected = ""
t.page.SelectedID = ""
t.view.Highlight("")
return
} else if t.page.Mode == structs.ModeLinkSelect {
t.view.Highlight(t.page.SelectedID)
if t.mode == tabModeDone {
// Page is not loading so bottomBar can change
t.barLabel = "[::b]Link: [::-]"
t.barText = t.page.Selected
}
}
}
// applyAll uses applyScroll and applySelected to put a tab's TextView back the way it was.
// It also uses applyBottomBar if this is the current tab.
func (t *tab) applyAll() {
t.applySelected()
t.applyScroll()
if t == tabs[curTab] {
t.applyBottomBar()
}
}
// highlightedURL returns the currently selected URL
func (t *tab) highlightedURL() string {
currentSelection := tabs[curTab].view.GetHighlights()
if len(currentSelection) > 0 {
linkN, _ := strconv.Atoi(currentSelection[0])
selectedURL := tabs[curTab].page.Links[linkN]
return selectedURL
}
return ""
}
// label returns the label to use for the tab name
func (t *tab) label() string {
tn := tabNumber(t)
if tn < 0 {
// Invalid tab, shouldn't happen
return ""
}
// Increment so there's no tab 0 in the label
tn++
if t.page.URL == "" || t.page.URL == "about:newtab" {
// Just use tab number
// Spaces around to keep original Amfora look
return fmt.Sprintf(" %d ", tn)
}
if strings.HasPrefix(t.page.URL, "about:") {
// Don't look for domain, put the whole URL except query strings
return strings.SplitN(t.page.URL, "?", 2)[0]
}
if strings.HasPrefix(t.page.URL, "file://") {
// File URL, use file or folder as tab name
return cview.Escape(path.Base(t.page.URL[7:]))
}
// Otherwise, it's a Gemini URL
pu, err := url.Parse(t.page.URL)
if err != nil {
return fmt.Sprintf(" %d ", tn)
}
return pu.Host
}
amfora-1.10.0/cache/ 0000755 0001750 0001750 00000000000 14575704331 013440 5 ustar nilesh nilesh amfora-1.10.0/cache/redir.go 0000644 0001750 0001750 00000002407 14575704331 015077 0 ustar nilesh nilesh package cache
import "sync"
// Functions for caching redirects.
var redirUrls = make(map[string]string) // map original URL to redirect
var redirMu = sync.RWMutex{}
// AddRedir adds a original-to-redirect pair to the cache.
func AddRedir(og, redir string) {
redirMu.Lock()
defer redirMu.Unlock()
for k, v := range redirUrls {
if og == v {
// The original URL param is the redirect URL for `k`.
// This means there is a chain: k -> og -> redir
// The chain should be removed
redirUrls[k] = redir
}
if redir == k {
// There's a loop
// The newer version is preferred
delete(redirUrls, k)
}
}
redirUrls[og] = redir
}
// ClearRedirs removes all redirects from the cache.
func ClearRedirs() {
redirMu.Lock()
redirUrls = make(map[string]string)
redirMu.Unlock()
}
// Redirect takes the provided URL and returns a redirected version, if a redirect
// exists for that URL in the cache.
// If one does not then the original URL is returned.
func Redirect(u string) string {
redirMu.RLock()
defer redirMu.RUnlock()
// A single lookup is enough, because AddRedir
// removes loops and chains.
redir, ok := redirUrls[u]
if ok {
return redir
}
return u
}
func NumRedirs() int {
redirMu.RLock()
defer redirMu.RUnlock()
return len(redirUrls)
}
amfora-1.10.0/cache/page_test.go 0000644 0001750 0001750 00000003012 14575704331 015736 0 ustar nilesh nilesh 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"}
func reset() {
ClearPages()
SetMaxPages(0)
SetMaxSize(0)
}
func TestMaxPages(t *testing.T) {
reset()
SetMaxPages(1)
AddPage(&p)
AddPage(&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())
AddPage(&p)
assert.Equal(1, NumPages(), "one page should be added")
AddPage(&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()
AddPage(&p)
RemovePage(p.URL)
assert.Equal(t, 0, NumPages(), "there shouldn't be any pages after the removal")
}
func TestClearAndNumPages(t *testing.T) {
reset()
AddPage(&p)
ClearPages()
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()
AddPage(&p)
assert.Equal(t, p.Size(), SizePages(), "sizes should match")
}
func TestGet(t *testing.T) {
reset()
AddPage(&p)
AddPage(&p2)
page, ok := GetPage(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")
}
}
amfora-1.10.0/cache/page.go 0000644 0001750 0001750 00000006362 14575704331 014712 0 ustar nilesh nilesh // Package cache provides an interface for a cache of strings, aka text/gemini pages, and redirects.
// It is fully thread safe.
package cache
import (
"sync"
"time"
"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 mu = sync.RWMutex{}
var timeout = time.Duration(0)
// 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 page cache can be, in bytes.
// A value <= 0 means infinite size.
func SetMaxSize(max int) {
maxSize = max
}
// SetTimeout sets the max number of a seconds a page can still
// be valid for. A value <= 0 means forever.
func SetTimeout(t int) {
if t <= 0 {
timeout = time.Duration(0)
return
}
timeout = time.Duration(t) * time.Second
}
func removeIndex(s []string, i int) []string {
s[len(s)-1], s[i] = s[i], s[len(s)-1]
return s[:len(s)-1]
}
func removeURL(url string) {
for i := range urls {
if urls[i] == url {
urls = removeIndex(urls, i)
return
}
}
}
// AddPage 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 AddPage(p *structs.Page) {
if p.URL == "" {
// Just in case, these pages shouldn't be cached
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 {
RemovePage(urls[0])
}
// Do the same but for cache size
for SizePages()+p.Size() > maxSize && maxSize > 0 {
RemovePage(urls[0])
}
mu.Lock()
defer mu.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)
}
// RemovePage will remove a page from the cache.
// Even if the page doesn't exist there will be no error.
func RemovePage(url string) {
mu.Lock()
defer mu.Unlock()
delete(pages, url)
removeURL(url)
}
// ClearPages removes all pages from the cache.
func ClearPages() {
mu.Lock()
defer mu.Unlock()
pages = make(map[string]*structs.Page)
urls = make([]string, 0)
}
// SizePages returns the approx. current size of the cache in bytes.
func SizePages() int {
mu.RLock()
defer mu.RUnlock()
n := 0
for _, page := range pages {
n += page.Size()
}
return n
}
func NumPages() int {
mu.RLock()
defer mu.RUnlock()
return len(pages)
}
// GetPage returns the page struct, and a bool indicating if the page was in the cache or not.
// (nil, false) is returned if the page isn't in the cache.
func GetPage(url string) (*structs.Page, bool) {
mu.RLock()
defer mu.RUnlock()
p, ok := pages[url]
if ok && (timeout == 0 || time.Since(p.MadeAt) < timeout) {
return p, ok
}
return nil, false
}
amfora-1.10.0/cache/redir_test.go 0000644 0001750 0001750 00000001006 14575704331 016130 0 ustar nilesh nilesh package cache
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestAddRedir(t *testing.T) {
ClearRedirs()
AddRedir("A", "B")
assert.Equal(t, "B", Redirect("A"), "A redirects to B")
// Chain
AddRedir("B", "C")
assert.Equal(t, "C", Redirect("B"), "B redirects to C")
assert.Equal(t, "C", Redirect("A"), "A now redirects to C too")
// Loop
ClearRedirs()
AddRedir("A", "B")
AddRedir("B", "A")
assert.Equal(t, "A", Redirect("B"), "B redirects to A - most recent version of loop is used")
}
amfora-1.10.0/.gitignore 0000644 0001750 0001750 00000007265 14575704331 014377 0 ustar nilesh nilesh # Binaries
amfora
amfora.exe
amfora-*
build
dist
# Recording
rec.yml
# Test logs
*.log
# GIMP files
*.xcf
# Created by https://www.toptal.com/developers/gitignore/api/code,go,linux,macos,windows,python
# Edit at https://www.toptal.com/developers/gitignore?templates=code,go,linux,macos,windows,python
### Code ###
.vscode/*
!.vscode/tasks.json
!.vscode/launch.json
*.code-workspace
### 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
# Dependency directories (remove the comment below to include it)
# vendor/
### 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
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
pytestdebug.log
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
doc/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
pythonenv*
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# profiling data
.prof
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# 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,python
amfora-1.10.0/go.sum 0000644 0001750 0001750 00000157442 14575704331 013545 0 ustar nilesh nilesh 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.44.3/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 v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
code.rocketnine.space/tslocum/cbind v0.1.5 h1:i6NkeLLNPNMS4NWNi3302Ay3zSU6MrqOT+yJskiodxE=
code.rocketnine.space/tslocum/cbind v0.1.5/go.mod h1:LtfqJTzM7qhg88nAvNhx+VnTjZ0SXBJtxBObbfBWo/M=
code.rocketnine.space/tslocum/cview v1.5.6-0.20210530175404-7e8817f20bdc h1:nAcBp7ZCWHpa8fHpynCbULDTAZgPQv28+Z+QnhnFG7E=
code.rocketnine.space/tslocum/cview v1.5.6-0.20210530175404-7e8817f20bdc/go.mod h1:KBRxzIsj8bfgFpnMpkGVoxsrPUvnQsRnX29XJ2yzB6M=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
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/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
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/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
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/v2 v2.2.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
github.com/gdamore/tcell/v2 v2.3.3/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
github.com/gdamore/tcell/v2 v2.6.0 h1:OKbluoP9VYmJwZwq/iLb4BxwKcwGthaa1YNBJIyCySg=
github.com/gdamore/tcell/v2 v2.6.0/go.mod h1:be9omFATkdr0D9qewWW3d+MEvl5dha+Etb5y65J2H8Y=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/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/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
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/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
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/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
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/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/makeworld-the-better-one/go-gemini v0.13.1 h1:qStBcQhgE29ViPCwCAyW65ibqeIEeyUV8TSp8hHJRkU=
github.com/makeworld-the-better-one/go-gemini v0.13.1/go.mod h1:SL62NFyZi6zcjtGwBc1euN1S3x/MHgcYdA/Ninrnwmo=
github.com/makeworld-the-better-one/go-gemini-socks5 v1.0.0 h1:D2o1rIfP/KOxcL3m3rzo4cfWNqfcGaMIhnU0keJc1+o=
github.com/makeworld-the-better-one/go-gemini-socks5 v1.0.0/go.mod h1:mfPK9BfBAAyLKuxPEbZi8mgrGmVlzMKVTGElVspuVR8=
github.com/makeworld-the-better-one/rr v1.0.0 h1:NclI3Z32Q/+kNzP8OOlpPFuYeN0BFGgKU0MLd9ZmfQQ=
github.com/makeworld-the-better-one/rr v1.0.0/go.mod h1:sd3i5WAdkx/7ALu3V6AbVUyDw8uqmDQv55LgHta0f7g=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
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/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mmcdole/gofeed v1.2.1 h1:tPbFN+mfOLcM1kDF1x2c/N68ChbdBatkppdzf/vDe1s=
github.com/mmcdole/gofeed v1.2.1/go.mod h1:2wVInNpgmC85q16QTTuwbuKxtKkHLCDDtf0dCmnrNr4=
github.com/mmcdole/goxpp v1.1.0 h1:WwslZNF7KNAXTFuzRtn/OKZxFLJAAyOA9w82mDz2ZGI=
github.com/mmcdole/goxpp v1.1.0/go.mod h1:v+25+lT2ViuQ7mVxcncQ8ch1URund48oH+jhjiwEgS8=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw=
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rkoesters/xdg v0.0.1 h1:RmfYxghVvIsb4d51u5LtNOcwqY5r3P44u6o86qqvBMA=
github.com/rkoesters/xdg v0.0.1/go.mod h1:5DcbjvJkY00fIOKkaBnylbC/rmc1NNJP5dmUcnlcm7U=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/schollz/progressbar/v3 v3.13.1 h1:o8rySDYiQ59Mwzy2FELeHY5ZARXZTVJC7iHD6PEFUiE=
github.com/schollz/progressbar/v3 v3.13.1/go.mod h1:xvrbki8kfT1fzWzBT/UZd9L6GA+jdL7HAgq2RFnO6fQ=
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
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.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=
github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
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/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
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/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
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/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
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-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/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
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/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
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/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
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/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
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-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-20190524140312-2c0ae7006135/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-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
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/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
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/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
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=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/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.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
amfora-1.10.0/contrib/ 0000755 0001750 0001750 00000000000 14575704331 014035 5 ustar nilesh nilesh amfora-1.10.0/contrib/themes/ 0000755 0001750 0001750 00000000000 14575704331 015322 5 ustar nilesh nilesh amfora-1.10.0/contrib/themes/dracula-variant.toml 0000644 0001750 0001750 00000002137 14575704331 021277 0 ustar nilesh nilesh #[theme]
bg = "#282a36"
tab_num = "#bd93f9"
tab_divider = "#f8f8f2"
bottombar_label = "#bd93f9"
bottombar_text = "#8be9fd"
bottombar_bg = "#44475a"
scrollbar = "#44475a"
hdg_1 = "#bd93f9"
hdg_2 = "#bd93f9"
hdg_3 = "#bd93f9"
amfora_link = "#ff79c6"
foreign_link = "#ffb86c"
link_number = "#8be9fd"
regular_text = "#f8f8f2"
quote_text = "#f1fa8c"
preformatted_text = "#ffb86c"
list_text = "#f8f8f2"
btn_bg = "#44475a"
btn_text = "#f8f8f2"
dl_choice_modal_bg = "#6272a4"
dl_choice_modal_text = "#f8f8f2"
dl_modal_bg = "#6272a4"
dl_modal_text = "#f8f8f2"
info_modal_bg = "#6272a4"
info_modal_text = "#f8f8f2"
error_modal_bg = "#ff5555"
error_modal_text = "#f8f8f2"
yesno_modal_bg = "#6272a4"
yesno_modal_text = "#f8f8f2"
tofu_modal_bg = "#6272a4"
tofu_modal_text = "#f8f8f2"
subscription_modal_bg = "#6272a4"
subscription_modal_text = "#f8f8f2"
input_modal_bg = "#6272a4"
input_modal_text = "#f8f8f2"
input_modal_field_bg = "#44475a"
input_modal_field_text = "#f8f8f2"
bkmk_modal_bg = "#6272a4"
bkmk_modal_text = "#f8f8f2"
bkmk_modal_label = "#f8f8f2"
bkmk_modal_field_bg = "#44475a"
bkmk_modal_field_text = "#f8f8f2"
amfora-1.10.0/contrib/themes/atelier-forest-light.toml 0000644 0001750 0001750 00000003211 14575704331 022246 0 ustar nilesh nilesh #[theme]
# atelier forest light
bg = "#f1efee"
fg = "#68615e"
tab_num = "#68615e"
tab_divider = "#e6e2e0"
bottombar_label = "#3d97b8"
bottombar_text = "#68615e"
bottombar_bg = "#f1efee"
scrollbar = "#68615e"
hdg_1 = "#f22c40"
hdg_2 = "#7b9726"
hdg_3 = "#c33ff3"
amfora_link = "#407ee7"
foreign_link = "#f22c40"
link_number = "#68615e"
regular_text = "#68615e"
quote_text = "#68615e"
preformatted_text = "#68615e"
list_text = "#68615e"
btn_bg = "#407ee7"
btn_text = "#f1efee"
dl_choice_modal_bg = "#e6e2e0"
dl_choice_modal_text = "#68615e"
dl_modal_bg = "#e6e2e0"
dl_modal_text = "#68615e"
info_modal_bg = "#e6e2e0"
info_modal_text = "#68615e"
error_modal_bg = "#e6e2e0"
error_modal_text = "#f22c40"
yesno_modal_bg = "#e6e2e0"
yesno_modal_text = "#68615e"
tofu_modal_bg = "#e6e2e0"
tofu_modal_text = "#68615e"
subscription_modal_bg = "#e6e2e0"
subscription_modal_text = "#68615e"
input_modal_bg = "#e6e2e0"
input_modal_text = "#68615e"
input_modal_field_bg = "#f1efee"
input_modal_field_text = "#68615e"
bkmk_modal_bg = "#e6e2e0"
bkmk_modal_text = "#68615e"
bkmk_modal_label = "#3d97b8"
bkmk_modal_field_bg = "#f1efee"
bkmk_modal_field_text = "#68615e"
amfora-1.10.0/contrib/themes/one_dark.toml 0000644 0001750 0001750 00000006177 14575704331 020014 0 ustar nilesh nilesh # Atom One Dark theme ported to Amfora
# by Serge Tymoshenko
#[theme]
# This section is for changing the COLORS used in Amfora.
# These colors only apply if 'color' is enabled above.
# Colors can be set using a W3C color name, or a hex value such as "#ffffff".
# Note that not all colors will work on terminals that do not have truecolor support.
# If you want to stick to the standard 16 or 256 colors, you can get
# a list of those here: https://jonasjacek.github.io/colors/
# DO NOT use the names from that site, just the hex codes.
# Definitions:
# bg = background
# fg = foreground
# dl = download
# btn = button
# hdg = heading
# bkmk = bookmark
# modal = a popup window/box in the middle of the screen
# EXAMPLES:
# hdg_1 = "green"
# hdg_2 = "#5f0000"
# Available keys to set:
# bg: background for pages, tab row, app in general
# tab_num: The number/highlight of the tabs at the top
# tab_divider: The color of the divider character between tab numbers: |
# bottombar_label: The color of the prompt that appears when you press space
# bottombar_text: The color of the text you type
# bottombar_bg
bg = "#282c34"
fg = "#abb2bf"
tab_num = "#abb2bf"
tab_divider = "#abb2bf"
bottombar_bg = "#abb2bf"
bottombar_text = "#282c34"
bottombar_label = "#282c34"
# hdg_1
# hdg_2
# hdg_3
# amfora_link: A link that Amfora supports viewing. For now this is only gemini://
# foreign_link: HTTP(S), Gopher, etc
# link_number: The silver number that appears to the left of a link
# regular_text: Normal gemini text, and plaintext documents
# quote_text
# preformatted_text
# list_text
hdg_1 = "#e06c75"
hdg_2 = "#c678dd"
hdg_3 = "#c678dd"
amfora_link = "#61afef"
foreign_link = "#56b6c2"
link_number = "#abb2bf"
regular_text = "#abb2bf"
quote_text = "#98c379"
preformatted_text = "#e5c07b"
list_text = "#abb2bf"
# btn_bg: The bg color for all modal buttons
# btn_text: The text color for all modal buttons
btn_bg = "#282c34"
btn_text = "#abb2bf"
# dl_choice_modal_bg
# dl_choice_modal_text
# dl_modal_bg
# dl_modal_text
# info_modal_bg
# info_modal_text
# error_modal_bg
# error_modal_text
# yesno_modal_bg
# yesno_modal_text
# tofu_modal_bg
# tofu_modal_text
dl_choice_modal_bg = "#98c379"
dl_choice_modal_text = "#282c34"
dl_modal_bg = "#98c379"
dl_modal_text = "#282c34"
info_modal_bg = "#98c379"
info_modal_text = "#282c34"
error_modal_bg = "#e06c75"
error_modal_text = "#282c34"
yesno_modal_bg = "#e5c07b"
yesno_modal_text = "#282c34"
tofu_modal_bg = "#e5c07b"
tofu_modal_text = "#282c34"
# input_modal_bg
# input_modal_text
# input_modal_field_bg: The bg of the input field, where you type the text
# input_modal_field_text: The color of the text you type
input_modal_bg = "#98c379"
input_modal_text = "#282c34"
input_modal_field_bg = "#282c34"
input_modal_field_text = "#abb2bf"
# bkmk_modal_bg
# bkmk_modal_text
# bkmk_modal_label
# bkmk_modal_field_bg
# bkmk_modal_field_text
bkmk_modal_bg = "#98c379"
bkmk_modal_text = "#282c34"
bkmk_modal_label = "#282c34"
bkmk_modal_field_bg = "#282c34"
bkmk_modal_field_text = "#abb2bf"
# subscription_modal_bg
# subscription_modal_text
subscription_modal_bg = "#c678dd"
subscription_modal_text = "#282c34" amfora-1.10.0/contrib/themes/atelier-forest.toml 0000644 0001750 0001750 00000003203 14575704331 021142 0 ustar nilesh nilesh #[theme]
# atelier forest
bg = "#1b1918"
fg = "#a8a19f"
tab_num = "#a8a19f"
tab_divider = "#2c2421"
bottombar_label = "#3d97b8"
bottombar_text = "#a8a19f"
bottombar_bg = "#1b1918"
scrollbar = "#a8a19f"
hdg_1 = "#f22c40"
hdg_2 = "#7b9726"
hdg_3 = "#c33ff3"
amfora_link = "#407ee7"
foreign_link = "#f22c40"
link_number = "#a8a19f"
regular_text = "#a8a19f"
quote_text = "#a8a19f"
preformatted_text = "#a8a19f"
list_text = "#a8a19f"
btn_bg = "#407ee7"
btn_text = "#1b1918"
dl_choice_modal_bg = "#2c2421"
dl_choice_modal_text = "#a8a19f"
dl_modal_bg = "#2c2421"
dl_modal_text = "#a8a19f"
info_modal_bg = "#2c2421"
info_modal_text = "#a8a19f"
error_modal_bg = "#2c2421"
error_modal_text = "#f22c40"
yesno_modal_bg = "#2c2421"
yesno_modal_text = "#a8a19f"
tofu_modal_bg = "#2c2421"
tofu_modal_text = "#a8a19f"
subscription_modal_bg = "#2c2421"
subscription_modal_text = "#a8a19f"
input_modal_bg = "#2c2421"
input_modal_text = "#a8a19f"
input_modal_field_bg = "#1b1918"
input_modal_field_text = "#a8a19f"
bkmk_modal_bg = "#2c2421"
bkmk_modal_text = "#a8a19f"
bkmk_modal_label = "#3d97b8"
bkmk_modal_field_bg = "#1b1918"
bkmk_modal_field_text = "#a8a19f"
amfora-1.10.0/contrib/themes/solarized_dark.toml 0000644 0001750 0001750 00000005572 14575704331 021225 0 ustar nilesh nilesh #[theme]
# This section is for changing the COLORS used in Amfora.
# These colors only apply if 'color' is enabled above.
# Colors can be set using a W3C color name, or a hex value such as "#ffffff".
# Note that not all colors will work on terminals that do not have truecolor support.
# If you want to stick to the standard 16 or 256 colors, you can get
# a list of those here: https://jonasjacek.github.io/colors/
# DO NOT use the names from that site, just the hex codes.
# Definitions:
# bg = background
# fg = foreground
# dl = download
# btn = button
# hdg = heading
# bkmk = bookmark
# modal = a popup window/box in the middle of the screen
# EXAMPLES:
# hdg_1 = "green"
# hdg_2 = "#5f0000"
# Available keys to set:
# bg: background for pages, tab row, app in general
# tab_num: The number/highlight of the tabs at the top
# tab_divider: The color of the divider character between tab numbers: |
# bottombar_label: The color of the prompt that appears when you press space
# bottombar_text: The color of the text you type
# bottombar_bg
bg = "#002b36"
fg = "#EDE8D5"
tab_num = "#3889D2"
tab_divider = "#0F3642"
bottombar_bg = "#0F3642"
bottombar_text = "#93a1a1"
bottombar_label = "#3ea197"
# hdg_1
# hdg_2
# hdg_3
# amfora_link: A link that Amfora supports viewing. For now this is only gemini://
# foreign_link: HTTP(S), Gopher, etc
# link_number: The silver number that appears to the left of a link
# regular_text: Normal gemini text, and plaintext documents
# quote_text
# preformatted_text
# list_text
hdg_1 = "#3EA197"
hdg_2 = "#3889D2"
hdg_3 = "#6D6EC4"
amfora_link = "#94A1A1"
foreign_link = "#849496"
link_number = "#869B00"
regular_text = "#EDE8D5"
quote_text = "#EDE8D5"
preformatted_text = "#EDE8D5"
list_text = "#EDE8D5"
# btn_bg: The bg color for all modal buttons
# btn_text: The text color for all modal buttons
btn_bg = "#3889D2"
btn_text = "#FCF6E3"
dl_choice_modal_bg = "#073642"
dl_choice_modal_text = "#93a1a1"
dl_modal_bg = "#073642"
dl_modal_text = "#94a1a1"
info_modal_bg = "#073642"
info_modal_text = "#94a1a1"
error_modal_bg = "#073642"
error_modal_text = "#D53234"
yesno_modal_bg = "#073642"
yesno_modal_text = "#94a1a1"
tofu_modal_bg = "#073642"
tofu_modal_text = "#94a1a1"
# input_modal_bg
# input_modal_text
# input_modal_field_bg: The bg of the input field, where you type the text
# input_modal_field_text: The color of the text you type
input_modal_bg = "#073642"
input_modal_text = "#94a1a1"
input_modal_field_bg = "#062B36"
input_modal_field_text ="#94a1a1"
# bkmk_modal_bg
# bkmk_modal_text
# bkmk_modal_label
# bkmk_modal_field_bg
# bkmk_modal_field_text
bkmk_modal_bg = "#073642"
bkmk_modal_text = "#94a1a1"
bkmk_modal_label = "#3ea197"
bkmk_modal_field_bg = "#062B36"
bkmk_modal_field_text = "#94a1a1" amfora-1.10.0/contrib/themes/slimey.toml 0000644 0001750 0001750 00000004400 14575704331 017517 0 ustar nilesh nilesh #[theme]
# This section is for changing the COLORS used in Amfora.
# These colors only apply if 'color' is enabled above.
# Colors can be set using a W3C color name, or a hex value such as "#ffffff".
# Note that not all colors will work on terminals that do not have truecolor support.
# If you want to stick to the standard 16 or 256 colors, you can get
# a list of those here: https://jonasjacek.github.io/colors/
# DO NOT use the names from that site, just the hex codes.
# Definitions:
# bg = background
# fg = foreground
# dl = download
# btn = button
# hdg = heading
# bkmk = bookmark
# modal = a popup window/box in the middle of the screen
# EXAMPLES:
# hdg_1 = "green"
# hdg_2 = "#5f0000"
# Available keys to set:
# bg: background for pages, tab row, app in general
bg = "#c7fcd6"
# tab_num: The number/highlight of the tabs at the top
tab_num = "#f49ab9"
# tab_divider: The color of the divider character between tab numbers: |
tab_divider = "#117bf4"
# bottombar_label: The color of the prompt that appears when you press space
bottomrbar_label = "#e9f411"
# bottombar_text: The color of the text you type
bottombar_text = "#040405"
# bottombar_bg
bottombar_bg = "#1fbde0"
hdg_1 = "#a369ef"
hdg_2 = "#ef69ba"
hdg_3 = "#f4295f"
# amfora_link: A link that Amfora supports viewing. For now this is only gemini://
amfora_link = "#A020F0"
# foreign_link: HTTP(S), Gopher, etc
foreign_link = "#808080"
# link_number: The silver number that appears to the left of a link
link_number = "#2662e2"
# regular_text: Normal gemini text, and plaintext documents
regular_text = "#04000c"
# quote_text
quote_text = "#666699"
# preformatted_text
preformatted_text = "#ff1493"
# list_text
list_text = "#04000c"
# btn_bg: The bg color for all modal buttons
# btn_text: The text color for all modal buttons
# dl_choice_modal_bg
# dl_choice_modal_text
# dl_modal_bg
# dl_modal_text
# info_modal_bg
# info_modal_text
# error_modal_bg
# error_modal_text
# yesno_modal_bg
# yesno_modal_text
# tofu_modal_bg
# tofu_modal_text
# input_modal_bg
# input_modal_text
# input_modal_field_bg: The bg of the input field, where you type the text
# input_modal_field_text: The color of the text you type
# bkmk_modal_bg
# bkmk_modal_text
# bkmk_modal_label
# bkmk_modal_field_bg
# bkmk_modal_field_text
amfora-1.10.0/contrib/themes/dracula.toml 0000644 0001750 0001750 00000005504 14575704331 017636 0 ustar nilesh nilesh #[theme]
# This section is for changing the COLORS used in Amfora.
# These colors only apply if 'color' is enabled above.
# Colors can be set using a W3C color name, or a hex value such as "#ffffff".
# Note that not all colors will work on terminals that do not have truecolor support.
# If you want to stick to the standard 16 or 256 colors, you can get
# a list of those here: https://jonasjacek.github.io/colors/
# DO NOT use the names from that site, just the hex codes.
# Definitions:
# bg = background
# fg = foreground
# dl = download
# btn = button
# hdg = heading
# bkmk = bookmark
# modal = a popup window/box in the middle of the screen
# EXAMPLES:
# hdg_1 = "green"
# hdg_2 = "#5f0000"
# Available keys to set:
# bg: background for pages, tab row, app in general
# tab_num: The number/highlight of the tabs at the top
# tab_divider: The color of the divider character between tab numbers: |
# bottombar_label: The color of the prompt that appears when you press space
# bottombar_text: The color of the text you type
# bottombar_bg
bg = "#282a36"
fg = "#f8f8f2"
tab_num = "#50fa7b"
tab_divider = "#f8f8f2"
bottombar_bg = "#282a36"
bottombar_text = "#f8f8f2"
bottombar_label = "#9aedfe"
# hdg_1
# hdg_2
# hdg_3
# amfora_link: A link that Amfora supports viewing. For now this is only gemini://
# foreign_link: HTTP(S), Gopher, etc
# link_number: The silver number that appears to the left of a link
# regular_text: Normal gemini text, and plaintext documents
# quote_text
# preformatted_text
# list_text
hdg_1 = "#5af78e"
hdg_2 = "#9aedfe"
hdg_3 = "#caa9fa"
amfora_link = "#f4f99d"
foreign_link = "#d4d989"
link_number = "#ff5555"
regular_text = "#f8f8f2"
quote_text = "#E6E6E6"
preformatted_text = "#f8f8f2"
list_text = "#f8f8f2"
# btn_bg: The bg color for all modal buttons
# btn_text: The text color for all modal buttons
btn_bg = "#bfbfbf"
btn_text = "#4d4d4d"
dl_choice_modal_bg = "#282a36"
dl_choice_modal_text = "#f8f8f2"
dl_modal_bg = "#282a36"
dl_modal_text = "#f8f8f2"
info_modal_bg = "#282a36"
info_modal_text = "#f8f8f2"
error_modal_bg = "#282a36"
error_modal_text = "#ff5555"
yesno_modal_bg = "#282a36"
yesno_modal_text = "#f1fa8c"
tofu_modal_bg = "#282a36"
tofu_modal_text = "#f8f8f2"
# input_modal_bg
# input_modal_text
# input_modal_field_bg: The bg of the input field, where you type the text
# input_modal_field_text: The color of the text you type
input_modal_bg = "#282a36"
input_modal_text = "#f8f8f2"
input_modal_field_bg = "#4d4d4d"
input_modal_field_text ="#f8f8f2"
# bkmk_modal_bg
# bkmk_modal_text
# bkmk_modal_label
# bkmk_modal_field_bg
# bkmk_modal_field_text
bkmk_modal_bg = "#282a36"
bkmk_modal_text = "#f8f8f2"
bkmk_modal_label = "#f8f8f2"
bkmk_modal_field_bg = "#000000"
bkmk_modal_field_text = "#f8f8f2"
subscription_modal_bg = "#282a36"
subscription_modal_text = "#f8f8f2"
amfora-1.10.0/contrib/themes/rose-pine-moon.toml 0000644 0001750 0001750 00000002431 14575704331 021066 0 ustar nilesh nilesh ## name: Rosé Pine
## upstream: https://github.com/rose-pine/amfora/blob/main/themes/rose-pine-moon.toml
## description: All natural pine, faux fur and a bit of soho vibes for the classy minimalist
bg = "#232136"
tab_num = "#c4a7e7"
tab_divider = "#44415a"
bottombar_label = "#c4a7e7"
bottombar_text = "#e0def4"
bottombar_bg = "#2a273f"
scrollbar = "#2a283e"
hdg_1 = "#c4a7e7"
hdg_2 = "#9ccfd8"
hdg_3 = "#ea9a97"
amfora_link = "#f6c177"
foreign_link = "#908caa"
link_number = "#6e6a86"
regular_text = "#e0def4"
quote_text = "#e0def4"
preformatted_text = "#e0def4"
list_text = "#e0def4"
btn_bg = "#3e8fb0"
btn_text = "#e0def4"
dl_choice_modal_bg = "#2a273f"
dl_choice_modal_text = "#e0def4"
dl_modal_bg = "#2a273f"
dl_modal_text = "#e0def4"
info_modal_bg = "#2a273f"
info_modal_text = "#e0def4"
error_modal_bg = "#2a273f"
error_modal_text = "#eb6f92"
yesno_modal_bg = "#2a273f"
yesno_modal_text = "#e0def4"
tofu_modal_bg = "#2a273f"
tofu_modal_text = "#e0def4"
subscription_modal_bg = "#2a273f"
subscription_modal_text = "#e0def4"
input_modal_bg = "#2a273f"
input_modal_text = "#e0def4"
input_modal_field_bg = "#393552"
input_modal_field_text = "#e0def4"
bkmk_modal_bg = "#2a273f"
bkmk_modal_text = "#e0def4"
bkmk_modal_label = "#c4a7e7"
bkmk_modal_field_bg = "#393552"
bkmk_modal_field_text = "#e0def4"
amfora-1.10.0/contrib/themes/rose-pine.toml 0000644 0001750 0001750 00000002424 14575704331 020122 0 ustar nilesh nilesh ## name: Rosé Pine
## upstream: https://github.com/rose-pine/amfora/blob/main/themes/rose-pine.toml
## description: All natural pine, faux fur and a bit of soho vibes for the classy minimalist
bg = "#191724"
tab_num = "#c4a7e7"
tab_divider = "#403d52"
bottombar_label = "#c4a7e7"
bottombar_text = "#e0def4"
bottombar_bg = "#1f1d2e"
scrollbar = "#21202e"
hdg_1 = "#c4a7e7"
hdg_2 = "#9ccfd8"
hdg_3 = "#ebbcba"
amfora_link = "#f6c177"
foreign_link = "#908caa"
link_number = "#6e6a86"
regular_text = "#e0def4"
quote_text = "#e0def4"
preformatted_text = "#e0def4"
list_text = "#e0def4"
btn_bg = "#31748f"
btn_text = "#e0def4"
dl_choice_modal_bg = "#1f1d2e"
dl_choice_modal_text = "#e0def4"
dl_modal_bg = "#1f1d2e"
dl_modal_text = "#e0def4"
info_modal_bg = "#1f1d2e"
info_modal_text = "#e0def4"
error_modal_bg = "#1f1d2e"
error_modal_text = "#eb6f92"
yesno_modal_bg = "#1f1d2e"
yesno_modal_text = "#e0def4"
tofu_modal_bg = "#1f1d2e"
tofu_modal_text = "#e0def4"
subscription_modal_bg = "#1f1d2e"
subscription_modal_text = "#e0def4"
input_modal_bg = "#1f1d2e"
input_modal_text = "#e0def4"
input_modal_field_bg = "#26233a"
input_modal_field_text = "#e0def4"
bkmk_modal_bg = "#1f1d2e"
bkmk_modal_text = "#e0def4"
bkmk_modal_label = "#c4a7e7"
bkmk_modal_field_bg = "#26233a"
bkmk_modal_field_text = "#e0def4"
amfora-1.10.0/contrib/themes/nord.toml 0000644 0001750 0001750 00000006025 14575704331 017164 0 ustar nilesh nilesh #[theme]
# This section is for changing the COLORS used in Amfora.
# These colors only apply if 'color' is enabled above.
# Colors can be set using a W3C color name, or a hex value such as "#ffffff".
# Note that not all colors will work on terminals that do not have truecolor support.
# If you want to stick to the standard 16 or 256 colors, you can get
# a list of those here: https://jonasjacek.github.io/colors/
# DO NOT use the names from that site, just the hex codes.
# Definitions:
# bg = background
# fg = foreground
# dl = download
# btn = button
# hdg = heading
# bkmk = bookmark
# modal = a popup window/box in the middle of the screen
# EXAMPLES:
# hdg_1 = "green"
# hdg_2 = "#5f0000"
# Available keys to set:
# bg: background for pages, tab row, app in general
# tab_num: The number/highlight of the tabs at the top
# tab_divider: The color of the divider character between tab numbers: |
# bottombar_label: The color of the prompt that appears when you press space
# bottombar_text: The color of the text you type
# bottombar_bg
bg = "#2e3440"
tab_num = "#88c0d0"
tab_divider = "#4c566a"
bottombar_label = "#88c0d0"
bottombar_text = "#eceff4"
bottombar_bg = "#3b4252"
# hdg_1
# hdg_2
# hdg_3
# amfora_link: A link that Amfora supports viewing. For now this is only gemini://
# foreign_link: HTTP(S), Gopher, etc
# link_number: The silver number that appears to the left of a link
# regular_text: Normal gemini text, and plaintext documents
# quote_text
# preformatted_text
# list_text
hdg_1 = "#5e81ac"
hdg_2 = "#81a1c1"
hdg_3 = "#8fbcbb"
amfora_link = "#88c0d0"
foreign_link = "#b48ead"
link_number = "#a3be8c"
regular_text = "#eceff4"
quote_text = "#81a1c1"
preformatted_text = "#8fbcbb"
list_text = "#d8dee9"
# btn_bg: The bg color for all modal buttons
# btn_text: The text color for all modal buttons
btn_bg = "#4c566a"
btn_text = "#eceff4"
# dl_choice_modal_bg
# dl_choice_modal_text
# dl_modal_bg
# dl_modal_text
# info_modal_bg
# info_modal_text
# error_modal_bg
# error_modal_text
# yesno_modal_bg
# yesno_modal_text
# tofu_modal_bg
# tofu_modal_text
# subscription_modal_bg
# subscription_modal_text
dl_choice_modal_bg = "#3b4252"
dl_choice_modal_text = "#eceff4"
dl_modal_bg = "#3b4252"
dl_modal_text = "#eceff4"
info_modal_bg = "#3b4252"
info_modal_text = "#eceff4"
error_modal_bg = "#bf616a"
error_modal_text = "#eceff4"
yesno_modal_bg = "#3b4252"
yesno_modal_text = "#eceff4"
tofu_modal_bg = "#3b4252"
tofu_modal_text = "#eceff4"
subscription_modal_bg = "#3b4252"
subscription_modal_text = "#eceff4"
# input_modal_bg
# input_modal_text
# input_modal_field_bg: The bg of the input field, where you type the text
# input_modal_field_text: The color of the text you type
input_modal_bg = "#3b4252"
input_modal_text = "#eceff4"
input_modal_field_bg = "#4c566a"
input_modal_field_text = "#eceff4"
# bkmk_modal_bg
# bkmk_modal_text
# bkmk_modal_label
# bkmk_modal_field_bg
# bkmk_modal_field_text
bkmk_modal_bg = "#3b4252"
bkmk_modal_text = "#eceff4"
bkmk_modal_label = "#eceff4"
bkmk_modal_field_bg = "#4c566a"
bkmk_modal_field_text = "#eceff4"
amfora-1.10.0/contrib/themes/rose-pine-dawn.toml 0000644 0001750 0001750 00000002431 14575704331 021047 0 ustar nilesh nilesh ## name: Rosé Pine
## upstream: https://github.com/rose-pine/amfora/blob/main/themes/rose-pine-dawn.toml
## description: All natural pine, faux fur and a bit of soho vibes for the classy minimalist
bg = "#faf4ed"
tab_num = "#907aa9"
tab_divider = "#dfdad9"
bottombar_label = "#907aa9"
bottombar_text = "#575279"
bottombar_bg = "#fffaf3"
scrollbar = "#f4ede8"
hdg_1 = "#907aa9"
hdg_2 = "#56949f"
hdg_3 = "#d7827e"
amfora_link = "#ea9d34"
foreign_link = "#797593"
link_number = "#9893a5"
regular_text = "#575279"
quote_text = "#575279"
preformatted_text = "#575279"
list_text = "#575279"
btn_bg = "#286983"
btn_text = "#575279"
dl_choice_modal_bg = "#fffaf3"
dl_choice_modal_text = "#575279"
dl_modal_bg = "#fffaf3"
dl_modal_text = "#575279"
info_modal_bg = "#fffaf3"
info_modal_text = "#575279"
error_modal_bg = "#fffaf3"
error_modal_text = "#b4637a"
yesno_modal_bg = "#fffaf3"
yesno_modal_text = "#575279"
tofu_modal_bg = "#fffaf3"
tofu_modal_text = "#575279"
subscription_modal_bg = "#fffaf3"
subscription_modal_text = "#575279"
input_modal_bg = "#fffaf3"
input_modal_text = "#575279"
input_modal_field_bg = "#f2e9e1"
input_modal_field_text = "#575279"
bkmk_modal_bg = "#fffaf3"
bkmk_modal_text = "#575279"
bkmk_modal_label = "#907aa9"
bkmk_modal_field_bg = "#f2e9e1"
bkmk_modal_field_text = "#575279"
amfora-1.10.0/contrib/themes/gruvbox.toml 0000644 0001750 0001750 00000005332 14575704331 017716 0 ustar nilesh nilesh #[theme]
# This section is for changing the COLORS used in Amfora.
# These colors only apply if 'color' is enabled above.
# Colors can be set using a W3C color name, or a hex value such as "#ffffff".
# Note that not all colors will work on terminals that do not have truecolor support.
# If you want to stick to the standard 16 or 256 colors, you can get
# a list of those here: https://jonasjacek.github.io/colors/
# DO NOT use the names from that site, just the hex codes.
# Definitions:
# bg = background
# fg = foreground
# dl = download
# btn = button
# hdg = heading
# bkmk = bookmark
# modal = a popup window/box in the middle of the screen
bg = "#1d2021"
fg = "#ebdbb2"
tab_num = "#928374"
tab_divider = "#928374"
bottombar_bg = "#1d2021"
bottombar_text = "#ebdbb2"
bottombar_label = "#ebdbb2"
# EXAMPLES:
# hdg_1 = "green"
# hdg_2 = "#5f0000"
# Available keys to set:
# bg: background for pages, tab row, app in general
# tab_num: The number/highlight of the tabs at the top
# tab_divider: The color of the divider character between tab numbers: |
# bottombar_label: The color of the prompt that appears when you press space
# bottombar_text: The color of the text you type
# bottombar_bg
# hdg_1
# hdg_2
# hdg_3
# amfora_link: A link that Amfora supports viewing. For now this is only gemini://
# foreign_link: HTTP(S), Gopher, etc
# link_number: The silver number that appears to the left of a link
# regular_text: Normal gemini text, and plaintext documents
# quote_text
# preformatted_text
# list_text
hdg_1 = "#b8bb26"
hdg_2 = "#8ec07c"
hdg_3 = "#689d6a"
amfora_link = "#ebdbb2"
foreign_link = "#bdae93"
link_number = "#83a598"
regular_text = "#ebdbb2"
quote_text = "#928374"
preformatted_text = "#ebdbb2"
list_text = "#ebdbb2"
# btn_bg: The bg color for all modal buttons
# btn_text: The text color for all modal buttons
btn_bg = "#3c3836"
btn_text = "#ebdbb2"
dl_choice_modal_bg = "#3c3836"
dl_choice_modal_text = "#ebdbb2"
dl_modal_bg = "#3c3836"
dl_modal_text = "#ebdbb2"
info_modal_bg = "#3c3836"
info_modal_text = "#ebdbb2"
error_modal_bg = "#3c3836"
error_modal_text = "#fb4934"
yesno_modal_bg = "#3c3836"
yesno_modal_text = "#ebdbb2"
tofu_modal_bg = "#3c3836"
tofu_modal_text = "#ebdbb2"
# input_modal_bg
# input_modal_text
# input_modal_field_bg: The bg of the input field, where you type the text
# input_modal_field_text: The color of the text you type
input_modal_bg = "#3c3836"
input_modal_text = "#ebdbb2"
input_modal_field_bg = "#1d2021"
input_modal_field_text = "#ebdbb2"
# bkmk_modal_bg
# bkmk_modal_text
# bkmk_modal_label
# bkmk_modal_field_bg
# bkmk_modal_field_text
bkmk_modal_bg = "#3c3836"
bkmk_modal_text = "#ebdbb2"
bkmk_modal_label = "#ebdbb2"
bkmk_modal_field_bg = "#1d2021"
bkmk_modal_field_text = "#f8f8f2"
amfora-1.10.0/contrib/themes/greyscale-light.toml 0000644 0001750 0001750 00000002111 14575704331 021275 0 ustar nilesh nilesh #[theme]
bg = "#ffffff"
tab_num = "#000000"
tab_divider = "#000000"
bottombar_label = "#ffffff"
bottombar_text = "#ffffff"
bottombar_bg = "#000000"
hdg_1 = "#000000"
hdg_2 = "#000000"
hdg_3 = "#000000"
amfora_link = "#000000"
foreign_link = "#7f7f7f"
link_number = "#000000"
regular_text = "#000000"
quote_text = "#000000"
preformatted_text = "#000000"
list_text = "#000000"
btn_bg = "#efefef"
btn_text = "#000000"
dl_choice_modal_bg = "#efefef"
dl_choice_modal_text = "#000000"
dl_modal_bg = "#efefef"
dl_modal_text = "#000000"
info_modal_bg = "#efefef"
info_modal_text = "#000000"
error_modal_bg = "#efefef"
error_modal_text = "#000000"
yesno_modal_bg = "#efefef"
yesno_modal_text = "#000000"
tofu_modal_bg = "#efefef"
tofu_modal_text = "#000000"
subscription_modal_bg = "#efefef"
subscription_modal_text = "#000000"
input_modal_bg = "#efefef"
input_modal_text = "#000000"
input_modal_field_bg = "#ffffff"
input_modal_field_text = "#000000"
bkmk_modal_bg = "#efefef"
bkmk_modal_text = "#000000"
bkmk_modal_label = "#000000"
bkmk_modal_field_bg = "#ffffff"
bkmk_modal_field_text = "#000000"
amfora-1.10.0/contrib/themes/amfora.toml 0000644 0001750 0001750 00000002201 14575704331 017457 0 ustar nilesh nilesh #[theme]
# Only the 256 xterm colors are used, so truecolor support is not needed
bg = "black"
tab_num = "#008787"
tab_divider = "white"
bottombar_label = "#008787"
bottombar_text = "black"
bottombar_bg = "white"
scrollbar = "white"
btn_bg = "#000080"
btn_text = "white"
dl_choice_modal_bg = "#800080"
dl_choice_modal_text = "white"
dl_modal_bg = "#af5f00"
dl_modal_text = "white"
info_modal_bg = "#808080"
info_modal_text = "white"
error_modal_bg = "#800000"
error_modal_text = "white"
yesno_modal_bg = "#800080"
yesno_modal_text = "white"
tofu_modal_bg = "#800000"
tofu_modal_text = "white"
subscription_modal_bg = "#5f5faf"
subscription_modal_text = "white"
input_modal_bg = "#008000"
input_modal_text = "white"
input_modal_field_bg = "#0000ff"
input_modal_field_text = "white"
bkmk_modal_bg = "#008080"
bkmk_modal_text = "white"
bkmk_modal_label = "#ffff00"
bkmk_modal_field_bg = "#0000ff"
bkmk_modal_field_text = "white"
hdg_1 = "#ff0000"
hdg_2 = "#00ff00"
hdg_3 = "#ff00ff"
amfora_link = "#0087ff"
foreign_link = "#8700d7"
link_number = "#c0c0c0"
regular_text = "white"
quote_text = "white"
preformatted_text = "#ffffaf"
list_text = "white"
amfora-1.10.0/contrib/themes/tokyo-night.toml 0000644 0001750 0001750 00000002176 14575704331 020501 0 ustar nilesh nilesh #[theme]
# Tokyo Night
bg = "#1a1b26"
fg = "#a9b1d6"
tab_num = "#565f89"
tab_divider = "#3b4261"
bottombar_label = "#7aa2f7"
bottombar_text = "#7aa2f7"
bottombar_bg = "#1f2335"
scrollbar = "#565f89"
hdg_1 = "#f7768e"
hdg_2 = "#7dcfff"
hdg_3 = "#bb9af7"
amfora_link = "#73daca"
foreign_link = "#b4f9f8"
link_number = "#ff9e64"
regular_text = "#a9b1d6"
quote_text = "#e0af68"
preformatted_text = "#2ac3de"
list_text = "#a9b1d6"
btn_bg = "#414868"
btn_text = "#7aa2f7"
dl_choice_modal_bg = "#414868"
dl_choice_modal_text = "#c0caf5"
dl_modal_bg = "#414868"
dl_modal_text = "#c0caf5"
info_modal_bg = "#414868"
info_modal_text = "#c0caf5"
error_modal_bg = "#414868"
error_modal_text = "#f7768e"
yesno_modal_bg = "#414868"
yesno_modal_text = "#e0af68"
tofu_modal_bg = "#414868"
tofu_modal_text = "#2ac3de"
subscription_modal_bg = "#414868"
subscription_modal_text = "#bb9af7"
input_modal_bg = "#414868"
input_modal_text = "#c0caf5"
input_modal_field_bg = "#33467c"
input_modal_field_text = "#a9b1d6"
bkmk_modal_bg = "#414868"
bkmk_modal_text = "#c0caf5"
bkmk_modal_label = "#c0caf5"
bkmk_modal_field_bg = "#33467c"
bkmk_modal_field_text = "#a9b1d6"
amfora-1.10.0/contrib/themes/iceberg.toml 0000644 0001750 0001750 00000006025 14575704331 017622 0 ustar nilesh nilesh #[theme]
# This section is for changing the COLORS used in Amfora.
# These colors only apply if 'color' is enabled above.
# Colors can be set using a W3C color name, or a hex value such as "#ffffff".
# Note that not all colors will work on terminals that do not have truecolor support.
# If you want to stick to the standard 16 or 256 colors, you can get
# a list of those here: https://jonasjacek.github.io/colors/
# DO NOT use the names from that site, just the hex codes.
# Definitions:
# bg = background
# fg = foreground
# dl = download
# btn = button
# hdg = heading
# bkmk = bookmark
# modal = a popup window/box in the middle of the screen
# EXAMPLES:
# hdg_1 = "green"
# hdg_2 = "#5f0000"
# Available keys to set:
# bg: background for pages, tab row, app in general
# tab_num: The number/highlight of the tabs at the top
# tab_divider: The color of the divider character between tab numbers: |
# bottombar_label: The color of the prompt that appears when you press space
# bottombar_text: The color of the text you type
# bottombar_bg
bg = "#161821"
tab_num = "#6b7089"
tab_divider = "#e2a478"
bottombar_label = "#6b7089"
bottombar_text = "#89b8c2"
bottombar_bg = "#161821"
# hdg_1
# hdg_2
# hdg_3
# amfora_link: A link that Amfora supports viewing. For now this is only gemini://
# foreign_link: HTTP(S), Gopher, etc
# link_number: The silver number that appears to the left of a link
# regular_text: Normal gemini text, and plaintext documents
# quote_text
# preformatted_text
# list_text
hdg_1 = "#c0ca8e"
hdg_2 = "#e98989"
hdg_3 = "#c6c8d1"
amfora_link = "#6b7089"
foreign_link = "#d2d4de"
kink_number = "#95c4ce"
regular_text = "#c6c8d1"
quote_text = "#e98989"
preformatted_text = "#c6c8d1"
list_text = "#84a0c6"
# btn_bg: The bg color for all modal buttons
# btn_text: The text color for all modal buttons
btn_bg = "#e27878"
btn_text = "#d2d4de"
# dl_choice_modal_bg
# dl_choice_modal_text
# dl_modal_bg
# dl_modal_text
# info_modal_bg
# info_modal_text
# error_modal_bg
# error_modal_text
# yesno_modal_bg
# yesno_modal_text
# tofu_modal_bg
# tofu_modal_text
# subscription_modal_bg
# subscription_modal_text
dl_choice_modal_bg = "#84a0c6"
dl_choice_modal_text = "#161821"
dl_modal_bg = "#84a0c6"
dl_modal_text = "#161821"
info_modal_bg = "#84a0c6"
info_modal_text = "#161821"
error_modal_bg = "#e98989"
error_modal_text = "#161821"
yesno_modal_bg = "#84a0c6"
yesno_modal_text = "#161821"
tofu_modal_bg = "#84a0c6"
tofu_modal_text = "#161821"
subscription_modal_bg = "#84a0c6"
subscription_modal_text = "#161821"
# input_modal_bg
# input_modal_text
# input_modal_field_bg: The bg of the input field, where you type the text
# input_modal_field_text: The color of the text you type
input_modal_bg = "#161821"
input_modal_text = "#c6c8d1"
input_modal_field_bg = "#d2d4de"
input_modal_field_text = "#6b7089"
# bkmk_modal_bg
# bkmk_modal_text
# bkmk_modal_label
# bkmk_modal_field_bg
# bkmk_modal_field_text
bkmk_modal_bg = "#161821"
bkmk_modal_text = "#c6c8d1"
bkmk_modal_label = "#c6c8d1"
bkmk_modal_field_bg = "#d2d4de"
bkmk_modal_field_text = "#6b7089"
amfora-1.10.0/contrib/themes/solarized_light.toml 0000644 0001750 0001750 00000005572 14575704331 021413 0 ustar nilesh nilesh #[theme]
# This section is for changing the COLORS used in Amfora.
# These colors only apply if 'color' is enabled above.
# Colors can be set using a W3C color name, or a hex value such as "#ffffff".
# Note that not all colors will work on terminals that do not have truecolor support.
# If you want to stick to the standard 16 or 256 colors, you can get
# a list of those here: https://jonasjacek.github.io/colors/
# DO NOT use the names from that site, just the hex codes.
# Definitions:
# bg = background
# fg = foreground
# dl = download
# btn = button
# hdg = heading
# bkmk = bookmark
# modal = a popup window/box in the middle of the screen
# EXAMPLES:
# hdg_1 = "green"
# hdg_2 = "#5f0000"
# Available keys to set:
# bg: background for pages, tab row, app in general
# tab_num: The number/highlight of the tabs at the top
# tab_divider: The color of the divider character between tab numbers: |
# bottombar_label: The color of the prompt that appears when you press space
# bottombar_text: The color of the text you type
# bottombar_bg
bg = "#FCF6E3"
fg = "#5A6E75"
tab_num = "#3889D2"
tab_divider = "#EDE8D5"
bottombar_bg = "#EDE8D5"
bottombar_text = "#5A6E75"
bottombar_label = "#3ea197"
# hdg_1
# hdg_2
# hdg_3
# amfora_link: A link that Amfora supports viewing. For now this is only gemini://
# foreign_link: HTTP(S), Gopher, etc
# link_number: The silver number that appears to the left of a link
# regular_text: Normal gemini text, and plaintext documents
# quote_text
# preformatted_text
# list_text
hdg_1 = "#3EA197"
hdg_2 = "#3889D2"
hdg_3 = "#6D6EC4"
amfora_link = "#5A6E75"
foreign_link = "#677B83"
link_number = "#CC3283"
regular_text = "#0F3642"
quote_text = "#0F3642"
preformatted_text = "#0F3642"
list_text = "#0F3642"
# btn_bg: The bg color for all modal buttons
# btn_text: The text color for all modal buttons
btn_bg = "#3889D2"
btn_text = "#FCF6E3"
dl_choice_modal_bg = "#EDE8D5"
dl_choice_modal_text = "#0F3642"
dl_modal_bg = "#EDE8D5"
dl_modal_text = "#0F3642"
info_modal_bg = "#EDE8D5"
info_modal_text = "#0F3642"
error_modal_bg = "#EDE8D5"
error_modal_text = "#D53234"
yesno_modal_bg = "#EDE8D5"
yesno_modal_text = "#0F3642"
tofu_modal_bg = "#EDE8D5"
tofu_modal_text = "#0F3642"
# input_modal_bg
# input_modal_text
# input_modal_field_bg: The bg of the input field, where you type the text
# input_modal_field_text: The color of the text you type
input_modal_bg = "#EDE8D5"
input_modal_text = "#0F3642"
input_modal_field_bg = "#FCF6E3"
input_modal_field_text ="#0F3642"
# bkmk_modal_bg
# bkmk_modal_text
# bkmk_modal_label
# bkmk_modal_field_bg
# bkmk_modal_field_text
bkmk_modal_bg = "#EDE8D5"
bkmk_modal_text = "#0F3642"
bkmk_modal_label = "#3ea197"
bkmk_modal_field_bg = "#FCF6E3"
bkmk_modal_field_text = "#0F3642" amfora-1.10.0/contrib/themes/README.md 0000644 0001750 0001750 00000016737 14575704331 016617 0 ustar nilesh nilesh # User Contributed Themes
You can use these themes by replacing the `[theme]` section of your [config](https://github.com/makeworld-the-better-one/amfora/wiki/Configuration) with their contents. Some themes won't display properly on terminals that do not have truecolor support.
## Amfora
This is the original Amfora theme we all know and love. From v1.9.0 and onwards, the user's terminal theme is used by default. Use this theme to restore the original Amfora look.
## Nord
Contributed by **[@lokesh-krishna](https://github.com/lokesh-krishna)**.

## Dracula
Contributed by **[@crdpa](https://github.com/crdpa)**.

More screenshots


## Dracula variant
Contributed by **[@marcransome](https://github.com/marcransome)**.

More screenshots




## Greyscale Light
Contributed by **[@leifmetcalf](https://github.com/leifmetcalf)**.

More screenshots

## Gruvbox
Contributed by **[@Skraylet](https://github.com/Skraylet)**.

Another screenshot

## Solarized
Contributed by **[@bnthor](https://github.com/bnthor)**.
### Dark

Another screenshot

### Light

Another screenshot

## One Dark
Contributed by **[@sergetymo](https://github.com/sergetymo)**.

More screenshots


## Ayu Light
Contributed by **[@sergetymo](https://github.com/sergetymo)**.

More screenshots


## Atelier Forest
Contributed by **[@joyalicegu](https://github.com/joyalicegu)**.
### Dark

### Light

## Slimey
Contributed by **[@lee2sman](https://github.com/lee2sman)**.

## Gruvbox Dark
Contributed by **[@thumb](https://github.com/thumbfighter)**.

More screenshots



## Iceberg Dark
Contributed by **[@knix3](https://github.com/knix3)**

More screenshots


## Tokyo Night
Contributed by **[@luetage](https://github.com/luetage)**

## Rosé Pine
Contributed by **[@mvllow](https://github.com/mvllow)**.
### Rosé Pine
### Rosé Pine Moon
### Rosé Pine Dawn
## Yours?
Contribute your own theme by opening a PR.
amfora-1.10.0/contrib/themes/ayu_light.toml 0000644 0001750 0001750 00000002241 14575704331 020203 0 ustar nilesh nilesh # Ayu Light theme ported to Amfora
# by Serge Tymoshenko
bg = "#fcfcfc"
fg = "#5c6166"
tab_num = "#5c6166"
tab_divider = "#5c6166"
bottombar_bg = "#fcfcfc"
bottombar_text = "#5c6166"
bottombar_label = "#5c6166"
hdg_1 = "#fa8d3e"
hdg_2 = "#f2ae49"
hdg_3 = "#f2ae49"
amfora_link = "#399ee6"
foreign_link = "#a37acc"
link_number = "#5c6166"
regular_text = "#5c6166"
quote_text = "#4cbf99"
preformatted_text = "#86b300"
list_text = "#5c6166"
btn_bg = "#55b4d4"
btn_text = "#fcfcfc"
dl_choice_modal_bg = "#f2ae49"
dl_choice_modal_text = "#fcfcfc"
dl_modal_bg = "#f2ae49"
dl_modal_text = "#fcfcfc"
info_modal_bg = "#f2ae49"
info_modal_text = "#fcfcfc"
error_modal_bg = "#f07171"
error_modal_text = "#fcfcfc"
yesno_modal_bg = "#f2ae49"
yesno_modal_text = "#fcfcfc"
tofu_modal_bg = "#ed9366"
tofu_modal_text = "#282c34"
input_modal_bg = "#f2ae49"
input_modal_text = "#fcfcfc"
input_modal_field_bg = "#e6ba7e"
input_modal_field_text = "#5c6166"
bkmk_modal_bg = "#f2ae49"
bkmk_modal_text = "#fcfcfc"
bkmk_modal_label = "#fcfcfc"
bkmk_modal_field_bg = "#e6ba7e"
bkmk_modal_field_text = "#5c6166"
subscription_modal_bg = "#f2ae49"
subscription_modal_text = "#5c6166"
amfora-1.10.0/contrib/themes/gruvbox_dark.toml 0000644 0001750 0001750 00000003061 14575704331 020714 0 ustar nilesh nilesh #[theme]
# Gruvbox Dark theme
bg = "#282828"
fg = "#32302f"
tab_num = "#7c6f64"
tab_divider = "#d5c4a1"
bottombar_label = "#8f3f71"
bottombar_text = "#bdae93"
bottombar_bg = "#282828"
scrollbar = "#504945"
hdg_1 = "#cc241d"
hdg_2 = "#fabd2f"
hdg_3 = "#d65d0e"
amfora_link = "#8ec073"
foreign_link = "#458588"
link_number = "#504945"
regular_text = "#f9f5d7"
quote_text = "#d3869b"
preformatted_text = "#d3869b"
list_text = "#bdae93"
btn_bg = "#3c3836"
btn_text = "#ebdbb2"
dl_choice_modal_bg = "#3c3836"
dl_choice_modal_text = "#ebdbb2"
dl_modal_bg = "#3c3836"
dl_modal_text = "#ebdbb2"
info_modal_bg = "#3c3836"
info_modal_text = "#ebdbb2"
error_modal_bg = "#3c3836"
error_modal_text = "#fe8019"
yesno_modal_bg = "#3c3836"
yesno_modal_text = "#ebdbb2"
tofu_modal_bg = "#3c3836"
tofu_modal_text = "#ebdbb2"
subscription_modal_bg = "#3c3836"
subscription_modal_text = "#ebdbb2"
input_modal_bg = "#3c3836"
input_modal_text = "#ebdbb2"
input_modal_field_bg = "#1d2021"
input_modal_field_text = "#ebdbb2"
bkmk_modal_bg = "#3c3836"
bkmk_modal_text = "#ebdbb2"
bkmk_modal_label = "#ebdbb2"
bkmk_modal_field_bg = "#1d2021"
bkmk_modal_field_text = "#f9f5d7"
amfora-1.10.0/contrib/gemini-wiki/ 0000755 0001750 0001750 00000000000 14575704331 016246 5 ustar nilesh nilesh amfora-1.10.0/contrib/gemini-wiki/requirements.txt 0000644 0001750 0001750 00000000013 14575704331 021524 0 ustar nilesh nilesh md2gemini<2 amfora-1.10.0/contrib/gemini-wiki/main.py 0000644 0001750 0001750 00000004373 14575704331 017553 0 ustar nilesh nilesh #!/usr/bin/env python3
# Formatted with black.
import shutil
import subprocess
import sys
import os
import md2gemini
TMP_WIKI_CLONE = "/tmp/amfora.wiki"
def md2gem(markdown):
return md2gemini.md2gemini(
markdown,
links="copy",
plain=False,
strip_html=True,
md_links=True,
link_func=link_func,
)
def link_func(link):
if "://" in link:
# Absolute URL
return link
# Link to other wiki page
return link + ".gmi"
def run_cmd(*args):
proc = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
if proc.returncode != 0:
print(
"Command "
+ " ".join(args)
+ "failed with exit code "
+ str(proc.returncode)
)
print("Output was:")
print()
print(proc.stdout.decode())
sys.exit(1)
# Delete leftover git repo
try:
shutil.rmtree(TMP_WIKI_CLONE)
except FileNotFoundError:
pass
os.mkdir(TMP_WIKI_CLONE)
run_cmd(
"git",
"clone",
"--depth",
"1",
"https://github.com/makeworld-the-better-one/amfora.wiki.git",
TMP_WIKI_CLONE,
)
# Save special files
with open(os.path.join(TMP_WIKI_CLONE, "_Footer.md"), "r") as f:
footer = md2gem(f.read())
# Get files
(_, _, files) = next(os.walk(TMP_WIKI_CLONE))
# Create list of pages
pages = "## Pages\n\n=>.. Home\n"
for file in files:
if file in ["_Footer.md", "_Sidebar.md", "Home.md"]:
continue
if not file.endswith(".md"):
continue
pages += "=>" + file[:-2] + "gmi " + file[:-3].replace("-", " ") + "\n"
pages += "\n\n"
for file in files:
filepath = os.path.join(TMP_WIKI_CLONE, file)
if file in ["_Footer.md", "_Sidebar.md"]:
continue
if not file.endswith(".md"):
# Could be a resource like an image file, copy it
shutil.copyfile(filepath, file)
continue
# Markdown file
with open(filepath, "r") as f:
gemtext = md2gem(f.read())
# Add title, sidebar, footer
gemtext = "# " + file[:-3].replace("-", " ") + "\n\n" + pages + gemtext
gemtext += "\n\n\n\n" + footer
if file == "Home.md":
file = "index.md"
new_name = file[:-2] + "gmi"
with open(new_name, "w") as f:
f.write(gemtext) amfora-1.10.0/contrib/gemini-wiki/README.md 0000644 0001750 0001750 00000000615 14575704331 017527 0 ustar nilesh nilesh # gemini-wiki
This folder contains a Python script that downloads the Amfora [wiki](https://github.com/makeworld-the-better-one/amfora/wiki)
and converts it to gemtext, incorporating the sidebar and footer as well.
The script expects to be run inside the folder where the Gemini version of the wiki should be.
The output of this script can be viewed at `gemini://makeworld.space/amfora-wiki/`.
amfora-1.10.0/sysopen/ 0000755 0001750 0001750 00000000000 14575704331 014075 5 ustar nilesh nilesh amfora-1.10.0/sysopen/open_browser_unix.go 0000644 0001750 0001750 00000002225 14575704331 020174 0 ustar nilesh nilesh //go:build linux || freebsd || netbsd || openbsd
// +build linux freebsd netbsd openbsd
//nolint:goerr113
package sysopen
import (
"fmt"
"os"
"os/exec"
)
// Open opens `path` in default system viewer. It tries to do so using
// xdg-open. It only works if there is a display server working.
func Open(path string) (string, error) {
var (
xorgDisplay = os.Getenv("DISPLAY")
waylandDisplay = os.Getenv("WAYLAND_DISPLAY")
xdgOpenPath, xdgOpenNotFoundErr = exec.LookPath("xdg-open")
)
switch {
case xorgDisplay == "" && waylandDisplay == "":
return "", fmt.Errorf("no display server was found. " +
"You may set a default command in the config")
case xdgOpenNotFoundErr == nil:
// Use start rather than run or output in order
// to make application run in background.
proc := exec.Command(xdgOpenPath, path)
if err := proc.Start(); err != nil {
return "", err
}
//nolint:errcheck
go proc.Wait() // Prevent zombies, see #219
return "Opened in default system viewer", nil
default:
return "", fmt.Errorf("could not determine default system viewer. " +
"Set a catch-all command in the config")
}
}
amfora-1.10.0/sysopen/open_browser_other.go 0000644 0001750 0001750 00000000607 14575704331 020334 0 ustar nilesh nilesh //go:build !linux && !darwin && !windows && !freebsd && !netbsd && !openbsd
// +build !linux,!darwin,!windows,!freebsd,!netbsd,!openbsd
package sysopen
import "fmt"
// Open opens `path` in default system viewer, but not on this OS.
func Open(path string) (string, error) {
return "", fmt.Errorf("unsupported OS for default system viewer. " +
"Set a catch-all command in the config")
}
amfora-1.10.0/sysopen/open_browser_darwin.go 0000644 0001750 0001750 00000000556 14575704331 020502 0 ustar nilesh nilesh //go:build darwin
// +build darwin
package sysopen
import "os/exec"
// Open opens `path` in default system viewer.
func Open(path string) (string, error) {
proc := exec.Command("open", path)
err := proc.Start()
if err != nil {
return "", err
}
//nolint:errcheck
go proc.Wait() // Prevent zombies, see #219
return "Opened in default system viewer", nil
}
amfora-1.10.0/sysopen/open_browser_windows.go 0000644 0001750 0001750 00000001001 14575704331 020672 0 ustar nilesh nilesh //go:build windows && (!linux || !darwin || !freebsd || !netbsd || !openbsd)
// +build windows
// +build !linux !darwin !freebsd !netbsd !openbsd
package sysopen
import "os/exec"
// Open opens `path` in default system vierwer.
func Open(path string) (string, error) {
proc := exec.Command("rundll32", "url.dll,FileProtocolHandler", path)
err := proc.Start()
if err != nil {
return "", err
}
//nolint:errcheck
go proc.Wait() // Prevent zombies, see #219
return "Opened in default system viewer", nil
}
amfora-1.10.0/LICENSE 0000644 0001750 0001750 00000104515 14575704331 013410 0 ustar nilesh nilesh GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Copyright (C)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Copyright (C)
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
.
amfora-1.10.0/amfora.go 0000644 0001750 0001750 00000005534 14575704331 014200 0 ustar nilesh nilesh package main
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/makeworld-the-better-one/amfora/bookmarks"
"github.com/makeworld-the-better-one/amfora/client"
"github.com/makeworld-the-better-one/amfora/config"
"github.com/makeworld-the-better-one/amfora/display"
"github.com/makeworld-the-better-one/amfora/logger"
"github.com/makeworld-the-better-one/amfora/subscriptions"
)
var (
version = "v1.10.0"
commit = "unknown"
builtBy = "unknown"
)
func main() {
log, err := logger.GetLogger()
if err != nil {
panic(err)
}
debugModeEnabled := os.Getenv("AMFORA_DEBUG") == "1"
if debugModeEnabled {
log.Println("Debug mode enabled")
}
if len(os.Args) > 1 {
if os.Args[1] == "--version" || os.Args[1] == "-v" {
fmt.Println("Amfora", version)
fmt.Println("Commit:", commit)
fmt.Println("Built by:", builtBy)
return
}
if os.Args[1] == "--help" || os.Args[1] == "-h" {
fmt.Println("Amfora is a fancy terminal browser for the Gemini protocol.")
fmt.Println()
fmt.Println("Usage:")
fmt.Println("amfora [URL]")
fmt.Println("amfora --version, -v")
return
}
}
err = config.Init()
if err != nil {
fmt.Fprintf(os.Stderr, "Config error: %v\n", err)
os.Exit(1)
}
err = client.Init()
if err != nil {
fmt.Fprintf(os.Stderr, "Client error: %v\n", err)
os.Exit(1)
}
err = subscriptions.Init()
if err != nil {
fmt.Fprintf(os.Stderr, "subscriptions.json error: %v\n", err)
os.Exit(1)
}
err = bookmarks.Init()
if err != nil {
fmt.Fprintf(os.Stderr, "bookmarks.xml error: %v\n", err)
os.Exit(1)
}
// Initialize lower-level cview app
if err = display.App.Init(); err != nil {
panic(err)
}
// Initialize Amfora's settings
display.Init(version, commit, builtBy)
// Load a URL, file, or render from stdin
if len(os.Args[1:]) > 0 {
url := os.Args[1]
if !strings.Contains(url, "://") || strings.HasPrefix(url, "../") || strings.HasPrefix(url, "./") {
fileName := url
if _, err := os.Stat(fileName); err == nil {
if !strings.HasPrefix(fileName, "/") {
cwd, err := os.Getwd()
if err != nil {
fmt.Fprintf(os.Stderr, "error getting working directory path: %v\n", err)
os.Exit(1)
}
fileName = filepath.Join(cwd, fileName)
}
url = "file://" + fileName
}
}
display.NewTabWithURL(url)
} else if !isStdinEmpty() {
display.NewTab()
renderFromStdin()
} else {
display.NewTab()
}
// Start
if err = display.App.Run(); err != nil {
panic(err)
}
}
func isStdinEmpty() bool {
stat, _ := os.Stdin.Stat()
return (stat.Mode() & os.ModeCharDevice) != 0
}
func renderFromStdin() {
stdinTextBuilder := new(strings.Builder)
_, err := io.Copy(stdinTextBuilder, os.Stdin)
if err != nil {
fmt.Fprintf(os.Stderr, "error reading from standard input: %v\n", err)
os.Exit(1)
}
stdinText := stdinTextBuilder.String()
display.RenderFromString(stdinText)
}
amfora-1.10.0/NOTES.md 0000644 0001750 0001750 00000001361 14575704331 013610 0 ustar nilesh nilesh # Notes
## Issues
- URL for each tab should not be stored as a string - in the current code there's lots of reparsing the URL
## Upstream Bugs
- Bookmark keys aren't deleted, just set to `""`
- Waiting on [this viper PR](https://github.com/spf13/viper/pull/519) to be merged
- [ANSI conversion is messed up](https://code.rocketnine.space/tslocum/cview/issues/48)
- [WordWrap is broken in some cases](https://code.rocketnine.space/tslocum/cview/issues/27) - close #156 if this is fixed
- [Prevent panic when reformatting](https://code.rocketnine.space/tslocum/cview/issues/50) - can't reliably reproduce or debug
- [Unicode bullet symbol mask causes issues with PasswordInput](https://code.rocketnine.space/tslocum/cview/issues/55)
## Upstream PRs
amfora-1.10.0/config/ 0000755 0001750 0001750 00000000000 14575704331 013642 5 ustar nilesh nilesh amfora-1.10.0/config/keybindings.go 0000644 0001750 0001750 00000016400 14575704331 016500 0 ustar nilesh nilesh package config
import (
"strings"
"code.rocketnine.space/tslocum/cview"
"github.com/gdamore/tcell/v2"
"github.com/spf13/viper"
)
// NOTE: CmdLink[1-90] and CmdTab[1-90] need to be in-order and consecutive
// This property is used to simplify key handling in display/display.go
type Command int
const (
CmdInvalid Command = 0
CmdLink1 = 1
CmdLink2 = 2
CmdLink3 = 3
CmdLink4 = 4
CmdLink5 = 5
CmdLink6 = 6
CmdLink7 = 7
CmdLink8 = 8
CmdLink9 = 9
CmdLink0 = 10
CmdTab1 = 11
CmdTab2 = 12
CmdTab3 = 13
CmdTab4 = 14
CmdTab5 = 15
CmdTab6 = 16
CmdTab7 = 17
CmdTab8 = 18
CmdTab9 = 19
CmdTab0 = 20
CmdBottom = iota
CmdEdit
CmdHome
CmdBookmarks
CmdAddBookmark
CmdSave
CmdReload
CmdBack
CmdForward
CmdMoveUp
CmdMoveDown
CmdMoveLeft
CmdMoveRight
CmdPgup
CmdPgdn
CmdNewTab
CmdCloseTab
CmdNextTab
CmdPrevTab
CmdQuit
CmdHelp
CmdSub
CmdAddSub
CmdCopyPageURL
CmdCopyTargetURL
CmdBeginning
CmdEnd
CmdURLHandlerOpen // See #143
)
type keyBinding struct {
key tcell.Key
mod tcell.ModMask
r rune
}
// Map of active keybindings to commands.
var bindings map[keyBinding]Command
// inversion of tcell.KeyNames, used to simplify config parsing.
// used by parseBinding() below.
var tcellKeys map[string]tcell.Key
// helper function that takes a single keyBinding object and returns
// a string in the format used by the configuration file. Support
// function for GetKeyBinding(), used to make the help panel helpful.
func keyBindingToString(kb keyBinding) (string, bool) {
var prefix string
if kb.mod&tcell.ModAlt == tcell.ModAlt {
prefix = "Alt-"
}
if kb.key == tcell.KeyRune {
if kb.r == ' ' {
return prefix + "Space", true
}
return prefix + string(kb.r), true
}
s, ok := tcell.KeyNames[kb.key]
if ok {
return prefix + s, true
}
return "", false
}
// Get all keybindings for a Command as a string.
// Used by the help panel so bindable keys display with their
// bound values rather than hardcoded defaults.
func GetKeyBinding(cmd Command) string {
var s string
for kb, c := range bindings {
if c == cmd {
t, ok := keyBindingToString(kb)
if ok {
s += t + ", "
}
}
}
if len(s) > 0 {
return s[:len(s)-2]
}
return s
}
// Parse a single keybinding string and add it to the binding map
func parseBinding(cmd Command, binding string) {
var k tcell.Key
var m tcell.ModMask
var r rune
if strings.HasPrefix(binding, "Alt-") {
m = tcell.ModAlt
binding = binding[4:]
}
if strings.HasPrefix(binding, "Shift-") {
m += tcell.ModShift
binding = binding[6:]
}
if len([]rune(binding)) == 1 {
k = tcell.KeyRune
r = []rune(binding)[0]
} else if len(binding) == 0 {
return
} else if binding == "Space" {
k = tcell.KeyRune
r = ' '
} else {
var ok bool
k, ok = tcellKeys[binding]
if !ok { // Bad keybinding! Quietly ignore...
return
}
if strings.HasPrefix(binding, "Ctrl") {
m += tcell.ModCtrl
}
}
bindings[keyBinding{k, m, r}] = cmd
}
// Generate the bindings map from the TOML configuration file.
// Called by config.Init()
func KeyInit() {
configBindings := map[Command]string{
CmdLink1: "keybindings.bind_link1",
CmdLink2: "keybindings.bind_link2",
CmdLink3: "keybindings.bind_link3",
CmdLink4: "keybindings.bind_link4",
CmdLink5: "keybindings.bind_link5",
CmdLink6: "keybindings.bind_link6",
CmdLink7: "keybindings.bind_link7",
CmdLink8: "keybindings.bind_link8",
CmdLink9: "keybindings.bind_link9",
CmdLink0: "keybindings.bind_link0",
CmdBottom: "keybindings.bind_bottom",
CmdEdit: "keybindings.bind_edit",
CmdHome: "keybindings.bind_home",
CmdBookmarks: "keybindings.bind_bookmarks",
CmdAddBookmark: "keybindings.bind_add_bookmark",
CmdSave: "keybindings.bind_save",
CmdReload: "keybindings.bind_reload",
CmdBack: "keybindings.bind_back",
CmdForward: "keybindings.bind_forward",
CmdMoveUp: "keybindings.bind_moveup",
CmdMoveDown: "keybindings.bind_movedown",
CmdMoveLeft: "keybindings.bind_moveleft",
CmdMoveRight: "keybindings.bind_moveright",
CmdPgup: "keybindings.bind_pgup",
CmdPgdn: "keybindings.bind_pgdn",
CmdNewTab: "keybindings.bind_new_tab",
CmdCloseTab: "keybindings.bind_close_tab",
CmdNextTab: "keybindings.bind_next_tab",
CmdPrevTab: "keybindings.bind_prev_tab",
CmdQuit: "keybindings.bind_quit",
CmdHelp: "keybindings.bind_help",
CmdSub: "keybindings.bind_sub",
CmdAddSub: "keybindings.bind_add_sub",
CmdCopyPageURL: "keybindings.bind_copy_page_url",
CmdCopyTargetURL: "keybindings.bind_copy_target_url",
CmdBeginning: "keybindings.bind_beginning",
CmdEnd: "keybindings.bind_end",
CmdURLHandlerOpen: "keybindings.bind_url_handler_open",
}
// This is split off to allow shift_numbers to override bind_tab[1-90]
// (This is needed for older configs so that the default bind_tab values
// aren't used)
configTabNBindings := map[Command]string{
CmdTab1: "keybindings.bind_tab1",
CmdTab2: "keybindings.bind_tab2",
CmdTab3: "keybindings.bind_tab3",
CmdTab4: "keybindings.bind_tab4",
CmdTab5: "keybindings.bind_tab5",
CmdTab6: "keybindings.bind_tab6",
CmdTab7: "keybindings.bind_tab7",
CmdTab8: "keybindings.bind_tab8",
CmdTab9: "keybindings.bind_tab9",
CmdTab0: "keybindings.bind_tab0",
}
tcellKeys = make(map[string]tcell.Key)
bindings = make(map[keyBinding]Command)
for k, kname := range tcell.KeyNames {
tcellKeys[kname] = k
}
// Set cview navigation keys to use user-set ones
cview.Keys.MoveUp2 = viper.GetStringSlice(configBindings[CmdMoveUp])
cview.Keys.MoveDown2 = viper.GetStringSlice(configBindings[CmdMoveDown])
cview.Keys.MoveLeft2 = viper.GetStringSlice(configBindings[CmdMoveLeft])
cview.Keys.MoveRight2 = viper.GetStringSlice(configBindings[CmdMoveRight])
cview.Keys.MoveFirst = viper.GetStringSlice(configBindings[CmdBeginning])
cview.Keys.MoveFirst2 = nil
cview.Keys.MoveLast = viper.GetStringSlice(configBindings[CmdEnd])
cview.Keys.MoveLast2 = nil
for c, allb := range configBindings {
for _, b := range viper.GetStringSlice(allb) {
parseBinding(c, b)
}
}
// Backwards compatibility with the old shift_numbers config line.
shiftNumbers := []rune(viper.GetString("keybindings.shift_numbers"))
if len(shiftNumbers) > 0 && len(shiftNumbers) <= 10 {
for i, r := range shiftNumbers {
bindings[keyBinding{tcell.KeyRune, 0, r}] = CmdTab1 + Command(i)
}
} else {
for c, allb := range configTabNBindings {
for _, b := range viper.GetStringSlice(allb) {
parseBinding(c, b)
}
}
}
}
// Used by the display package to turn a tcell.EventKey into a Command
func TranslateKeyEvent(e *tcell.EventKey) Command {
var ok bool
var cmd Command
k := e.Key()
if k == tcell.KeyRune {
cmd, ok = bindings[keyBinding{k, e.Modifiers(), e.Rune()}]
} else { // Sometimes tcell sets e.Rune() on non-KeyRune events.
cmd, ok = bindings[keyBinding{k, e.Modifiers(), 0}]
}
if ok {
return cmd
}
return CmdInvalid
}
amfora-1.10.0/config/default.go 0000644 0001750 0001750 00000033540 14575704331 015622 0 ustar nilesh nilesh 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.
# You can edit this file to set your own configuration for Amfora.
# When Amfora updates, defaults may change, but this file on your drive will not.
# You can always get the latest defaults on GitHub.
# https://github.com/makeworld-the-better-one/amfora/blob/master/default-config.toml
# Please also check out the Amfora Wiki for more help
# https://github.com/makeworld-the-better-one/amfora/wiki
# gemini://makeworld.space/amfora-wiki/
# All URL values may omit the scheme and/or port, as well as the beginning double slash
# Valid URL examples:
# gemini://example.com
# //example.com
# example.com
# example.com:123
[a-general]
# Press Ctrl-H to access it
home = "gemini://geminiprotocol.net"
# Follow up to 5 Gemini redirects without prompting.
# A prompt is always shown after the 5th redirect and for redirects to protocols other than Gemini.
# If set to false, a prompt will be shown before following redirects.
auto_redirect = false
# What command to run to open a HTTP(S) URL.
# Set to "default" to try to guess the browser, or set to "off" to not open HTTP(S) 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 to the URL.
#
# The best way to define a command is using a string array.
# Examples:
# http = ['firefox']
# http = ['custom-browser', '--flag', '--option=2']
# http = ['/path/with spaces/in it/firefox']
#
# Note the use of single quotes, so that backslashes will not be escaped.
# Using just a string will also work, but it is deprecated, and will degrade if
# you use paths with spaces.
http = 'default'
# Any URL that will accept a query string can be put here
search = "gemini://geminispace.info/search"
# Whether colors will be used in the terminal
color = true
# Whether ANSI color codes from the page content should be rendered
ansi = true
# Whether or not to support source code highlighting in preformatted blocks based on alt text
highlight_code = true
# Which highlighting style to use (see https://xyproto.github.io/splash/docs/)
highlight_style = "monokai"
# Whether to replace list asterisks with unicode bullets
bullets = true
# Whether to show link after link text
show_link = false
# The max number of columns to wrap a page's text to. Preformatted blocks are not wrapped.
max_width = 80
# 'downloads' is the path to a downloads folder.
# An empty value means the code will find the default downloads folder for your system.
# If the path does not exist it will be created.
# Note the use of single quotes, so that backslashes will not be escaped.
downloads = ''
# Max size for displayable content in bytes - after that size a download window pops up
page_max_size = 2097152 # 2 MiB
# Max time it takes to load a page in seconds - after that a download window pops up
page_max_time = 10
# When a scrollbar appears. "never", "auto", and "always" are the only valid values.
# "auto" means the scrollbar only appears when the page is longer than the window.
scrollbar = "auto"
# Underline non-gemini URLs
# This is done to help color blind users
underline = true
[auth]
# Authentication settings
# Note the use of single quotes for values, so that backslashes will not be escaped.
[auth.certs]
# Client certificates
# Set URL equal to path to client cert file
#
# "example.com" = 'mycert.crt' # Cert is used for all paths on this domain
# "example.com/dir/"= 'mycert.crt' # Cert is used for /dir/ and everything below only
#
# See the comment at the beginning of this file for examples of all valid types of
# URLs, ports and schemes can be used too
[auth.keys]
# Client certificate keys
# Same as [auth.certs] but the path is to the client key file.
[keybindings]
# If you have a non-US keyboard, use bind_tab1 through bind_tab0 to
# setup the shift-number bindings: Eg, for US keyboards (the default):
# bind_tab1 = "!"
# bind_tab2 = "@"
# bind_tab3 = "#"
# bind_tab4 = "$"
# bind_tab5 = "%"
# bind_tab6 = "^"
# bind_tab7 = "&"
# bind_tab8 = "*"
# bind_tab9 = "("
# bind_tab0 = ")"
# Whitespace is not allowed in any of the keybindings! Use 'Space' and 'Tab' to bind to those keys.
# Multiple keys can be bound to one command, just use a TOML array.
# To add the Alt modifier, the binding must start with Alt-, should be reasonably universal
# Ctrl- won't work on all keys, see this for a list:
# https://github.com/gdamore/tcell/blob/cb1e5d6fa606/key.go#L83
# An example of a TOML array for multiple keys being bound to one command is the default
# binding for reload:
# bind_reload = ["R","Ctrl-R"]
# One thing to note here is that "R" is capitalization sensitive, so it means shift-r.
# "Ctrl-R" means both ctrl-r and ctrl-shift-R (this is a quirk of what ctrl-r means on
# an ANSI terminal)
# The default binding for opening the bottom bar for entering a URL or link number is:
# bind_bottom = "Space"
# This is how to get the Spacebar as a keybinding, if you try to use " ", it won't work.
# And, finally, an example of a simple, unmodified character is:
# bind_edit = "e"
# This binds the "e" key to the command to edit the current URL.
# The bind_link[1-90] options are for the commands to go to the first 10 links on a page,
# typically these are bound to the number keys:
# bind_link1 = "1"
# bind_link2 = "2"
# bind_link3 = "3"
# bind_link4 = "4"
# bind_link5 = "5"
# bind_link6 = "6"
# bind_link7 = "7"
# bind_link8 = "8"
# bind_link9 = "9"
# bind_link0 = "0"
# All keybindings:
#
# bind_bottom
# bind_edit
# bind_home
# bind_bookmarks
# bind_add_bookmark
# bind_save
# bind_reload
# bind_back
# bind_forward
# bind_moveup
# bind_movedown
# bind_moveleft
# bind_moveright
# bind_pgup
# bind_pgdn
# bind_new_tab
# bind_close_tab
# bind_next_tab
# bind_prev_tab
# bind_quit
# bind_help
# bind_sub: for viewing the subscriptions page
# bind_add_sub
# bind_copy_page_url
# bind_copy_target_url
# bind_beginning: moving to beginning of page (top left)
# bind_end: same but the for the end (bottom left)
# bind_url_handler_open: Open highlighted URL with URL handler (#143)
[url-handlers]
# Allows setting the commands to run for various URL schemes.
# E.g. to open FTP URLs with FileZilla set the following key:
# ftp = ['filezilla']
# You can set any scheme to 'off' or '' to disable handling it, or
# just leave the key unset.
#
# DO NOT use this for setting the HTTP command.
# Use the http setting in the "a-general" section above.
#
# NOTE: These settings are overrided by the ones in the proxies section.
#
# The best way to define a command is using a string array.
# Examples:
# magnet = ['transmission']
# foo = ['custom-browser', '--flag', '--option=2']
# tel = ['/path/with spaces/in it/telephone']
#
# Note the use of single quotes, so that backslashes will not be escaped.
# Using just a string will also work, but it is deprecated, and will degrade if
# you use paths with spaces.
# This is a special key that defines the handler for all URL schemes for which
# no handler is defined.
# It uses the special value 'default', which will try and use the default
# application on your computer for opening this kind of URI.
other = 'default'
[url-prompts]
# Specify whether a confirmation prompt should be shown before following URL schemes.
# The special key 'other' matches all schemes that don't match any other key.
#
# Example: prompt on every non-gemini URL
# other = true
# gemini = false
#
# Example: only prompt on HTTP(S)
# other = false
# http = true
# https = true
# [[mediatype-handlers]] section
# ---------------------------------
#
# Specify what applications will open certain media types.
# By default your default application will be used to open the file when you select "Open".
# You only need to configure this section if you want to override your default application,
# or do special things like streaming.
#
# Note the use of single quotes for commands, so that backslashes will not be escaped.
#
#
# To open jpeg files with the feh command:
#
# [[mediatype-handlers]]
# cmd = ['feh']
# types = ["image/jpeg"]
#
# Each command that you specify must come under its own [[mediatype-handlers]]. You may
# specify as many [[mediatype-handlers]] as you want to setup multiple commands.
#
# If the subtype is omitted then the specified command will be used for the
# entire type:
#
# [[mediatype-handlers]]
# command = ['vlc', '--flag']
# types = ["audio", "video"]
#
# A catch-all handler can by specified with "*".
# Note that there are already catch-all handlers in place for all OSes,
# that open the file using your default application. This is only if you
# want to override that.
#
# [[mediatype-handlers]]
# cmd = ['some-command']
# types = [
# "application/pdf",
# "*",
# ]
#
# You can also choose to stream the data instead of downloading it all before
# opening it. This is especially useful for large video or audio files, as
# well as radio streams, which will never complete. You can do this like so:
#
# [[mediatype-handlers]]
# cmd = ['vlc', '-']
# types = ["audio", "video"]
# stream = true
#
# This uses vlc to stream all video and audio content.
# By default stream is set to off for all handlers
#
#
# If you want to always open a type in its viewer without the download or open
# prompt appearing, you can add no_prompt = true
#
# [[mediatype-handlers]]
# cmd = ['feh']
# types = ["image"]
# no_prompt = true
#
# Note: Multiple handlers cannot be defined for the same full media type, but
# still there needs to be an order for which handlers are used. The following
# order applies regardless of the order written in the config:
#
# 1. Full media type: "image/jpeg"
# 2. Just type: "image"
# 3. Catch-all: "*"
[cache]
# Options for page cache - which is only for text pages
# Increase the cache size to speed up browsing at the expense of memory
# Zero values mean there is no limit
max_size = 0 # Size in bytes
max_pages = 30 # The maximum number of pages the cache will store
# How long a page will stay in cache, in seconds.
timeout = 1800 # 30 mins
[proxies]
# Allows setting a Gemini proxy for different schemes.
# The settings are similar to the url-handlers section above.
# E.g. to open a gopher page by connecting to a Gemini proxy server:
# gopher = "example.com:123"
#
# Port 1965 is assumed if no port is specified.
#
# NOTE: These settings override any external handlers specified in
# the url-handlers section.
#
# Note that HTTP and HTTPS are treated as separate protocols here.
[subscriptions]
# For tracking feeds and pages
# Whether a pop-up appears when viewing a potential feed
popup = true
# How often to check for updates to subscriptions in the background, in seconds.
# Set it to 0 to disable this feature. You can still update individual feeds
# manually, or restart the browser.
#
# Note Amfora will check for updates on browser start no matter what this setting is.
update_interval = 1800 # 30 mins
# How many subscriptions can be checked at the same time when updating.
# If you have many subscriptions you may want to increase this for faster
# update times. Any value below 1 will be corrected to 1.
workers = 3
# The number of subscription updates displayed per page.
entries_per_page = 20
# Set to false to remove the explanatory text from the top of the subscription page
header = true
[theme]
# This section is for changing the COLORS used in Amfora.
# These colors only apply if 'color' is enabled above.
# Colors can be set using a W3C color name, or a hex value such as "#ffffff".
# Setting a background to "default" keeps the terminal default
# If your terminal has transparency, set any background to "default" to keep it transparent
# The key "bg" is already set to "default", but this can be used on other backgrounds,
# like for modals.
# Note that not all colors will work on terminals that do not have truecolor support.
# If you want to stick to the standard 16 or 256 colors, you can get
# a list of those here: https://jonasjacek.github.io/colors/
# DO NOT use the names from that site, just the hex codes.
# Definitions:
# bg = background
# fg = foreground
# dl = download
# btn = button
# hdg = heading
# bkmk = bookmark
# modal = a popup window/box in the middle of the screen
# EXAMPLES:
# hdg_1 = "green"
# hdg_2 = "#5f0000"
# bg = "default"
# Available keys to set:
# bg: background for pages, tab row, app in general
# tab_num: The number/highlight of the tabs at the top
# tab_divider: The color of the divider character between tab numbers: |
# bottombar_label: The color of the prompt that appears when you press space
# bottombar_text: The color of the text you type
# bottombar_bg
# scrollbar: The scrollbar that appears on the right for long pages
# You can also set an 'include' key to process another TOML file that contains theme keys.
# Example:
# include = "my/path/to/special-theme.toml"
#
# Any other theme keys will override this external file.
# You can use this special key to switch between themes easily.
# Download other themes here: https://github.com/makeworld-the-better-one/amfora/tree/master/contrib/themes
# hdg_1
# hdg_2
# hdg_3
# amfora_link: A link that Amfora supports viewing. For now this is only gemini://
# foreign_link: HTTP(S), Gopher, etc
# link_number: The silver number that appears to the left of a link
# regular_text: Normal gemini text, and plaintext documents
# quote_text
# preformatted_text
# list_text
# btn_bg: The bg color for all modal buttons
# btn_text: The text color for all modal buttons
# dl_choice_modal_bg
# dl_choice_modal_text
# dl_modal_bg
# dl_modal_text
# info_modal_bg
# info_modal_text
# error_modal_bg
# error_modal_text
# yesno_modal_bg
# yesno_modal_text
# tofu_modal_bg
# tofu_modal_text
# subscription_modal_bg
# subscription_modal_text
# input_modal_bg
# input_modal_text
# input_modal_field_bg: The bg of the input field, where you type the text
# input_modal_field_text: The color of the text you type
# bkmk_modal_bg
# bkmk_modal_text
# bkmk_modal_label
# bkmk_modal_field_bg
# bkmk_modal_field_text
`)
amfora-1.10.0/config/default.sh 0000755 0001750 0001750 00000000316 14575704331 015625 0 ustar nilesh nilesh #!/usr/bin/env sh
cat > default.go <<-EOF
package config
//go:generate ./default.sh
EOF
echo -n 'var defaultConf = []byte(`' >> default.go
cat ../default-config.toml >> default.go
echo '`)' >> default.go
amfora-1.10.0/config/config.go 0000644 0001750 0001750 00000034757 14575704331 015456 0 ustar nilesh nilesh // Package config initializes all files required for Amfora, even those used by
// other packages. It also reads in the config file and initializes a Viper and
// the theme
//nolint:golint,goerr113
package config
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"code.rocketnine.space/tslocum/cview"
"github.com/gdamore/tcell/v2"
"github.com/makeworld-the-better-one/amfora/cache"
homedir "github.com/mitchellh/go-homedir"
"github.com/muesli/termenv"
"github.com/rkoesters/xdg/basedir"
"github.com/rkoesters/xdg/userdirs"
"github.com/spf13/viper"
)
var amforaAppData string // Where amfora files are stored on Windows - cached here
var configDir string
var configPath string
var NewTabPath string
var CustomNewTab bool
var TofuStore = viper.New()
var tofuDBDir string
var tofuDBPath string
// Bookmarks
var BkmkStore = viper.New() // TOML API for old bookmarks file
var bkmkDir string
var OldBkmkPath string // Old bookmarks file that used TOML format
var BkmkPath string // New XBEL (XML) bookmarks file, see #68
var DownloadsDir string
var TempDownloadsDir string
// Subscriptions
var subscriptionDir string
var SubscriptionPath string
// Command for opening HTTP(S) URLs in the browser, from "a-general.http" in config.
var HTTPCommand []string
type MediaHandler struct {
Cmd []string
NoPrompt bool
Stream bool
}
var MediaHandlers = make(map[string]MediaHandler)
// Controlled by "a-general.scrollbar" in config
// Defaults to ScrollBarAuto on an invalid value
var ScrollBar cview.ScrollBarVisibility
// Whether the user's terminal is dark or light
// Defaults to dark, but is determined in Init()
// Used to prevent white text on a white background with the default theme
var hasDarkTerminalBackground bool
func Init() error {
// *** Set paths ***
// Windows uses paths under APPDATA, Unix systems use XDG paths
// Windows systems use XDG paths if variables are defined, see #255
home, err := homedir.Dir()
if err != nil {
return err
}
// Store AppData path
if runtime.GOOS == "windows" { //nolint:goconst
appdata, ok := os.LookupEnv("APPDATA")
if ok {
amforaAppData = filepath.Join(appdata, "amfora")
} else {
amforaAppData = filepath.Join(home, filepath.FromSlash("AppData/Roaming/amfora/"))
}
}
// Store config directory and file paths
if runtime.GOOS == "windows" && os.Getenv("XDG_CONFIG_HOME") == "" {
configDir = amforaAppData
} else {
// Unix / POSIX system, or Windows with XDG_CONFIG_HOME defined
configDir = filepath.Join(basedir.ConfigHome, "amfora")
}
configPath = filepath.Join(configDir, "config.toml")
// Search for a custom new tab
NewTabPath = filepath.Join(configDir, "newtab.gmi")
CustomNewTab = false
if _, err := os.Stat(NewTabPath); err == nil {
CustomNewTab = true
}
// Store TOFU db directory and file paths
if runtime.GOOS == "windows" && os.Getenv("XDG_CACHE_HOME") == "" {
// Windows just stores it in APPDATA along with other stuff
tofuDBDir = amforaAppData
} else {
// XDG cache dir on POSIX systems
tofuDBDir = filepath.Join(basedir.CacheHome, "amfora")
}
tofuDBPath = filepath.Join(tofuDBDir, "tofu.toml")
// Store bookmarks dir and path
if runtime.GOOS == "windows" && os.Getenv("XDG_DATA_HOME") == "" {
// Windows just keeps it in APPDATA along with other Amfora files
bkmkDir = amforaAppData
} else {
// XDG data dir on POSIX systems
bkmkDir = filepath.Join(basedir.DataHome, "amfora")
}
OldBkmkPath = filepath.Join(bkmkDir, "bookmarks.toml")
BkmkPath = filepath.Join(bkmkDir, "bookmarks.xml")
// Feeds dir and path
if runtime.GOOS == "windows" && os.Getenv("XDG_DATA_HOME") == "" {
// In APPDATA beside other Amfora files
subscriptionDir = amforaAppData
} else {
// XDG data dir on POSIX systems
subscriptionDir = filepath.Join(basedir.DataHome, "amfora")
}
SubscriptionPath = filepath.Join(subscriptionDir, "subscriptions.json")
// *** Create necessary files and folders ***
// Config
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()
}
// TOFU
err = os.MkdirAll(tofuDBDir, 0755)
if err != nil {
return err
}
f, err = os.OpenFile(tofuDBPath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
if err == nil {
f.Close()
}
// Bookmarks
err = os.MkdirAll(bkmkDir, 0755)
if err != nil {
return err
}
// OldBkmkPath isn't created because it shouldn't be there anyway
// Feeds
err = os.MkdirAll(subscriptionDir, 0755)
if err != nil {
return err
}
// *** Setup vipers ***
TofuStore.SetConfigFile(tofuDBPath)
TofuStore.SetConfigType("toml")
err = TofuStore.ReadInConfig()
if err != nil {
return err
}
BkmkStore.SetConfigFile(OldBkmkPath)
BkmkStore.SetConfigType("toml")
err = BkmkStore.ReadInConfig()
if err != nil {
// File doesn't exist, so remove the viper
BkmkStore = nil
}
// Setup main config
viper.SetDefault("a-general.home", "gemini://geminiprotocol.net")
viper.SetDefault("a-general.auto_redirect", false)
viper.SetDefault("a-general.http", "default")
viper.SetDefault("a-general.search", "gemini://geminispace.info/search")
viper.SetDefault("a-general.color", true)
viper.SetDefault("a-general.ansi", true)
viper.SetDefault("a-general.highlight_code", true)
viper.SetDefault("a-general.highlight_style", "monokai")
viper.SetDefault("a-general.bullets", true)
viper.SetDefault("a-general.show_link", false)
viper.SetDefault("a-general.max_width", 80)
viper.SetDefault("a-general.downloads", "")
viper.SetDefault("a-general.temp_downloads", "")
viper.SetDefault("a-general.page_max_size", 2097152)
viper.SetDefault("a-general.page_max_time", 10)
viper.SetDefault("a-general.scrollbar", "auto")
viper.SetDefault("a-general.underline", true)
viper.SetDefault("keybindings.bind_reload", []string{"R", "Ctrl-R"})
viper.SetDefault("keybindings.bind_home", "Backspace")
viper.SetDefault("keybindings.bind_bookmarks", "Ctrl-B")
viper.SetDefault("keybindings.bind_add_bookmark", "Ctrl-D")
viper.SetDefault("keybindings.bind_sub", "Ctrl-A")
viper.SetDefault("keybindings.bind_add_sub", "Ctrl-X")
viper.SetDefault("keybindings.bind_save", "Ctrl-S")
viper.SetDefault("keybindings.bind_moveup", "k")
viper.SetDefault("keybindings.bind_movedown", "j")
viper.SetDefault("keybindings.bind_moveleft", "h")
viper.SetDefault("keybindings.bind_moveright", "l")
viper.SetDefault("keybindings.bind_pgup", []string{"PgUp", "u"})
viper.SetDefault("keybindings.bind_pgdn", []string{"PgDn", "d"})
viper.SetDefault("keybindings.bind_bottom", "Space")
viper.SetDefault("keybindings.bind_edit", "e")
viper.SetDefault("keybindings.bind_back", []string{"b", "Alt-Left"})
viper.SetDefault("keybindings.bind_forward", []string{"f", "Alt-Right"})
viper.SetDefault("keybindings.bind_new_tab", "Ctrl-T")
viper.SetDefault("keybindings.bind_close_tab", "Ctrl-W")
viper.SetDefault("keybindings.bind_next_tab", "F2")
viper.SetDefault("keybindings.bind_prev_tab", "F1")
viper.SetDefault("keybindings.bind_quit", []string{"Ctrl-C", "Ctrl-Q", "Q"})
viper.SetDefault("keybindings.bind_help", "?")
viper.SetDefault("keybindings.bind_link1", "1")
viper.SetDefault("keybindings.bind_link2", "2")
viper.SetDefault("keybindings.bind_link3", "3")
viper.SetDefault("keybindings.bind_link4", "4")
viper.SetDefault("keybindings.bind_link5", "5")
viper.SetDefault("keybindings.bind_link6", "6")
viper.SetDefault("keybindings.bind_link7", "7")
viper.SetDefault("keybindings.bind_link8", "8")
viper.SetDefault("keybindings.bind_link9", "9")
viper.SetDefault("keybindings.bind_link0", "0")
viper.SetDefault("keybindings.bind_tab1", "!")
viper.SetDefault("keybindings.bind_tab2", "@")
viper.SetDefault("keybindings.bind_tab3", "#")
viper.SetDefault("keybindings.bind_tab4", "$")
viper.SetDefault("keybindings.bind_tab5", "%")
viper.SetDefault("keybindings.bind_tab6", "^")
viper.SetDefault("keybindings.bind_tab7", "&")
viper.SetDefault("keybindings.bind_tab8", "*")
viper.SetDefault("keybindings.bind_tab9", "(")
viper.SetDefault("keybindings.bind_tab0", ")")
viper.SetDefault("keybindings.bind_copy_page_url", "C")
viper.SetDefault("keybindings.bind_copy_target_url", "c")
viper.SetDefault("keybindings.bind_beginning", []string{"Home", "g"})
viper.SetDefault("keybindings.bind_end", []string{"End", "G"})
viper.SetDefault("keybindings.shift_numbers", "")
viper.SetDefault("keybindings.bind_url_handler_open", "Ctrl-U")
viper.SetDefault("url-handlers.other", "default")
viper.SetDefault("url-prompts.other", false)
viper.SetDefault("cache.max_size", 0)
viper.SetDefault("cache.max_pages", 20)
viper.SetDefault("cache.timeout", 1800)
viper.SetDefault("subscriptions.popup", true)
viper.SetDefault("subscriptions.update_interval", 1800)
viper.SetDefault("subscriptions.workers", 3)
viper.SetDefault("subscriptions.entries_per_page", 20)
viper.SetDefault("subscriptions.header", true)
viper.SetConfigFile(configPath)
viper.SetConfigType("toml")
err = viper.ReadInConfig()
if err != nil {
return err
}
// Setup the key bindings
KeyInit()
// *** Downloads paths, setup, and creation ***
// Setup downloads dir
if viper.GetString("a-general.downloads") == "" {
// Find default Downloads dir
if userdirs.Download == "" {
DownloadsDir = filepath.Join(home, "Downloads")
} else {
DownloadsDir = userdirs.Download
}
// Create it just in case
err = os.MkdirAll(DownloadsDir, 0755)
if err != nil {
return fmt.Errorf("downloads path could not be created: %s", DownloadsDir)
}
} else {
// Validate path
dDir := viper.GetString("a-general.downloads")
di, err := os.Stat(dDir)
if err == nil {
if !di.IsDir() {
return fmt.Errorf("downloads path specified is not a directory: %s", dDir)
}
} else if os.IsNotExist(err) {
// Try to create path
err = os.MkdirAll(dDir, 0755)
if err != nil {
return fmt.Errorf("downloads path could not be created: %s", dDir)
}
} else {
// Some other error
return fmt.Errorf("couldn't access downloads directory: %s", dDir)
}
DownloadsDir = dDir
}
// Setup temporary downloads dir
if viper.GetString("a-general.temp_downloads") == "" {
TempDownloadsDir = filepath.Join(os.TempDir(), "amfora_temp")
// Make sure it exists
err = os.MkdirAll(TempDownloadsDir, 0755)
if err != nil {
return fmt.Errorf("temp downloads path could not be created: %s", TempDownloadsDir)
}
} else {
// Validate path
dDir := viper.GetString("a-general.temp_downloads")
di, err := os.Stat(dDir)
if err == nil {
if !di.IsDir() {
return fmt.Errorf("temp downloads path specified is not a directory: %s", dDir)
}
} else if os.IsNotExist(err) {
// Try to create path
err = os.MkdirAll(dDir, 0755)
if err != nil {
return fmt.Errorf("temp downloads path could not be created: %s", dDir)
}
} else {
// Some other error
return fmt.Errorf("couldn't access temp downloads directory: %s", dDir)
}
TempDownloadsDir = dDir
}
// Setup cache from config
cache.SetMaxSize(viper.GetInt("cache.max_size"))
cache.SetMaxPages(viper.GetInt("cache.max_pages"))
cache.SetTimeout(viper.GetInt("cache.timeout"))
setColor := func(k string, colorStr string) error {
if k == "include" {
return nil
}
colorStr = strings.ToLower(colorStr)
var color tcell.Color
if colorStr == "default" {
if strings.HasSuffix(k, "bg") {
color = tcell.ColorDefault
} else {
return fmt.Errorf(`"default" is only valid for a background color (color ending in "bg"), not "%s"`, k)
}
} else {
color = tcell.GetColor(colorStr)
if color == tcell.ColorDefault {
return fmt.Errorf(`invalid color format for "%s": %s`, k, colorStr)
}
}
SetColor(k, color)
return nil
}
// Setup theme
configTheme := viper.Sub("theme")
if configTheme != nil {
// Include key comes first
if incPath := configTheme.GetString("include"); incPath != "" {
incViper := viper.New()
newIncPath, err := homedir.Expand(incPath)
if err == nil {
incViper.SetConfigFile(newIncPath)
} else {
incViper.SetConfigFile(incPath)
}
incViper.SetConfigType("toml")
err = incViper.ReadInConfig()
if err != nil {
return err
}
for k2, v2 := range incViper.AllSettings() {
colorStr, ok := v2.(string)
if !ok {
return fmt.Errorf(`include: value for "%s" is not a string: %v`, k2, v2)
}
if err := setColor(k2, colorStr); err != nil {
return err
}
}
}
for k, v := range configTheme.AllSettings() {
colorStr, ok := v.(string)
if !ok {
return fmt.Errorf(`value for "%s" is not a string: %v`, k, v)
}
if err := setColor(k, colorStr); err != nil {
return err
}
}
}
if viper.GetBool("a-general.color") {
cview.Styles.PrimitiveBackgroundColor = GetColor("bg")
} else {
// No colors allowed, set background to black instead of default
themeMu.Lock()
theme["bg"] = tcell.ColorBlack
cview.Styles.PrimitiveBackgroundColor = tcell.ColorBlack
themeMu.Unlock()
}
hasDarkTerminalBackground = termenv.HasDarkBackground()
// Parse HTTP command
HTTPCommand = viper.GetStringSlice("a-general.http")
if len(HTTPCommand) == 0 {
// Not a string array, interpret as a string instead
// Split on spaces to maintain compatibility with old versions
// The new better way to is to just define a string array in config
HTTPCommand = strings.Fields(viper.GetString("a-general.http"))
}
var rawMediaHandlers []struct {
Cmd []string `mapstructure:"cmd"`
Types []string `mapstructure:"types"`
NoPrompt bool `mapstructure:"no_prompt"`
Stream bool `mapstructure:"stream"`
}
err = viper.UnmarshalKey("mediatype-handlers", &rawMediaHandlers)
if err != nil {
return fmt.Errorf("couldn't parse mediatype-handlers section in config: %w", err)
}
for _, rawMediaHandler := range rawMediaHandlers {
if len(rawMediaHandler.Cmd) == 0 {
return fmt.Errorf("empty cmd array in mediatype-handlers section")
}
if len(rawMediaHandler.Types) == 0 {
return fmt.Errorf("empty types array in mediatype-handlers section")
}
for _, typ := range rawMediaHandler.Types {
if _, ok := MediaHandlers[typ]; ok {
return fmt.Errorf("multiple mediatype-handlers defined for %v", typ)
}
MediaHandlers[typ] = MediaHandler{
Cmd: rawMediaHandler.Cmd,
NoPrompt: rawMediaHandler.NoPrompt,
Stream: rawMediaHandler.Stream,
}
}
}
// Parse scrollbar options
switch viper.GetString("a-general.scrollbar") {
case "never":
ScrollBar = cview.ScrollBarNever
case "always":
ScrollBar = cview.ScrollBarAlways
default:
ScrollBar = cview.ScrollBarAuto
}
return nil
}
amfora-1.10.0/config/theme.go 0000644 0001750 0001750 00000031601 14575704331 015274 0 ustar nilesh nilesh package config
import (
"fmt"
"sync"
"github.com/gdamore/tcell/v2"
)
// Functions to allow themeing configuration.
// UI element tcell.Colors are mapped to a string key, such as "error" or "tab_bg"
// These are the same keys used in the config file.
// Special color with no real color value
// Used for a default foreground color
// White is the terminal background is black, black if the terminal background is white
// Converted to a real color in this file before being sent out to other modules
const ColorFg = tcell.ColorSpecial | 2
// The same as ColorFg, but inverted
const ColorBg = tcell.ColorSpecial | 3
var themeMu = sync.RWMutex{}
var theme = map[string]tcell.Color{
// Map these for special uses in code
"ColorBg": ColorBg,
"ColorFg": ColorFg,
// Default values below
// Only the 16 Xterm system tcell.Colors are used, because those are the tcell.Colors overrided
// by the user's default terminal theme
// Used for cview.Styles.PrimitiveBackgroundColor
// Set to tcell.ColorDefault because that allows transparent terminals to work
// The rest of this theme assumes that the background is equivalent to black, but
// white colors switched to black later if the background is determined to be white.
//
// Also, this is set to tcell.ColorBlack in config.go if colors are disabled in the config.
"bg": tcell.ColorDefault,
"tab_num": tcell.ColorTeal,
"tab_divider": ColorFg,
"bottombar_label": tcell.ColorTeal,
"bottombar_text": ColorBg,
"bottombar_bg": ColorFg,
"scrollbar": ColorFg,
// Modals
"btn_bg": tcell.ColorTeal, // All modal buttons
"btn_text": tcell.ColorWhite, // White instead of ColorFg because background is known to be Teal
"dl_choice_modal_bg": tcell.ColorOlive,
"dl_choice_modal_text": tcell.ColorWhite,
"dl_modal_bg": tcell.ColorOlive,
"dl_modal_text": tcell.ColorWhite,
"info_modal_bg": tcell.ColorGray,
"info_modal_text": tcell.ColorWhite,
"error_modal_bg": tcell.ColorMaroon,
"error_modal_text": tcell.ColorWhite,
"yesno_modal_bg": tcell.ColorTeal,
"yesno_modal_text": tcell.ColorWhite,
"tofu_modal_bg": tcell.ColorMaroon,
"tofu_modal_text": tcell.ColorWhite,
"subscription_modal_bg": tcell.ColorTeal,
"subscription_modal_text": tcell.ColorWhite,
"input_modal_bg": tcell.ColorGreen,
"input_modal_text": tcell.ColorWhite,
"input_modal_field_bg": tcell.ColorNavy,
"input_modal_field_text": tcell.ColorWhite,
"bkmk_modal_bg": tcell.ColorTeal,
"bkmk_modal_text": tcell.ColorWhite,
"bkmk_modal_label": tcell.ColorYellow,
"bkmk_modal_field_bg": tcell.ColorNavy,
"bkmk_modal_field_text": tcell.ColorWhite,
"hdg_1": tcell.ColorRed,
"hdg_2": tcell.ColorLime,
"hdg_3": tcell.ColorFuchsia,
"amfora_link": tcell.ColorBlue,
"foreign_link": tcell.ColorPurple,
"link_number": tcell.ColorSilver,
"regular_text": ColorFg,
"quote_text": ColorFg,
"preformatted_text": ColorFg,
"list_text": ColorFg,
}
func SetColor(key string, color tcell.Color) {
themeMu.Lock()
// Use truecolor because this is only called with user-set tcell.Colors
// Which should be represented exactly
theme[key] = color.TrueColor()
themeMu.Unlock()
}
// GetColor will return tcell.ColorBlack if there is no tcell.Color for the provided key.
func GetColor(key string) tcell.Color {
themeMu.RLock()
defer themeMu.RUnlock()
color := theme[key]
if color == ColorFg {
if hasDarkTerminalBackground {
return tcell.ColorWhite
}
return tcell.ColorBlack
}
if color == ColorBg {
if hasDarkTerminalBackground {
return tcell.ColorBlack
}
return tcell.ColorWhite
}
return color
}
// colorToString converts a color to a string for use in a cview tag
func colorToString(color tcell.Color) string {
if color == tcell.ColorDefault {
return "-"
}
if color == ColorFg {
if hasDarkTerminalBackground {
return "white"
}
return "black"
}
if color == ColorBg {
if hasDarkTerminalBackground {
return "black"
}
return "white"
}
if color&tcell.ColorIsRGB == 0 {
// tcell.Color is not RGB/TrueColor, it's a tcell.Color from the default terminal
// theme as set above
// Return a tcell.Color name instead of a hex code, so that cview doesn't use TrueColor
return ColorToColorName[color]
}
// Color set by user, must be respected exactly so hex code is used
return fmt.Sprintf("#%06x", color.Hex())
}
// GetColorString returns a string that can be used in a cview tcell.Color tag,
// for the given theme key.
// It will return "#000000" if there is no tcell.Color for the provided key.
func GetColorString(key string) string {
themeMu.RLock()
defer themeMu.RUnlock()
return colorToString(theme[key])
}
// GetContrastingColor returns tcell.ColorBlack if tcell.Color is brighter than gray
// otherwise returns tcell.ColorWhite if tcell.Color is dimmer than gray
// if tcell.Color is tcell.ColorDefault (undefined luminance) this returns tcell.ColorDefault
func GetContrastingColor(color tcell.Color) tcell.Color {
if color == tcell.ColorDefault {
// tcell.Color should never be tcell.ColorDefault
// only config keys which end in bg are allowed to be set to default
// and the only way the argument of this function is set to tcell.ColorDefault
// is if both the text and bg of an element in the UI are set to default
return tcell.ColorDefault
}
r, g, b := color.RGB()
luminance := (77*r + 150*g + 29*b + 1<<7) >> 8
const gray = 119 // The middle gray
if luminance > gray {
return tcell.ColorBlack
}
return tcell.ColorWhite
}
// GetTextColor is the Same as GetColor, unless the key is "default".
// This happens on focus of a UI element which has a bg of default, in which case
// It return tcell.ColorBlack or tcell.ColorWhite, depending on which is more readable
func GetTextColor(key, bg string) tcell.Color {
themeMu.RLock()
defer themeMu.RUnlock()
color := theme[key].TrueColor()
if color != tcell.ColorDefault {
return color
}
return GetContrastingColor(theme[bg].TrueColor())
}
// GetTextColorString is the Same as GetColorString, unless the key is "default".
// This happens on focus of a UI element which has a bg of default, in which case
// It return tcell.ColorBlack or tcell.ColorWhite, depending on which is more readable
func GetTextColorString(key, bg string) string {
return colorToString(GetTextColor(key, bg))
}
// Inverted version of a tcell map
// https://github.com/gdamore/tcell/blob/v2.3.3/color.go#L845
var ColorToColorName = map[tcell.Color]string{
tcell.ColorBlack: "black",
tcell.ColorMaroon: "maroon",
tcell.ColorGreen: "green",
tcell.ColorOlive: "olive",
tcell.ColorNavy: "navy",
tcell.ColorPurple: "purple",
tcell.ColorTeal: "teal",
tcell.ColorSilver: "silver",
tcell.ColorGray: "gray",
tcell.ColorRed: "red",
tcell.ColorLime: "lime",
tcell.ColorYellow: "yellow",
tcell.ColorBlue: "blue",
tcell.ColorFuchsia: "fuchsia",
tcell.ColorAqua: "aqua",
tcell.ColorWhite: "white",
tcell.ColorAliceBlue: "aliceblue",
tcell.ColorAntiqueWhite: "antiquewhite",
tcell.ColorAquaMarine: "aquamarine",
tcell.ColorAzure: "azure",
tcell.ColorBeige: "beige",
tcell.ColorBisque: "bisque",
tcell.ColorBlanchedAlmond: "blanchedalmond",
tcell.ColorBlueViolet: "blueviolet",
tcell.ColorBrown: "brown",
tcell.ColorBurlyWood: "burlywood",
tcell.ColorCadetBlue: "cadetblue",
tcell.ColorChartreuse: "chartreuse",
tcell.ColorChocolate: "chocolate",
tcell.ColorCoral: "coral",
tcell.ColorCornflowerBlue: "cornflowerblue",
tcell.ColorCornsilk: "cornsilk",
tcell.ColorCrimson: "crimson",
tcell.ColorDarkBlue: "darkblue",
tcell.ColorDarkCyan: "darkcyan",
tcell.ColorDarkGoldenrod: "darkgoldenrod",
tcell.ColorDarkGray: "darkgray",
tcell.ColorDarkGreen: "darkgreen",
tcell.ColorDarkKhaki: "darkkhaki",
tcell.ColorDarkMagenta: "darkmagenta",
tcell.ColorDarkOliveGreen: "darkolivegreen",
tcell.ColorDarkOrange: "darkorange",
tcell.ColorDarkOrchid: "darkorchid",
tcell.ColorDarkRed: "darkred",
tcell.ColorDarkSalmon: "darksalmon",
tcell.ColorDarkSeaGreen: "darkseagreen",
tcell.ColorDarkSlateBlue: "darkslateblue",
tcell.ColorDarkSlateGray: "darkslategray",
tcell.ColorDarkTurquoise: "darkturquoise",
tcell.ColorDarkViolet: "darkviolet",
tcell.ColorDeepPink: "deeppink",
tcell.ColorDeepSkyBlue: "deepskyblue",
tcell.ColorDimGray: "dimgray",
tcell.ColorDodgerBlue: "dodgerblue",
tcell.ColorFireBrick: "firebrick",
tcell.ColorFloralWhite: "floralwhite",
tcell.ColorForestGreen: "forestgreen",
tcell.ColorGainsboro: "gainsboro",
tcell.ColorGhostWhite: "ghostwhite",
tcell.ColorGold: "gold",
tcell.ColorGoldenrod: "goldenrod",
tcell.ColorGreenYellow: "greenyellow",
tcell.ColorHoneydew: "honeydew",
tcell.ColorHotPink: "hotpink",
tcell.ColorIndianRed: "indianred",
tcell.ColorIndigo: "indigo",
tcell.ColorIvory: "ivory",
tcell.ColorKhaki: "khaki",
tcell.ColorLavender: "lavender",
tcell.ColorLavenderBlush: "lavenderblush",
tcell.ColorLawnGreen: "lawngreen",
tcell.ColorLemonChiffon: "lemonchiffon",
tcell.ColorLightBlue: "lightblue",
tcell.ColorLightCoral: "lightcoral",
tcell.ColorLightCyan: "lightcyan",
tcell.ColorLightGoldenrodYellow: "lightgoldenrodyellow",
tcell.ColorLightGray: "lightgray",
tcell.ColorLightGreen: "lightgreen",
tcell.ColorLightPink: "lightpink",
tcell.ColorLightSalmon: "lightsalmon",
tcell.ColorLightSeaGreen: "lightseagreen",
tcell.ColorLightSkyBlue: "lightskyblue",
tcell.ColorLightSlateGray: "lightslategray",
tcell.ColorLightSteelBlue: "lightsteelblue",
tcell.ColorLightYellow: "lightyellow",
tcell.ColorLimeGreen: "limegreen",
tcell.ColorLinen: "linen",
tcell.ColorMediumAquamarine: "mediumaquamarine",
tcell.ColorMediumBlue: "mediumblue",
tcell.ColorMediumOrchid: "mediumorchid",
tcell.ColorMediumPurple: "mediumpurple",
tcell.ColorMediumSeaGreen: "mediumseagreen",
tcell.ColorMediumSlateBlue: "mediumslateblue",
tcell.ColorMediumSpringGreen: "mediumspringgreen",
tcell.ColorMediumTurquoise: "mediumturquoise",
tcell.ColorMediumVioletRed: "mediumvioletred",
tcell.ColorMidnightBlue: "midnightblue",
tcell.ColorMintCream: "mintcream",
tcell.ColorMistyRose: "mistyrose",
tcell.ColorMoccasin: "moccasin",
tcell.ColorNavajoWhite: "navajowhite",
tcell.ColorOldLace: "oldlace",
tcell.ColorOliveDrab: "olivedrab",
tcell.ColorOrange: "orange",
tcell.ColorOrangeRed: "orangered",
tcell.ColorOrchid: "orchid",
tcell.ColorPaleGoldenrod: "palegoldenrod",
tcell.ColorPaleGreen: "palegreen",
tcell.ColorPaleTurquoise: "paleturquoise",
tcell.ColorPaleVioletRed: "palevioletred",
tcell.ColorPapayaWhip: "papayawhip",
tcell.ColorPeachPuff: "peachpuff",
tcell.ColorPeru: "peru",
tcell.ColorPink: "pink",
tcell.ColorPlum: "plum",
tcell.ColorPowderBlue: "powderblue",
tcell.ColorRebeccaPurple: "rebeccapurple",
tcell.ColorRosyBrown: "rosybrown",
tcell.ColorRoyalBlue: "royalblue",
tcell.ColorSaddleBrown: "saddlebrown",
tcell.ColorSalmon: "salmon",
tcell.ColorSandyBrown: "sandybrown",
tcell.ColorSeaGreen: "seagreen",
tcell.ColorSeashell: "seashell",
tcell.ColorSienna: "sienna",
tcell.ColorSkyblue: "skyblue",
tcell.ColorSlateBlue: "slateblue",
tcell.ColorSlateGray: "slategray",
tcell.ColorSnow: "snow",
tcell.ColorSpringGreen: "springgreen",
tcell.ColorSteelBlue: "steelblue",
tcell.ColorTan: "tan",
tcell.ColorThistle: "thistle",
tcell.ColorTomato: "tomato",
tcell.ColorTurquoise: "turquoise",
tcell.ColorViolet: "violet",
tcell.ColorWheat: "wheat",
tcell.ColorWhiteSmoke: "whitesmoke",
tcell.ColorYellowGreen: "yellowgreen",
}
amfora-1.10.0/.goreleaser.yml 0000644 0001750 0001750 00000001534 14575704331 015331 0 ustar nilesh nilesh project_name: amfora
env:
- GO111MODULE=on
before:
hooks:
- go mod download
- go generate ./...
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
- freebsd
- netbsd
- openbsd
goarch:
- 386
- amd64
- arm64
- arm
goarm:
- 6
- 7
ignore:
- goos: darwin
goarch: 386
- goos: freebsd
goarch: arm
- goos: freebsd
goarch: arm64
- goos: netbsd
goarch: arm
- goos: netbsd
goarch: arm64
- goos: openbsd
goarch: arm
- goos: openbsd
goarch: arm64
- goos: windows
goarch: arm
archives:
- format: binary
replacements:
darwin: macOS
386: 32-bit
amd64: 64-bit
milestones:
- close: true
changelog:
skip: true
amfora-1.10.0/logger/ 0000755 0001750 0001750 00000000000 14575704331 013654 5 ustar nilesh nilesh amfora-1.10.0/logger/logger.go 0000644 0001750 0001750 00000001365 14575704331 015467 0 ustar nilesh nilesh package logger
// For debugging
import (
"io"
"io/ioutil"
"log"
"os"
)
var Logger *log.Logger
func GetLogger() (*log.Logger, error) {
if Logger != nil {
return Logger, nil
}
var writer io.Writer
var err error
debugModeEnabled := os.Getenv("AMFORA_DEBUG") == "1"
if debugModeEnabled {
writer, err = os.Create("debug.log")
if err != nil {
return nil, err
}
} else {
// Suppress all logging output if debug mode is disabled
writer = ioutil.Discard
}
Logger = log.New(writer, "", log.LstdFlags)
if !debugModeEnabled {
// Clear all flags to skip log output formatting step to increase
// performance somewhat if we're not logging anything
Logger.SetFlags(0)
}
Logger.Println("Started logger")
return Logger, nil
}
amfora-1.10.0/go.mod 0000644 0001750 0001750 00000004535 14575704331 013512 0 ustar nilesh nilesh module github.com/makeworld-the-better-one/amfora
go 1.19
require (
code.rocketnine.space/tslocum/cview v1.5.6-0.20210530175404-7e8817f20bdc
github.com/alecthomas/chroma v0.10.0
github.com/atotto/clipboard v0.1.4
github.com/dustin/go-humanize v1.0.1
github.com/gdamore/tcell/v2 v2.6.0
github.com/makeworld-the-better-one/go-gemini v0.13.1
github.com/makeworld-the-better-one/go-gemini-socks5 v1.0.0
github.com/makeworld-the-better-one/rr v1.0.0
github.com/mitchellh/go-homedir v1.1.0
github.com/mmcdole/gofeed v1.2.1
github.com/muesli/termenv v0.15.2
github.com/rkoesters/xdg v0.0.1
github.com/schollz/progressbar/v3 v3.13.1
github.com/spf13/viper v1.16.0
github.com/stretchr/testify v1.8.4
golang.org/x/text v0.13.0
)
require (
code.rocketnine.space/tslocum/cbind v0.1.5 // indirect
github.com/PuerkitoBio/goquery v1.8.0 // indirect
github.com/andybalholm/cascadia v1.3.1 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gdamore/encoding v1.0.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mmcdole/goxpp v1.1.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.3 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/term v0.13.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
amfora-1.10.0/amfora.desktop 0000644 0001750 0001750 00000000355 14575704331 015240 0 ustar nilesh nilesh [Desktop Entry]
Type=Application
Name=Amfora
GenericName=Gemini TUI Browser
Comment=Browse Gemini in the terminal.
Categories=Network;WebBrowser;ConsoleOnly;
Keywords=gemini
Terminal=true
Exec=amfora %u
MimeType=x-scheme-handler/gemini;
amfora-1.10.0/Makefile 0000644 0001750 0001750 00000002000 14575704331 014025 0 ustar nilesh nilesh GITV != git describe --tags
GITC != git rev-parse --verify HEAD
SRC != find . -type f -name '*.go' ! -name '*_test.go'
TEST != find . -type f -name '*_test.go'
PREFIX ?= /usr/local
VERSION ?= $(GITV)
COMMIT ?= $(GITC)
BUILDER ?= Makefile
GO := go
INSTALL := install
RM := rm
amfora: go.mod go.sum $(SRC)
GO111MODULE=on CGO_ENABLED=0 $(GO) build -o $@ -ldflags="-s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.builtBy=$(BUILDER)"
.PHONY: clean
clean:
$(RM) -f amfora
.PHONY: install
install: amfora amfora.desktop
$(INSTALL) -d $(DESTDIR)$(PREFIX)/bin/
$(INSTALL) -m 755 amfora $(DESTDIR)$(PREFIX)/bin/amfora
$(INSTALL) -d $(DESTDIR)$(PREFIX)/share/applications/
$(INSTALL) -m 644 amfora.desktop $(DESTDIR)$(PREFIX)/share/applications/amfora.desktop
.PHONY: uninstall
uninstall:
$(RM) -f $(DESTDIR)$(PREFIX)/bin/amfora
$(RM) -f $(DESTDIR)$(PREFIX)/share/applications/amfora.desktop
# Development helpers
.PHONY: fmt
fmt:
$(GO) fmt ./...
.PHONY: gen
gen:
$(GO) generate ./...
amfora-1.10.0/default-config.toml 0000644 0001750 0001750 00000033430 14575704331 016164 0 ustar nilesh nilesh # This is the default config file.
# It also shows all the default values, if you don't create the file.
# You can edit this file to set your own configuration for Amfora.
# When Amfora updates, defaults may change, but this file on your drive will not.
# You can always get the latest defaults on GitHub.
# https://github.com/makeworld-the-better-one/amfora/blob/master/default-config.toml
# Please also check out the Amfora Wiki for more help
# https://github.com/makeworld-the-better-one/amfora/wiki
# gemini://makeworld.space/amfora-wiki/
# All URL values may omit the scheme and/or port, as well as the beginning double slash
# Valid URL examples:
# gemini://example.com
# //example.com
# example.com
# example.com:123
[a-general]
# Press Ctrl-H to access it
home = "gemini://geminiprotocol.net"
# Follow up to 5 Gemini redirects without prompting.
# A prompt is always shown after the 5th redirect and for redirects to protocols other than Gemini.
# If set to false, a prompt will be shown before following redirects.
auto_redirect = false
# What command to run to open a HTTP(S) URL.
# Set to "default" to try to guess the browser, or set to "off" to not open HTTP(S) 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 to the URL.
#
# The best way to define a command is using a string array.
# Examples:
# http = ['firefox']
# http = ['custom-browser', '--flag', '--option=2']
# http = ['/path/with spaces/in it/firefox']
#
# Note the use of single quotes, so that backslashes will not be escaped.
# Using just a string will also work, but it is deprecated, and will degrade if
# you use paths with spaces.
http = 'default'
# Any URL that will accept a query string can be put here
search = "gemini://geminispace.info/search"
# Whether colors will be used in the terminal
color = true
# Whether ANSI color codes from the page content should be rendered
ansi = true
# Whether or not to support source code highlighting in preformatted blocks based on alt text
highlight_code = true
# Which highlighting style to use (see https://xyproto.github.io/splash/docs/)
highlight_style = "monokai"
# Whether to replace list asterisks with unicode bullets
bullets = true
# Whether to show link after link text
show_link = false
# The max number of columns to wrap a page's text to. Preformatted blocks are not wrapped.
max_width = 80
# 'downloads' is the path to a downloads folder.
# An empty value means the code will find the default downloads folder for your system.
# If the path does not exist it will be created.
# Note the use of single quotes, so that backslashes will not be escaped.
downloads = ''
# Max size for displayable content in bytes - after that size a download window pops up
page_max_size = 2097152 # 2 MiB
# Max time it takes to load a page in seconds - after that a download window pops up
page_max_time = 10
# When a scrollbar appears. "never", "auto", and "always" are the only valid values.
# "auto" means the scrollbar only appears when the page is longer than the window.
scrollbar = "auto"
# Underline non-gemini URLs
# This is done to help color blind users
underline = true
[auth]
# Authentication settings
# Note the use of single quotes for values, so that backslashes will not be escaped.
[auth.certs]
# Client certificates
# Set URL equal to path to client cert file
#
# "example.com" = 'mycert.crt' # Cert is used for all paths on this domain
# "example.com/dir/"= 'mycert.crt' # Cert is used for /dir/ and everything below only
#
# See the comment at the beginning of this file for examples of all valid types of
# URLs, ports and schemes can be used too
[auth.keys]
# Client certificate keys
# Same as [auth.certs] but the path is to the client key file.
[keybindings]
# If you have a non-US keyboard, use bind_tab1 through bind_tab0 to
# setup the shift-number bindings: Eg, for US keyboards (the default):
# bind_tab1 = "!"
# bind_tab2 = "@"
# bind_tab3 = "#"
# bind_tab4 = "$"
# bind_tab5 = "%"
# bind_tab6 = "^"
# bind_tab7 = "&"
# bind_tab8 = "*"
# bind_tab9 = "("
# bind_tab0 = ")"
# Whitespace is not allowed in any of the keybindings! Use 'Space' and 'Tab' to bind to those keys.
# Multiple keys can be bound to one command, just use a TOML array.
# To add the Alt modifier, the binding must start with Alt-, should be reasonably universal
# Ctrl- won't work on all keys, see this for a list:
# https://github.com/gdamore/tcell/blob/cb1e5d6fa606/key.go#L83
# An example of a TOML array for multiple keys being bound to one command is the default
# binding for reload:
# bind_reload = ["R","Ctrl-R"]
# One thing to note here is that "R" is capitalization sensitive, so it means shift-r.
# "Ctrl-R" means both ctrl-r and ctrl-shift-R (this is a quirk of what ctrl-r means on
# an ANSI terminal)
# The default binding for opening the bottom bar for entering a URL or link number is:
# bind_bottom = "Space"
# This is how to get the Spacebar as a keybinding, if you try to use " ", it won't work.
# And, finally, an example of a simple, unmodified character is:
# bind_edit = "e"
# This binds the "e" key to the command to edit the current URL.
# The bind_link[1-90] options are for the commands to go to the first 10 links on a page,
# typically these are bound to the number keys:
# bind_link1 = "1"
# bind_link2 = "2"
# bind_link3 = "3"
# bind_link4 = "4"
# bind_link5 = "5"
# bind_link6 = "6"
# bind_link7 = "7"
# bind_link8 = "8"
# bind_link9 = "9"
# bind_link0 = "0"
# All keybindings:
#
# bind_bottom
# bind_edit
# bind_home
# bind_bookmarks
# bind_add_bookmark
# bind_save
# bind_reload
# bind_back
# bind_forward
# bind_moveup
# bind_movedown
# bind_moveleft
# bind_moveright
# bind_pgup
# bind_pgdn
# bind_new_tab
# bind_close_tab
# bind_next_tab
# bind_prev_tab
# bind_quit
# bind_help
# bind_sub: for viewing the subscriptions page
# bind_add_sub
# bind_copy_page_url
# bind_copy_target_url
# bind_beginning: moving to beginning of page (top left)
# bind_end: same but the for the end (bottom left)
# bind_url_handler_open: Open highlighted URL with URL handler (#143)
[url-handlers]
# Allows setting the commands to run for various URL schemes.
# E.g. to open FTP URLs with FileZilla set the following key:
# ftp = ['filezilla']
# You can set any scheme to 'off' or '' to disable handling it, or
# just leave the key unset.
#
# DO NOT use this for setting the HTTP command.
# Use the http setting in the "a-general" section above.
#
# NOTE: These settings are overrided by the ones in the proxies section.
#
# The best way to define a command is using a string array.
# Examples:
# magnet = ['transmission']
# foo = ['custom-browser', '--flag', '--option=2']
# tel = ['/path/with spaces/in it/telephone']
#
# Note the use of single quotes, so that backslashes will not be escaped.
# Using just a string will also work, but it is deprecated, and will degrade if
# you use paths with spaces.
# This is a special key that defines the handler for all URL schemes for which
# no handler is defined.
# It uses the special value 'default', which will try and use the default
# application on your computer for opening this kind of URI.
other = 'default'
[url-prompts]
# Specify whether a confirmation prompt should be shown before following URL schemes.
# The special key 'other' matches all schemes that don't match any other key.
#
# Example: prompt on every non-gemini URL
# other = true
# gemini = false
#
# Example: only prompt on HTTP(S)
# other = false
# http = true
# https = true
# [[mediatype-handlers]] section
# ---------------------------------
#
# Specify what applications will open certain media types.
# By default your default application will be used to open the file when you select "Open".
# You only need to configure this section if you want to override your default application,
# or do special things like streaming.
#
# Note the use of single quotes for commands, so that backslashes will not be escaped.
#
#
# To open jpeg files with the feh command:
#
# [[mediatype-handlers]]
# cmd = ['feh']
# types = ["image/jpeg"]
#
# Each command that you specify must come under its own [[mediatype-handlers]]. You may
# specify as many [[mediatype-handlers]] as you want to setup multiple commands.
#
# If the subtype is omitted then the specified command will be used for the
# entire type:
#
# [[mediatype-handlers]]
# command = ['vlc', '--flag']
# types = ["audio", "video"]
#
# A catch-all handler can by specified with "*".
# Note that there are already catch-all handlers in place for all OSes,
# that open the file using your default application. This is only if you
# want to override that.
#
# [[mediatype-handlers]]
# cmd = ['some-command']
# types = [
# "application/pdf",
# "*",
# ]
#
# You can also choose to stream the data instead of downloading it all before
# opening it. This is especially useful for large video or audio files, as
# well as radio streams, which will never complete. You can do this like so:
#
# [[mediatype-handlers]]
# cmd = ['vlc', '-']
# types = ["audio", "video"]
# stream = true
#
# This uses vlc to stream all video and audio content.
# By default stream is set to off for all handlers
#
#
# If you want to always open a type in its viewer without the download or open
# prompt appearing, you can add no_prompt = true
#
# [[mediatype-handlers]]
# cmd = ['feh']
# types = ["image"]
# no_prompt = true
#
# Note: Multiple handlers cannot be defined for the same full media type, but
# still there needs to be an order for which handlers are used. The following
# order applies regardless of the order written in the config:
#
# 1. Full media type: "image/jpeg"
# 2. Just type: "image"
# 3. Catch-all: "*"
[cache]
# Options for page cache - which is only for text pages
# Increase the cache size to speed up browsing at the expense of memory
# Zero values mean there is no limit
max_size = 0 # Size in bytes
max_pages = 30 # The maximum number of pages the cache will store
# How long a page will stay in cache, in seconds.
timeout = 1800 # 30 mins
[proxies]
# Allows setting a Gemini proxy for different schemes.
# The settings are similar to the url-handlers section above.
# E.g. to open a gopher page by connecting to a Gemini proxy server:
# gopher = "example.com:123"
#
# Port 1965 is assumed if no port is specified.
#
# NOTE: These settings override any external handlers specified in
# the url-handlers section.
#
# Note that HTTP and HTTPS are treated as separate protocols here.
[subscriptions]
# For tracking feeds and pages
# Whether a pop-up appears when viewing a potential feed
popup = true
# How often to check for updates to subscriptions in the background, in seconds.
# Set it to 0 to disable this feature. You can still update individual feeds
# manually, or restart the browser.
#
# Note Amfora will check for updates on browser start no matter what this setting is.
update_interval = 1800 # 30 mins
# How many subscriptions can be checked at the same time when updating.
# If you have many subscriptions you may want to increase this for faster
# update times. Any value below 1 will be corrected to 1.
workers = 3
# The number of subscription updates displayed per page.
entries_per_page = 20
# Set to false to remove the explanatory text from the top of the subscription page
header = true
[theme]
# This section is for changing the COLORS used in Amfora.
# These colors only apply if 'color' is enabled above.
# Colors can be set using a W3C color name, or a hex value such as "#ffffff".
# Setting a background to "default" keeps the terminal default
# If your terminal has transparency, set any background to "default" to keep it transparent
# The key "bg" is already set to "default", but this can be used on other backgrounds,
# like for modals.
# Note that not all colors will work on terminals that do not have truecolor support.
# If you want to stick to the standard 16 or 256 colors, you can get
# a list of those here: https://jonasjacek.github.io/colors/
# DO NOT use the names from that site, just the hex codes.
# Definitions:
# bg = background
# fg = foreground
# dl = download
# btn = button
# hdg = heading
# bkmk = bookmark
# modal = a popup window/box in the middle of the screen
# EXAMPLES:
# hdg_1 = "green"
# hdg_2 = "#5f0000"
# bg = "default"
# Available keys to set:
# bg: background for pages, tab row, app in general
# tab_num: The number/highlight of the tabs at the top
# tab_divider: The color of the divider character between tab numbers: |
# bottombar_label: The color of the prompt that appears when you press space
# bottombar_text: The color of the text you type
# bottombar_bg
# scrollbar: The scrollbar that appears on the right for long pages
# You can also set an 'include' key to process another TOML file that contains theme keys.
# Example:
# include = "my/path/to/special-theme.toml"
#
# Any other theme keys will override this external file.
# You can use this special key to switch between themes easily.
# Download other themes here: https://github.com/makeworld-the-better-one/amfora/tree/master/contrib/themes
# hdg_1
# hdg_2
# hdg_3
# amfora_link: A link that Amfora supports viewing. For now this is only gemini://
# foreign_link: HTTP(S), Gopher, etc
# link_number: The silver number that appears to the left of a link
# regular_text: Normal gemini text, and plaintext documents
# quote_text
# preformatted_text
# list_text
# btn_bg: The bg color for all modal buttons
# btn_text: The text color for all modal buttons
# dl_choice_modal_bg
# dl_choice_modal_text
# dl_modal_bg
# dl_modal_text
# info_modal_bg
# info_modal_text
# error_modal_bg
# error_modal_text
# yesno_modal_bg
# yesno_modal_text
# tofu_modal_bg
# tofu_modal_text
# subscription_modal_bg
# subscription_modal_text
# input_modal_bg
# input_modal_text
# input_modal_field_bg: The bg of the input field, where you type the text
# input_modal_field_text: The color of the text you type
# bkmk_modal_bg
# bkmk_modal_text
# bkmk_modal_label
# bkmk_modal_field_bg
# bkmk_modal_field_text
amfora-1.10.0/CHANGELOG.md 0000644 0001750 0001750 00000037703 14575704331 014220 0 ustar nilesh nilesh # Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.10.0] - 2024-03-17
### Added
- Syntax highlighting for preformatted text blocks with alt text (#252, #263, [wiki page](https://github.com/makeworld-the-better-one/amfora/wiki/Source-Code-Highlighting))
- [Client certificates](https://github.com/makeworld-the-better-one/amfora/wiki/Client-Certificates) can be restricted to certain paths of a host (#115)
- `header` config option in `[subscriptions]` to allow disabling the header text on the subscriptions page (#191)
- Selected link and scroll position stays for non-cached pages (#122)
- Keybinding to open URL with URL handler instead of configured proxy (#143)
- `include` theme key to import themes from an external file (#154, #290)
- Support SOCKS5 proxying by setting `AMFORA_SOCKS5` environment variable (#155)
- When bookmarking a page, the first level one heading is suggested as the name (#267, #293)
- Confirmation prompts for URL schemes in new `[url-prompts]` config section (#301, #302)
### Changed
- Center text automatically, removing `left_margin` from the config (#233)
- `max_width` defaults to 80 columns instead of 100 (#233)
- Tabs have the domain of the current page instead of numbers (#202)
- Closing Amfora with q was removed in favor of Shift-q (#243)
- Paging up or down scrolls by 50% instead of 75%, to match `less` (#303)
- Update deps, require Go 1.17 (#336)
- Show local directory index file if available (#319)
- Updated Project Gemini URLs (#342)
### Fixed
- Modal can't be closed when opening non-gemini text URLs from the commandline (#283, #284)
- External programs started by Amfora remain as zombie processes (#219)
- Prevent link lines (and other types) from being wider than the `max_width` setting (#280)
- `new:7` on new tab page fails to open link (#306)
- Slashes aren't decoded in redirect URLs (#322, #324)
- Typing `localhost` in the bottom bar actually loads localhost instead of searching (#326, #327)
## [1.9.2] - 2021-12-10
### Fixed
- Preformatted text color showing even when `color = false` (bug since v1.8.0 at least) (#278)
- Link numbers and link text in color even when `color = false` (regression in v1.9.0) (#278)
## [1.9.1] - 2021-12-08
### Fixed
- Deadlock when loading an invalid `about:` URL (#277)
- Crash when rendering text from stdin
## [1.9.0] - 2021-12-07
### Added
- Support for version 1.1 JSON feeds
- Copy current URL or selected URL to clipboard (#220, #225)
- Uses C and c by default
- Configurable keybindings for scrolling on pages (#211, #222)
- Ability to save `about:` pages (#210, #236)
- `bind_beginning` and `bind_end` keybindings
- Display gemtext from stdin (#205, #242)
- Specifying `default` in the theme config uses the terminal's default background color, including transparency (#244, #245)
- Redirects occur automatically if it only adds a trailing slash (#271)
- Non-gemini links are underlined by default to help color blind users (#189)
- Text and element colors of default theme change to be black on terminals with light backgrounds (#181)
- Support paths with spaces in `[url-handlers]` config settings (#214)
- Display info modal when opening URL with custom application
- Files can be opened by relative path on the commandline (#231, #257)
- Support keybindings that use Shift (#269)
### Changed
- Bookmarks are stored using XML in the XBEL format, old bookmarks are transferred (#68)
- Text no longer disappears under the left margin when scrolling (regression in v1.8.0) (#197)
- Default search engine changed to geminispace.info from gus.guru
- The user's terminal theme colors are used by default (#181)
- By default, non-gemini URI schemes are opened in the default application. This requires a config change for previous users, see the [wiki](https://github.com/makeworld-the-better-one/amfora/wiki/Handling-Other-URL-Schemes) (#207)
- Windows uses paths set by `XDG` variables over `APPDATA` if they are set (#255)
- Treat status codes like 22 as equivalent to 20 as per the latest spec (#266)
- Show minimal loading page instead of `about:newtab` when loading a URL in a new tab (#272)
## Removed
- Favicon support (#199)
- The default Amfora theme, get it back [here](https://github.com/makeworld-the-better-one/amfora/blob/master/contrib/themes/amfora.toml) (#181)
### Fixed
- Help text is now the same color as `regular_text` in the theme config
- Non-ASCII (multibyte) characters can now be used as keybindings (#198, #200)
- Possible subscription update race condition on startup
- Plaintext documents are escaped properly (regression in v1.8.0)
- Help page scrollbar color matches what's in the theme config
- Regression where lists would not appear if `bullets = false` (#234, #235)
- Support multiple bookmarks with the same name
- Cert change message grammar: "an security" -> "a security" (#274)
- Display an error modal for status codes that can't be handled
- Prevent user from getting trapped in the help menu when keybindings are pressed (#241, #261)
## [1.8.0] - 2021-02-17
### Added
- **Media type handlers** - open non-text files in another application (#121, #134)
- Ability to set custom keybindings in config (#135)
- Added scrollbar, by default only appears on pages that go off-screen (#89, #107)
- More internal about pages, see `about:about` (#160, #187)
### Changed
- Update cview to `d776e728ef6d2a9990a5cd86a70b31f0678613e2` for large performance and feature updates (#107)
- Update to tcell v2 (dependency of cview)
- Display page even if mediatype params are malformed (#141)
- Sensitive input fields (status code 11) display with asterisks over the text (#106)
### Fixed
- Don't use cache when URL is typed in bottom bar (#159)
- Fix downloading of pages that are too large or timed out
- `about:` URLs can be typed into the bottom bar (#167)
- Bookmarks modal closes on ESC like the others (#173)
- Handle empty META string (#176)
- Whitespace around the URL entered in the bottom bar is stripped (#184)
- Don't break visiting IPv6 hosts when port 1965 is specified (#195)
- More reliable start, no more flash of unindented text, or text that stays unindented (#107)
- Pages with ANSI resets don't use the terminal's default text and background colors (#107)
- ANSI documents don't leak color into the left margin (#107)
- Rendering very long documents is now ~96% faster, excluding gemtext parsing (#26, #107)
- Due to that same change, less memory is used per-page (#26, #107)
## [1.7.2] - 2020-12-21
### Fixed
- Viewing subscriptions after subscribing to a certain user page won't crash Amfora (#157)
## [1.7.1] - 2020-12-21
### Fixed
- Fixed bug that caused Amfora to crash when subscribing to a page (#151)
## [1.7.0] - 2020-12-20
### Added
- **Subscriptions** to feeds and page changes (#61)
- Opening local files with `file://` URIs (#103, #117)
- `show_link` option added in config to optionally see the URL (#133)
- Support for Unicode in domain names (IDNs)
- Unnecessarily encoded characters in URLs will be decoded (#138)
- URLs are NFC-normalized before any processing (#138)
- Links to the wiki in the new tab
- Cache times out after 30 minutes by default (#110)
- `about:version` page (#126)
### Changed
- Updated [go-gemini](https://github.com/makeworld-the-better-one/go-gemini) to v0.11.0
- Supports CN-only wildcard certs
- Time out when header takes too long
- Preformatted text is now light yellow by default
- Downloading a file no longer uses a second request
- You can go back to the new tab page in history (#96)
### Fixed
- Single quotes are used in the default config for commands and paths so that Windows paths with backslashes will be parsed correctly
- Downloading now uses proxies when appropriate
- User-entered URLs with invalid characters will be percent-encoded (#138)
- Custom downloads dir is actually used (#148)
- Empty quote lines no longer disappear
## [1.6.0] - 2020-11-04
### Added
- **Support client certificates** through config (#112)
- `ansi` config setting, to disable ANSI colors in pages (#79, #86)
- Edit current URL with e (#87)
- If `emoji_favicons` is enabled, new bookmarks will have the domain's favicon prepended (#69, #90)
- The `BROWSER` env var is now also checked when opening web links on Unix (#93)
- More accurate error messages based on server response code
### Changed
- Disabling the `color` config setting also disables ANSI colors in pages (#79, #86)
- Updated [go-isemoji](https://github.com/makeworld-the-better-one/go-isemoji) to v1.1.0 to support Emoji 13.1 for favicons
- The web browser code doesn't check for Xorg anymore, just display variables (#93)
- Bookmarks can be made to non-gemini URLs (#94)
- Remove pointless directory fallbacks (#101)
- Don't load page from cache when redirected to it (#114)
### Fixed
- XDG user dir file is parsed instead of looking for XDG env vars (#97, #100)
- Support paths with spaces in HTTP browser config setting (#77)
- Clicking "Change" on an existing bookmark without changing the text no longer removes it (#91)
- Display HTTP Error if "Open In Portal" fails (#81)
- Support ANSI color codes again, but only in preformatted blocks (#59)
- Make the `..` command work lke it used to in v1.4.0
## [1.5.0] - 2020-09-01
### Added
- **Proxy support** - see the `[proxies]` section in the config (#66, #80)
- **Emoji favicons** can now be seen if `emoji_favicons` is enabled in the config (#62)
- `shift_numbers` key in the config was added, so that non US keyboard users can navigate tabs (#64)
- F1 and F2 keys for navigating to the previous and next tabs (#64)
- Resolving any relative path (starts with a `.`) in the bottom bar is supported, not just `..` (#71)
- You can now set external programs in the config to open other schemes, like `gopher://` or `magnet:` (#74)
- Auto-redirecting can be enabled - redirect within Gemini up to 5 times automatically (#75)
- Help page now documents paging keys (#78)
- The new tab page can be customized by creating a gemtext file called `newtab.gmi` in the config directory (#67, #83)
### Changed
- Update to [go-gemini](https://github.com/makeworld-the-better-one/go-gemini) v0.8.4
### Fixed
- Two digit (and higher) link texts are now in line with one digit ones (#60)
- Race condition when reloading pages that could have caused the cache to still be used
- Prevent panic (crash) when the server sends an error with an empty meta string (#73)
- URLs with with colon-only schemes (like `mailto:`) are properly recognized
- You can no longer navigate through the history when the help page is open (#55, #78)
## [1.4.0] - 2020-07-28
### Added
- **Theming** - check out [default-config.toml](./default-config.toml) for details (#46)
- Tab now also enters link selecting mode, like Enter (#48)
- Number keys can be pressed to navigate to links 1 through 10 (#47)
- Permanent redirects are cached for the session (#22)
- `.ansi` is also supported for `text/x-ansi` files, as well as the already supported `.ans`
### Changed
- Documented Ctrl-C as "Hard quit"
- Updated [cview](https://gitlab.com/tslocum/cview/) to latest commit: `cc7796c4ca44e3908f80d93e92e73694562d936a`
- The bottom bar label now uses the same color as the tabs at the top
- Tab and blue link colors were changed very slightly to be part of the 256 Xterm colors, for better terminal support
### Fixed
- You can't change link selection while the page is loading
- Only one request is made for each URL - `v1.3.0` accidentally made two requests each time (#50)
- Using the `..` command doesn't keep the query string (#49)
- Any error that occurs when downloading a file will be displayed, and the partially downloaded file will be deleted
- Allow for opening a new tab while the current one is loading
- Pressing Escape after typing in the bottom bar no longer jumps you back to the top of the page
- Repeated redirects where the last one is cancelled by the user doesn't leave the `Loading...` text in the bottom bar (#53)
## [1.3.0] - 2020-07-10
### Added
- **Downloading content** (#38)
- Configurable page size limit - `page_max_size` in config (#30)
- Configurable page timeout - `page_max_time` in config
- Link and heading lines are wrapped just like regular text lines
- Wrapped list items are indented to stay behind the bullet (#35)
- Certificate expiry date is stored when the cert IDs match (#39)
- What link was selected is remembered as you browse through history
- Render ANSI codes in `text/x-ansi` pages, or text pages that end with `.ans` (#45)
### Changed
- Pages are rewrapped dynamically, whenever the terminal size changes (#33)
- TOFU warning message mentions how long the previous cert was still valid for (#34)
### Fixed
- Many potential network and display race conditions eliminated
- Whether a tab is loading stays indicated when you switch away from it and go back
- Plain text documents are displayed faithfully (there were some edge conditions)
- Opening files in portal.mozz.us uses the `http` setting in the config (#42)
## [1.2.0] - 2020-07-02
### Added
- Alt-Left and Alt-Right for history navigation (#23)
- You can type `..` in the bottom bar to go up a directory in the URL (#21)
- Error popup for when input string would result in a too long out-of-spec URL (#25)
- Paging, using d and u, as well as Page Up and Page Down (#19)
- Esc can exit link highlighting mode (#24)
- Selected link URL is displayed in the bottom bar (#24)
- Pressing Ctrl-T with a link selected opens it in a new tab (#27)
- Writing `new:N` in the bottom bar will open link number N in a new tab (#27)
- Quote lines are now in italics (#28)
### Changed
- Bottom bar now says `URL/Num./Search: ` when space is pressed
- Update to [go-gemini](https://github.com/makeworld-the-better-one/go-gemini) v0.6.0
- Help layout doesn't have borders anymore
- Pages with query strings are still cached (#29)
- URLs or searches typed in the bottom bar are not loaded from the cache (#29)
### Fixed
- Actual unicode bullet symbol is used for lists: U+2022
- Performance when loading very long cached pages improved (#26)
- Doesn't crash when wrapping certain complex lines (#20)
- Input fields are always in focus when they appear (#5)
- Reloading the new tab page doesn't cause an error popup
- Help table cells are hardwrapped so the text can still be read entirely on an 80-column terminal
- New tab text is wrapped to terminal width like other pages (#31)
- TOFU "continue anyway" popup has a question mark at the end
## [1.1.0] - 2020-06-24
### Added
- **Bookmarks** (#10)
- **Support over 55 charsets** (#3)
- **Search using the bottom bar**
- Add titles to all modals
- Store ports in TOFU database (#7)
- Search from bottom bar
- Wrapping based on terminal width (#1)
- `left_margin` config option (#1)
- Right margin for text (#1)
- Desktop entry file
- Option to continue anyway when cert doesn't match TOFU database
- Display all `text/*` documents, not just gemini and plain (#12)
- Prefer XDG environment variables if they're set, to specify config dir, etc (#11)
- Version and help commands - `-v`, `--version`, `--help`, `-h` (#14)
### Changed
- Connection timeout is 15 seconds (was 5s)
- Hash `SubjectPublicKeyInfo` for TOFU instead (#7)
- `wrap_width` config option became `max_width` (#1)
- Make the help table look better
### Removed
- Opening multiple URLs from the command line
### Fixed
- Reset bottom bar on error / invalid URL
- Side scrolling doesn't cut off text on the left side (#1)
- Mark status code 21 as invalid
- Bottom bar is not in focus after clicking Enter
- Badly formed links on pages can no longer crash the browser
- Disabling color in config affects UI elements (#16)
- Keep bold for headings even with color disabled
- Don't make whole link text bold when color is disabled
- Get domain from URL for TOFU, not from certificate
## [1.0.0] - 2020-06-18
Initial release.
### Added
- Tabbed browsing
- TOFU
- Styled content
- Basic history for each tab
- Input
amfora-1.10.0/renderer/ 0000755 0001750 0001750 00000000000 14575704331 014203 5 ustar nilesh nilesh amfora-1.10.0/renderer/page.go 0000644 0001750 0001750 00000010660 14575704331 015451 0 ustar nilesh nilesh package renderer
import (
"bytes"
"errors"
"io"
"mime"
"os"
"strings"
"time"
"github.com/makeworld-the-better-one/amfora/structs"
"github.com/makeworld-the-better-one/go-gemini"
"github.com/spf13/viper"
"golang.org/x/text/encoding/ianaindex"
)
var ErrTooLarge = errors.New("page content would be too large")
var ErrTimedOut = errors.New("page download timed out")
var ErrCantDisplay = errors.New("invalid content for a page")
var ErrBadEncoding = errors.New("unsupported encoding")
var ErrBadMediatype = errors.New("displayable mediatype is not handled in the code, implementation error")
// isUTF8 returns true for charsets that are compatible with UTF-8 and don't need to be decoded.
func isUTF8(charset string) bool {
utfCharsets := []string{"", "utf-8", "us-ascii"}
for _, s := range utfCharsets {
if charset == s || strings.ToLower(charset) == s {
return true
}
}
return false
}
// decodeMeta returns the output of mime.ParseMediaType, but handles the empty
// META which is equal to "text/gemini; charset=utf-8" according to the spec.
func decodeMeta(meta string) (string, map[string]string, error) {
if meta == "" {
return "text/gemini", make(map[string]string), nil
}
mediatype, params, err := mime.ParseMediaType(meta)
if mediatype != "" && err != nil {
// The mediatype was successfully decoded but there's some error with the params
// Ignore the params
return mediatype, make(map[string]string), nil
}
return mediatype, params, err
}
// 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 := decodeMeta(res.Meta)
if err != nil {
return false
}
if !strings.HasPrefix(mediatype, "text/") {
// Amfora doesn't support other filetypes
return false
}
if isUTF8(params["charset"]) {
return true
}
enc, err := ianaindex.MIME.Encoding(params["charset"]) // Lowercasing is done inside
// Encoding sometimes returns nil, see #3 on this repo and golang/go#19421
return err == nil && enc != nil
}
// MakePage creates a formatted, rendered Page from the given network response and params.
// You must set the Page.Width value yourself.
func MakePage(url string, res *gemini.Response, width int, proxied bool) (*structs.Page, error) {
if !CanDisplay(res) {
return nil, ErrCantDisplay
}
buf := new(bytes.Buffer)
_, err := io.CopyN(buf, res.Body, viper.GetInt64("a-general.page_max_size")+1)
if err == nil {
// Content was larger than max size
return nil, ErrTooLarge
} else if err != io.EOF {
if os.IsTimeout(err) {
// I would use
// errors.Is(err, os.ErrDeadlineExceeded)
// but that isn't supported before Go 1.15.
return nil, ErrTimedOut
}
// Some other error
return nil, err
}
// Otherwise, the error is EOF, which is what we want.
mediatype, params, _ := decodeMeta(res.Meta)
// Convert content first
var utfText string
if isUTF8(params["charset"]) {
utfText = buf.String()
} else {
encoding, err := ianaindex.MIME.Encoding(params["charset"])
if encoding == nil || err != nil {
// Some encoding doesn't exist and wasn't caught in CanDisplay()
return nil, ErrBadEncoding
}
utfText, err = encoding.NewDecoder().String(buf.String())
if err != nil {
return nil, err
}
}
if mediatype == "text/gemini" {
rendered, links := RenderGemini(utfText, width, proxied)
return &structs.Page{
Mediatype: structs.TextGemini,
RawMediatype: mediatype,
URL: url,
Raw: utfText,
Content: rendered,
Links: links,
MadeAt: time.Now(),
}, nil
} else if strings.HasPrefix(mediatype, "text/") {
if mediatype == "text/x-ansi" || strings.HasSuffix(url, ".ans") || strings.HasSuffix(url, ".ansi") {
// ANSI
return &structs.Page{
Mediatype: structs.TextAnsi,
RawMediatype: mediatype,
URL: url,
Raw: utfText,
Content: RenderANSI(utfText),
Links: []string{},
MadeAt: time.Now(),
}, nil
}
// Treated as plaintext
return &structs.Page{
Mediatype: structs.TextPlain,
RawMediatype: mediatype,
URL: url,
Raw: utfText,
Content: RenderPlainText(utfText),
Links: []string{},
MadeAt: time.Now(),
}, nil
}
return nil, ErrBadMediatype
}
amfora-1.10.0/renderer/renderer.go 0000644 0001750 0001750 00000035517 14575704331 016353 0 ustar nilesh nilesh // 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 (
"bytes"
"fmt"
urlPkg "net/url"
"regexp"
"strconv"
"strings"
"code.rocketnine.space/tslocum/cview"
"github.com/alecthomas/chroma/formatters"
"github.com/alecthomas/chroma/lexers"
"github.com/alecthomas/chroma/styles"
"github.com/makeworld-the-better-one/amfora/config"
"github.com/spf13/viper"
)
// Terminal color information, set during display initialization by display/display.go
var TermColor string
// Regex for identifying ANSI color codes
var ansiRegex = regexp.MustCompile(`\x1b\[[0-9;]*m`)
// Regex for identifying possible language string, based on RFC 6838 and lexers used by Chroma
var langRegex = regexp.MustCompile(`^([a-zA-Z0-9]+/)?[a-zA-Z0-9]+([a-zA-Z0-9!_\#\$\&\-\^\.\+]+)*`)
// Regex for removing trailing newline (without disturbing ANSI codes) from code formatted with Chroma
var trailingNewline = regexp.MustCompile(`(\r?\n)(?:\x1b\[[0-9;]*m)*$`)
// RenderANSI renders plain text pages containing ANSI codes.
// Practically, it is used for the text/x-ansi.
func RenderANSI(s string) string {
s = cview.Escape(s)
if viper.GetBool("a-general.color") && viper.GetBool("a-general.ansi") {
s = cview.TranslateANSI(s)
} else {
s = ansiRegex.ReplaceAllString(s, "")
}
return s
}
// RenderPlainText should be used to format plain text pages.
func RenderPlainText(s string) string {
// It used to add a left margin, now this is done elsewhere.
// The function is kept for convenience and in case rendering
// is needed in the future.
return cview.Escape(s)
}
// wrapLine wraps a line to the provided width, and adds the provided prefix and suffix to each wrapped line.
// It recovers from wrapping panics and should never cause a panic.
// It returns a slice of lines, without newlines at the end.
//
// Set includeFirst to true if the prefix and suffix should be applied to the first wrapped line as well
func wrapLine(line string, width int, prefix, suffix string, includeFirst bool) []string {
if width < 1 {
width = 1
}
// Anonymous function to allow recovery from potential WordWrap panic
var ret []string
func() {
defer func() {
if r := recover(); r != nil {
// Use unwrapped line instead
if includeFirst {
ret = []string{prefix + line + suffix}
} else {
ret = []string{line}
}
}
}()
wrapped := cview.WordWrap(line, width)
for i := range wrapped {
if !includeFirst && i == 0 {
continue
}
wrapped[i] = prefix + wrapped[i] + suffix
}
ret = wrapped
}()
return ret
}
// convertRegularGemini converts non-preformatted blocks of text/gemini
// into a cview-compatible format.
// Since this only works on non-preformatted blocks, RenderGemini
// should always be used instead.
//
// It also returns a slice of link URLs.
// numLinks is the number of links that exist so far.
// width is the number of columns to wrap to.
//
//
// proxied is whether the request is through the gemini:// scheme.
// If it's not a gemini:// page, set this to true.
func convertRegularGemini(s string, numLinks, width int, proxied bool) (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], "#") {
// Headings
var tag string
if viper.GetBool("a-general.color") {
if strings.HasPrefix(lines[i], "###") {
tag = fmt.Sprintf("[%s::b]", config.GetColorString("hdg_3"))
} else if strings.HasPrefix(lines[i], "##") {
tag = fmt.Sprintf("[%s::b]", config.GetColorString("hdg_2"))
} else if strings.HasPrefix(lines[i], "#") {
tag = fmt.Sprintf("[%s::b]", config.GetColorString("hdg_1"))
}
wrappedLines = append(wrappedLines, wrapLine(lines[i], width, tag, "[-::-]", true)...)
} else {
// Just bold, no colors
wrappedLines = append(wrappedLines, wrapLine(lines[i], width, "[::b]", "[-::-]", true)...)
}
// 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 viper.GetBool("a-general.show_link") {
linkText += " (" + url + ")"
}
}
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)
num := numLinks + len(links) // Visible link number, one-indexed
var indent int
if num > 99 {
// Indent link text by 3 or more spaces
indent = len(strconv.Itoa(num)) + 4 // +4 indent for spaces and brackets
} else {
// One digit and two digit links have the same spacing - see #60
indent = 5 // +4 indent for spaces and brackets, and 1 for link number
}
// Spacing after link number: 1 or 2 spaces?
var spacing string
if num > 9 {
// One space to keep it in line with other links - see #60
spacing = " "
} else {
// One digit numbers use two spaces
spacing = " "
}
// Underline non-gemini links if enabled
var linkTag string
if viper.GetBool("a-general.underline") {
linkTag = `[` + config.GetColorString("foreign_link") + `::u]`
} else {
linkTag = `[` + config.GetColorString("foreign_link") + `]`
}
// Wrap and add link text
// Wrap the link text, but add some spaces to indent the wrapped lines past the link number
// Set the style tags
// Add them to the first line
var wrappedLink []string
pU, err := urlPkg.Parse(url)
if !proxied && err == nil &&
(pU.Scheme == "" || pU.Scheme == "gemini" || pU.Scheme == "about") {
// A gemini link
if viper.GetBool("a-general.color") {
// Add the link text in blue (in a region), and a gray link number to the left of it
// Those are the default colors, anyway
wrappedLink = wrapLine(linkText, width-indent,
strings.Repeat(" ", indent)+
`["`+strconv.Itoa(num-1)+`"][`+config.GetColorString("amfora_link")+`]`,
`[-][""]`,
false, // Don't indent the first line, it's the one with link number
)
// Add special stuff to first line, like the link number
wrappedLink[0] = fmt.Sprintf(`[%s::b][`, config.GetColorString("link_number")) +
strconv.Itoa(num) + "[]" + "[-::-]" + spacing +
`["` + strconv.Itoa(num-1) + `"][` + config.GetColorString("amfora_link") + `]` +
wrappedLink[0] + `[-][""]`
} else {
// No color
wrappedLink = wrapLine(linkText, width-indent,
strings.Repeat(" ", indent)+ // +4 for spaces and brackets
`["`+strconv.Itoa(num-1)+`"]`,
`[""]`,
false, // Don't indent the first line, it's the one with link number
)
wrappedLink[0] = `[::b][` + strconv.Itoa(num) + "[][::-] " +
`["` + strconv.Itoa(num-1) + `"]` +
wrappedLink[0] + `[""]`
}
} else {
// Not a gemini link
if viper.GetBool("a-general.color") {
// Color
wrappedLink = wrapLine(linkText, width-indent,
strings.Repeat(" ", indent)+
`["`+strconv.Itoa(num-1)+`"]`+linkTag,
`[-::-][""]`,
false, // Don't indent the first line, it's the one with link number
)
wrappedLink[0] = fmt.Sprintf(`[%s::b][`, config.GetColorString("link_number")) +
strconv.Itoa(num) + "[][-::-]" + spacing +
`["` + strconv.Itoa(num-1) + `"]` + linkTag +
wrappedLink[0] + `[-::-][""]`
} else {
// No color
wrappedLink = wrapLine(linkText, width-indent,
strings.Repeat(" ", indent)+
`["`+strconv.Itoa(num-1)+`"]`,
`[::-][""]`,
false, // Don't indent the first line, it's the one with link number
)
wrappedLink[0] = `[::b][` + strconv.Itoa(num) + "[][::-]" + spacing +
`["` + strconv.Itoa(num-1) + `"]` +
wrappedLink[0] + `[::-][""]`
}
}
wrappedLines = append(wrappedLines, wrappedLink...)
// Lists
} else if strings.HasPrefix(lines[i], "* ") {
if viper.GetBool("a-general.bullets") {
// Wrap list item, and indent wrapped lines past the bullet
wrappedItem := wrapLine(lines[i][1:],
width-4, // Subtract the 4 indent spaces
fmt.Sprintf(" [%s]", config.GetColorString("list_text")),
"[-]", false)
// Add bullet
wrappedItem[0] = fmt.Sprintf(" [%s]\u2022", config.GetColorString("list_text")) +
wrappedItem[0] + "[-]"
wrappedLines = append(wrappedLines, wrappedItem...)
} else {
wrappedItem := wrapLine(lines[i][1:],
width-4, // Subtract the 4 indent spaces
fmt.Sprintf(" [%s]", config.GetColorString("list_text")),
"[-]", false)
// Add "*"
wrappedItem[0] = fmt.Sprintf(" [%s]*", config.GetColorString("list_text")) +
wrappedItem[0] + "[-]"
wrappedLines = append(wrappedLines, wrappedItem...)
}
// Optionally list lines could be colored here too, if color is enabled
} else if strings.HasPrefix(lines[i], ">") {
// It's a quote line, add extra quote symbols and italics to the start of each wrapped line
if len(lines[i]) == 1 {
// Just an empty quote line
wrappedLines = append(wrappedLines, fmt.Sprintf("[%s::i]>[-::-]", config.GetColorString("quote_text")))
} else {
// Remove beginning quote and maybe space
lines[i] = strings.TrimPrefix(lines[i], ">")
lines[i] = strings.TrimPrefix(lines[i], " ")
wrappedLines = append(wrappedLines,
wrapLine(lines[i],
width-2, // Subtract 2 for width of prefix string
fmt.Sprintf("[%s::i]> ", config.GetColorString("quote_text")),
"[-::-]", true)...,
)
}
} else if strings.TrimSpace(lines[i]) == "" {
// Just add empty line without processing
wrappedLines = append(wrappedLines, "")
} else {
// Regular line, just wrap it
wrappedLines = append(wrappedLines, wrapLine(lines[i], width,
fmt.Sprintf("[%s]", config.GetColorString("regular_text")),
"[-]", true)...)
}
}
return strings.Join(wrappedLines, "\r\n"), links
}
// RenderGemini converts text/gemini into a cview displayable format.
// It also returns a slice of link URLs.
//
// width is the number of columns to wrap to.
// leftMargin is the number of blank spaces to prepend to each line.
//
// proxied is whether the request is through the gemini:// scheme.
// If it's not a gemini:// page, set this to true.
func RenderGemini(s string, width int, proxied bool) (string, []string) {
s = cview.Escape(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
// Language, formatter, and style for syntax highlighting
lang := ""
formatterName := TermColor
styleName := viper.GetString("a-general.highlight_style")
// processPre is for rendering preformatted blocks
processPre := func() {
syntaxHighlighted := false
// Perform syntax highlighting if language is set
if lang != "" {
style := styles.Get(styleName)
if style == nil {
style = styles.Fallback
}
formatter := formatters.Get(formatterName)
if formatter == nil {
formatter = formatters.Fallback
}
lexer := lexers.Get(lang)
if lexer == nil {
lexer = lexers.Fallback
}
// Tokenize and format the text after stripping ANSI codes, replacing buffer if there are no errors
iterator, err := lexer.Tokenise(nil, ansiRegex.ReplaceAllString(buf, ""))
if err == nil {
formattedBuffer := new(bytes.Buffer)
if formatter.Format(formattedBuffer, style, iterator) == nil {
// Strip extra newline added by Chroma and replace buffer
buf = string(trailingNewline.ReplaceAll(formattedBuffer.Bytes(), []byte{}))
}
syntaxHighlighted = true
}
}
// Support ANSI color codes in preformatted blocks - see #59
// This will also execute if code highlighting was successful for this block
if viper.GetBool("a-general.color") && (viper.GetBool("a-general.ansi") || syntaxHighlighted) {
buf = cview.TranslateANSI(buf)
// The TranslateANSI function will reset the colors when it encounters
// an ANSI reset code, injecting a full reset tag: [-:-:-]
// This uses the default foreground and background colors of the
// application, but in this case we want it to use the preformatted text
// color as the foreground, as we're still in a preformat block.
buf = strings.ReplaceAll(
buf, "[-:-:-]",
fmt.Sprintf("[%s:-:-]", config.GetColorString("preformatted_text")),
)
} else {
buf = ansiRegex.ReplaceAllString(buf, "")
}
// The final newline is removed (and re-added) to prevent background glitches
// where the terminal background color slips through. This only happens on
// preformatted blocks with ANSI characters.
//
// Lines are modified below to always end with \r\n
buf = strings.TrimSuffix(buf, "\r\n")
if viper.GetBool("a-general.color") {
rendered += fmt.Sprintf("[%s]", config.GetColorString("preformatted_text")) +
buf + fmt.Sprintf("[%s:%s:-]\r\n", config.GetColorString("regular_text"), config.GetColorString("bg"))
} else {
rendered += buf + "\r\n"
}
}
// processRegular processes non-preformatted sections
processRegular := func() {
// ANSI not allowed in regular text - see #59
buf = ansiRegex.ReplaceAllString(buf, "")
ren, lks := convertRegularGemini(buf, len(links), width, proxied)
links = append(links, lks...)
rendered += ren
}
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
processPre()
// Clear the language
lang = ""
} else {
// Not preformatted, regular text
processRegular()
if viper.GetBool("a-general.highlight_code") {
// Check for alt text indicating a language that Chroma can highlight
alt := strings.TrimSpace(strings.TrimPrefix(lines[i], "```"))
if matches := langRegex.FindStringSubmatch(alt); matches != nil {
if lexers.Get(matches[0]) != nil {
lang = matches[0]
}
}
}
}
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
processPre()
} else {
// Not preformatted, regular text
processRegular()
}
return rendered, links
}
amfora-1.10.0/.golangci.yml 0000644 0001750 0001750 00000001241 14575704331 014757 0 ustar nilesh nilesh linters:
fast: false
disable-all: true
enable:
- deadcode
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- structcheck
- typecheck
- unused
- varcheck
- dupl
- exhaustive
- exportloopref
- gocritic
- goerr113
- gofmt
- goimports
- revive
- goprintffuncname
- lll
- misspell
- nolintlint
- prealloc
- exportloopref
- unconvert
- unparam
issues:
exclude-use-default: true
max-issues-per-linter: 0
linters-settings:
gocritic:
disabled-checks:
- ifElseChain
goconst:
# minimal length of string constant, 3 by default
min-len: 5
amfora-1.10.0/bookmarks/ 0000755 0001750 0001750 00000000000 14575704331 014365 5 ustar nilesh nilesh amfora-1.10.0/bookmarks/xbel.go 0000644 0001750 0001750 00000002454 14575704331 015653 0 ustar nilesh nilesh package bookmarks
// Structs and code for the XBEL XML bookmark format.
// https://github.com/makeworld-the-better-one/amfora/issues/68
// http://xbel.sourceforge.net/
import (
"encoding/xml"
)
var xbelHeader = []byte(xml.Header + `
`)
const xbelVersion = "1.1"
type xbelBookmark struct {
XMLName xml.Name `xml:"bookmark"`
URL string `xml:"href,attr"`
Name string `xml:"title"`
}
// xbelFolder is unused as folders aren't supported by the UI yet.
// Follow #56 for details.
// https://github.com/makeworld-the-better-one/amfora/issues/56
//
//nolint:unused
type xbelFolder struct {
XMLName xml.Name `xml:"folder"`
Version string `xml:"version,attr"`
Folded string `xml:"folded,attr"` // Idk if this will be used or not
Name string `xml:"title"`
Bookmarks []*xbelBookmark `xml:"bookmark"`
Folders []*xbelFolder `xml:"folder"`
}
type xbel struct {
XMLName xml.Name `xml:"xbel"`
Version string `xml:"version,attr"`
Bookmarks []*xbelBookmark `xml:"bookmark"`
// Folders []*xbelFolder // Use later for #56
}
// Instance of xbel - loaded from bookmarks file
var data xbel
amfora-1.10.0/bookmarks/bookmarks.go 0000644 0001750 0001750 00000010613 14575704331 016705 0 ustar nilesh nilesh package bookmarks
import (
"encoding/base32"
"encoding/xml"
"fmt"
"io/ioutil"
"os"
"sort"
"strings"
"github.com/makeworld-the-better-one/amfora/config"
)
func Init() error {
f, err := os.Open(config.BkmkPath)
if err == nil {
// File exists and could be opened
fi, err := f.Stat()
if err == nil && fi.Size() > 0 {
// File is not empty
xbelBytes, err := ioutil.ReadAll(f)
f.Close()
if err != nil {
return fmt.Errorf("read bookmarks.xml error: %w", err)
}
err = xml.Unmarshal(xbelBytes, &data)
if err != nil {
return fmt.Errorf("bookmarks.xml is corrupted: %w", err)
}
}
f.Close()
} else if !os.IsNotExist(err) {
// There's an error opening the file, but it's not bc is doesn't exist
return fmt.Errorf("open bookmarks.xml error: %w", err)
}
if data.Bookmarks == nil {
data.Bookmarks = make([]*xbelBookmark, 0)
data.Version = xbelVersion
}
if config.BkmkStore != nil {
// There's still bookmarks stored in the old format
// Add them and delete the file
names, urls := oldBookmarks()
for i := range names {
data.Bookmarks = append(data.Bookmarks, &xbelBookmark{
URL: urls[i],
Name: names[i],
})
}
err := writeXbel()
if err != nil {
return fmt.Errorf("error saving old bookmarks into new format: %w", err)
}
err = os.Remove(config.OldBkmkPath)
if err != nil {
return fmt.Errorf(
"couldn't delete old bookmarks file (%s), you must delete it yourself to prevent duplicate bookmarks: %w",
config.OldBkmkPath,
err,
)
}
config.BkmkStore = nil
}
return nil
}
// oldBookmarks returns a slice of names and a slice of URLs of the
// bookmarks in config.BkmkStore.
func oldBookmarks() ([]string, []string) {
bkmksMap, ok := config.BkmkStore.AllSettings()["bookmarks"].(map[string]interface{})
if !ok {
// No bookmarks stored yet, return empty map
return []string{}, []string{}
}
names := make([]string, 0, len(bkmksMap))
urls := make([]string, 0, len(bkmksMap))
for b32Url, name := range bkmksMap {
if n, ok := name.(string); n == "" || !ok {
// name is not a string, or it's empty - ignore
// Likely means it is a removed bookmark
continue
}
url, err := base32.StdEncoding.DecodeString(strings.ToUpper(b32Url))
if err != nil {
// This would only happen if a user messed around with the bookmarks file
continue
}
names = append(names, name.(string))
urls = append(urls, string(url))
}
return names, urls
}
func writeXbel() error {
xbelBytes, err := xml.MarshalIndent(&data, "", " ")
if err != nil {
return err
}
xbelBytes = append(xbelHeader, xbelBytes...)
err = ioutil.WriteFile(config.BkmkPath, xbelBytes, 0666)
if err != nil {
return err
}
return nil
}
// Change the name of the bookmark at the provided URL.
func Change(url, name string) {
for _, bkmk := range data.Bookmarks {
if bkmk.URL == url {
bkmk.Name = name
writeXbel() //nolint:errcheck
return
}
}
}
// Add will add a new bookmark.
func Add(url, name string) {
data.Bookmarks = append(data.Bookmarks, &xbelBookmark{
URL: url,
Name: name,
})
writeXbel() //nolint:errcheck
}
// Get returns the NAME of the bookmark, given the URL.
// It also returns a bool indicating whether it exists.
func Get(url string) (string, bool) {
for _, bkmk := range data.Bookmarks {
if bkmk.URL == url {
return bkmk.Name, true
}
}
return "", false
}
func Remove(url string) {
for i, bkmk := range data.Bookmarks {
if bkmk.URL == url {
data.Bookmarks[i] = data.Bookmarks[len(data.Bookmarks)-1]
data.Bookmarks = data.Bookmarks[:len(data.Bookmarks)-1]
writeXbel() //nolint:errcheck
return
}
}
}
// bkmkNameSlice is used for sorting bookmarks alphabetically.
// It implements sort.Interface.
type bkmkNameSlice struct {
names []string
urls []string
}
func (b *bkmkNameSlice) Len() int {
return len(b.names)
}
func (b *bkmkNameSlice) Less(i, j int) bool {
return b.names[i] < b.names[j]
}
func (b *bkmkNameSlice) Swap(i, j int) {
b.names[i], b.names[j] = b.names[j], b.names[i]
b.urls[i], b.urls[j] = b.urls[j], b.urls[i]
}
// All returns all the bookmarks, as two arrays, one for names and one for URLs.
// They are sorted alphabetically.
func All() ([]string, []string) {
b := bkmkNameSlice{
make([]string, len(data.Bookmarks)),
make([]string, len(data.Bookmarks)),
}
for i, bkmk := range data.Bookmarks {
b.names[i] = bkmk.Name
b.urls[i] = bkmk.URL
}
sort.Sort(&b)
return b.names, b.urls
}
amfora-1.10.0/logo.png 0000644 0001750 0001750 00000050176 14575704331 014054 0 ustar nilesh nilesh PNG
IHDR } ~ iCCPICC profile (}=H@_S"+HP,8jP!
:\!4iHR\ׂUg]\AIEJ_Rhq?{ܽjiV鶙tfE2YIJwQܟ[Z30muMObY%>'5ď\W<~wY!3#&VLx8j:iU[b_K\98 ""lDiIXKRȵFyAv[+71%c@|]Vqcǩ gJoKU`JC=uCS`ɐMٕ4\x?o }@ת[}@J HZdrI# bKGD C pHYs tIME!:I5 IDATx{u rcApPAE-R4Ӻ)<$zhrsM+"C& ANa~^?Zﹿ3s}u=%I$ E ` / Xx ^ , ` / Xx ^ , ` / Xx ^ , ` / Xx , ` / Xx , ` / Xx , ` / Xx , ` / Xx , ` ZRG[lM6Euuu9SRRՋ8 6m%%%^3VٳgO<?O
3x83O;̿ A+I$1riݺuCW\aq_:u2@=G1c
cWS[" O?
w]wE]u5@=vo߾A0@=f~|r._ٳA A{}5\y^jժUѶm[h=1` t݃</Y5{lCM6 CK=@Q>0@=^Llٲ%7nlF]
tO=1wxɚM6~zC t/i}vC:Z KyҢ C ^ ` / Xx ^ `ᅌ)))1qa
^RAu |ӤIC Zha{`%M7nsAPڴic{`%mN;4C};߉M{XxI?(jgq! 4j۶mL2 (JÇ~{䉒$Ic ֮]
EeyѫW/ t݃</YתUx
C>{Xx!_~1c(vmމ=t4SsQFŢE_"Fu5@=3SYfō7h. x8묳D=t;˗ǬY3_T94hPt53C@=M6ۣ_qUWSOo/%%Q^hҤI4kLC3QiiiYxԩS{?[@F,Z(6ol{`K$?{Xx!bɒ%Alذ CٷqFC }C t/d
4@=B]Ȩw} CٷrJC |MC t/d;cdԌ3 CK/b
oE$I([n ȸEEΝ
=t
\=#H;wƺubӦMĦMbƍu֨۷Ν;nݺQ~hذapqDfv7ousNdKeeeޅΝqذaClܸo˛7oqMMM~ѠA_~4j(6m͚5&MDӦME~${´m۶X|y۱x_=XTUU?,DSNѱch]V.$6lYbENǎbŊXlY,^8͛կj{4F={Ν;GN]vѨQ#xtO=KٵkW,[,ϟ|qI'E=cǎ9{Qo~1k֬OfʛO~ѷoСCKԩScܸq>dܩo28˖-^z)?#M7QQQ'pBtſ{+^:/6,?FJfΜlܸqرc9;q6oޜ> y'ѣG<ɪU\=stO(8kdu]WѲdԩҥKjIii 9Y;{I}KڵkW3kW_}5ٵk3{^=/rDlwϤI_}fj*1rz̙S+Nj-JڢMEEE/&;wtFt=˞W^I;B^yŋwk&9Y=/NꢞѨQ^zO=stOK.$U[n%y>u.ӧO#'wdڵkn-U?~nK<9{XxShwߝڋlYYY2mڴd۶m;nI}&555=}dJzdݺu.9{Xxjjj?OI]l##G&,Ȝj>Nڵkw_O>l3Hz<^鞣{7֮][밷oܸL-[x>w?%螣{"5gΜG.rN?䭷J^uprrO~:˜>t-={>{غukr=()--M."prrL/~c:uje=G=,lɨQ\@ΠA
<6oޜL2lTTT$VsGto1Z`Aҷo_ )'I$k֬I***dN?=qtOM%I$AFC/Ѱa7s5Z6N9=@tcSSS