amfora-1.10.0/0000755000175000017500000000000014575704331012375 5ustar nileshnileshamfora-1.10.0/display/0000755000175000017500000000000014575704331014042 5ustar nileshnileshamfora-1.10.0/display/handlers.go0000644000175000017500000003464414575704331016204 0ustar nileshnileshpackage 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.sh0000755000175000017500000000032214575704331015666 0ustar nileshnilesh#!/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.go0000644000175000017500000000221014575704331015476 0ustar nileshnileshpackage 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.go0000644000175000017500000001071614575704331015326 0ustar nileshnileshpackage 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.go0000644000175000017500000002414314575704331015654 0ustar nileshnileshpackage 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.go0000644000175000017500000000047314575704331015657 0ustar nileshnileshpackage 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.go0000644000175000017500000000177014575704331015666 0ustar nileshnilesh//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.go0000644000175000017500000001027614575704331016051 0ustar nileshnileshpackage 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.sh0000755000175000017500000000031514575704331016022 0ustar nileshnilesh#!/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.go0000644000175000017500000000220014575704331015643 0ustar nileshnileshpackage 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.go0000644000175000017500000000173014575704331016073 0ustar nileshnileshpackage 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.go0000644000175000017500000000541714575704331015355 0ustar nileshnileshpackage 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.go0000644000175000017500000002543714575704331016213 0ustar nileshnileshpackage 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.go0000644000175000017500000000606314575704331015315 0ustar nileshnileshpackage 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.go0000644000175000017500000003750314575704331016046 0ustar nileshnileshpackage 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.go0000644000175000017500000001313314575704331016362 0ustar nileshnileshpackage 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.go0000644000175000017500000010462214575704331016020 0ustar nileshnileshpackage 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.go0000644000175000017500000002351614575704331017307 0ustar nileshnileshpackage 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.go0000644000175000017500000003615314575704331015147 0ustar nileshnileshpackage 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/0000755000175000017500000000000014575704331013440 5ustar nileshnileshamfora-1.10.0/cache/redir.go0000644000175000017500000000240714575704331015077 0ustar nileshnileshpackage 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.go0000644000175000017500000000301214575704331015736 0ustar nileshnileshpackage 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.go0000644000175000017500000000636214575704331014712 0ustar nileshnilesh// 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.go0000644000175000017500000000100614575704331016130 0ustar nileshnileshpackage 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/.gitignore0000644000175000017500000000726514575704331014377 0ustar nileshnilesh# 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.sum0000644000175000017500000015744214575704331013545 0ustar nileshnileshcloud.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/0000755000175000017500000000000014575704331014035 5ustar nileshnileshamfora-1.10.0/contrib/themes/0000755000175000017500000000000014575704331015322 5ustar nileshnileshamfora-1.10.0/contrib/themes/dracula-variant.toml0000644000175000017500000000213714575704331021277 0ustar nileshnilesh#[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.toml0000644000175000017500000000321114575704331022246 0ustar nileshnilesh#[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.toml0000644000175000017500000000617714575704331020014 0ustar nileshnilesh# 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.toml0000644000175000017500000000320314575704331021142 0ustar nileshnilesh#[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.toml0000644000175000017500000000557214575704331021225 0ustar nileshnilesh#[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.toml0000644000175000017500000000440014575704331017517 0ustar nileshnilesh#[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.toml0000644000175000017500000000550414575704331017636 0ustar nileshnilesh#[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.toml0000644000175000017500000000243114575704331021066 0ustar nileshnilesh## 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.toml0000644000175000017500000000242414575704331020122 0ustar nileshnilesh## 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.toml0000644000175000017500000000602514575704331017164 0ustar nileshnilesh#[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.toml0000644000175000017500000000243114575704331021047 0ustar nileshnilesh## 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.toml0000644000175000017500000000533214575704331017716 0ustar nileshnilesh#[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.toml0000644000175000017500000000211114575704331021275 0ustar nileshnilesh#[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.toml0000644000175000017500000000220114575704331017457 0ustar nileshnilesh#[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.toml0000644000175000017500000000217614575704331020501 0ustar nileshnilesh#[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.toml0000644000175000017500000000602514575704331017622 0ustar nileshnilesh#[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.toml0000644000175000017500000000557214575704331021413 0ustar nileshnilesh#[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.md0000644000175000017500000001673714575704331016617 0ustar nileshnilesh# 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. Demo GIF ## Nord Contributed by **[@lokesh-krishna](https://github.com/lokesh-krishna)**. ![screenshot of the nord theme](https://user-images.githubusercontent.com/20235646/102846450-005dc480-4436-11eb-89a9-a1a4350f5415.png) ## Dracula Contributed by **[@crdpa](https://github.com/crdpa)**. ![screenshot of dracula theme](https://user-images.githubusercontent.com/61637474/99983229-5b928d80-2d8a-11eb-8e5c-e5681bb274c5.png)
More screenshots ![screenshot of dracula theme](https://user-images.githubusercontent.com/61637474/99983237-5e8d7e00-2d8a-11eb-8e22-3a3459ae560a.png) ![screenshot of dracula theme](https://user-images.githubusercontent.com/61637474/99983210-53d2e900-2d8a-11eb-9ab7-12dc10c2933a.png)
## Dracula variant Contributed by **[@marcransome](https://github.com/marcransome)**. ![screenshot of dracula variant theme](https://user-images.githubusercontent.com/679401/132952433-563501ef-4d98-4d43-988e-f15bab7cb155.png)
More screenshots ![screenshot of dracula variant theme](https://user-images.githubusercontent.com/679401/132952340-96840ad8-fb78-499d-bf6b-3fcdf659edc7.png) ![screenshot of dracula variant theme](https://user-images.githubusercontent.com/679401/132952347-6b93d985-afc8-47b4-9569-1775ce4f37e7.png) ![screenshot of dracula variant theme](https://user-images.githubusercontent.com/679401/132952348-ffcbcc7a-f9ad-41c6-a7d2-5c870754c4c9.png) ![screenshot of dracula variant theme](https://user-images.githubusercontent.com/679401/132952352-50ca16f3-d255-4a1d-a25b-ccf53116957d.png)
## Greyscale Light Contributed by **[@leifmetcalf](https://github.com/leifmetcalf)**. ![screenshot of greyscale light theme](https://user-images.githubusercontent.com/35786377/109232516-53d10200-782d-11eb-9863-4bb745d27195.png)
More screenshots ![screenshot of greyscale light theme](https://user-images.githubusercontent.com/35786377/109232522-55022f00-782d-11eb-93a2-4d50c61317c5.png)
## Gruvbox Contributed by **[@Skraylet](https://github.com/Skraylet)**. ![screenshot of gruvbox theme](https://user-images.githubusercontent.com/26380693/100381730-4768bd80-3022-11eb-83ae-bcd0495f2ae9.png)
Another screenshot ![screenshot of gruvbox theme](https://user-images.githubusercontent.com/26380693/100381734-4a63ae00-3022-11eb-9531-a635df310052.png)
## Solarized Contributed by **[@bnthor](https://github.com/bnthor)**. ### Dark ![screenshot of solarized dark theme](https://user-images.githubusercontent.com/798657/100597218-77071680-32fd-11eb-8e0d-593ff95b7129.png)
Another screenshot ![screenshot of solarized dark theme](https://user-images.githubusercontent.com/798657/100597236-7b333400-32fd-11eb-8844-b92601da52c7.png)
### Light ![screenshot of solarized light theme](https://user-images.githubusercontent.com/798657/100597327-9aca5c80-32fd-11eb-8c91-fe3e324d8959.png)
Another screenshot ![screenshot of solarized light theme](https://user-images.githubusercontent.com/798657/100597349-a453c480-32fd-11eb-866e-10b0587228f6.png)
## One Dark Contributed by **[@sergetymo](https://github.com/sergetymo)**. ![screenshot of one dark theme](https://user-images.githubusercontent.com/65758149/101183151-c8920700-3657-11eb-87f5-7d1d6ae616f2.png)
More screenshots ![screenshot of bookmark modal](https://user-images.githubusercontent.com/65758149/101183267-f8410f00-3657-11eb-97fa-10f88a9d8de4.png) ![screenshot of error modal](https://user-images.githubusercontent.com/65758149/101183206-da73aa00-3657-11eb-8733-5040c8aefb99.png)
## Ayu Light Contributed by **[@sergetymo](https://github.com/sergetymo)**. ![screenshot of Ayu Light theme](https://user-images.githubusercontent.com/65758149/181745417-48a92840-10d2-4659-950d-fbc9b3588d5c.png)
More screenshots ![screenshot of bookmark modal](https://user-images.githubusercontent.com/65758149/181745413-b5a15120-2ff6-4879-8539-0f02f0eece21.png) ![screenshot of error modal](https://user-images.githubusercontent.com/65758149/181745400-c3e9ba95-aee4-4956-91a8-3dddcbad48cc.png)
## Atelier Forest Contributed by **[@joyalicegu](https://github.com/joyalicegu)**. ### Dark ![screenshot of atelier forest dark theme](https://user-images.githubusercontent.com/16532904/114287117-35a81580-9a19-11eb-8515-a4fa5fee00b8.png) ### Light ![screenshot of atelier forest light theme](https://user-images.githubusercontent.com/16532904/114287105-22954580-9a19-11eb-8c09-2bce083286b2.png) ## Slimey Contributed by **[@lee2sman](https://github.com/lee2sman)**. ![screenshot of Slimey theme](https://user-images.githubusercontent.com/7377908/114319350-212e5080-9adf-11eb-9d41-d4e800c6570f.png) ## Gruvbox Dark Contributed by **[@thumb](https://github.com/thumbfighter)**. ![screenshot of Gruvbox Dark theme](https://user-images.githubusercontent.com/19327775/114431954-b97f1080-9b85-11eb-9da5-e1ebf06beba9.png)
More screenshots ![screenshot of makeworld.space home page](https://user-images.githubusercontent.com/19327775/114432099-f0552680-9b85-11eb-9b4f-629d62d971b4.png) ![screenshot of error](https://user-images.githubusercontent.com/19327775/114432066-e4696480-9b85-11eb-8ed0-c454f37fe9cf.png) ![screenshot of add bookmark](https://user-images.githubusercontent.com/19327775/114432084-e8958200-9b85-11eb-9813-9982c3c0effa.png)
## Iceberg Dark Contributed by **[@knix3](https://github.com/knix3)** ![screenshot of Iceberg Dark theme](https://user-images.githubusercontent.com/69134168/118542790-7b938000-b721-11eb-81e1-ae45cdfbe546.png)
More screenshots ![screenshot of add bookmark](https://user-images.githubusercontent.com/69134168/118543243-07a5a780-b722-11eb-848a-95aa1de30044.png) ![screenshot of error](https://user-images.githubusercontent.com/69134168/118543250-096f6b00-b722-11eb-9dca-d2b1bd6a8885.png)
## Tokyo Night Contributed by **[@luetage](https://github.com/luetage)** ![screenshot of Tokyo Night theme](https://user-images.githubusercontent.com/13988217/130348393-69986b51-ddd7-4310-90ae-382461502535.png) ## Rosé Pine Contributed by **[@mvllow](https://github.com/mvllow)**. ### Rosé Pine screenshot of Rosé Pine theme ### Rosé Pine Moon screenshot of Rosé Pine Moon theme ### Rosé Pine Dawn screenshot of Rosé Pine Dawn theme ## Yours? Contribute your own theme by opening a PR. amfora-1.10.0/contrib/themes/ayu_light.toml0000644000175000017500000000224114575704331020203 0ustar nileshnilesh# 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.toml0000644000175000017500000000306114575704331020714 0ustar nileshnilesh#[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/0000755000175000017500000000000014575704331016246 5ustar nileshnileshamfora-1.10.0/contrib/gemini-wiki/requirements.txt0000644000175000017500000000001314575704331021524 0ustar nileshnileshmd2gemini<2amfora-1.10.0/contrib/gemini-wiki/main.py0000644000175000017500000000437314575704331017553 0ustar nileshnilesh#!/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.md0000644000175000017500000000061514575704331017527 0ustar nileshnilesh# 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/0000755000175000017500000000000014575704331014075 5ustar nileshnileshamfora-1.10.0/sysopen/open_browser_unix.go0000644000175000017500000000222514575704331020174 0ustar nileshnilesh//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.go0000644000175000017500000000060714575704331020334 0ustar nileshnilesh//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.go0000644000175000017500000000055614575704331020502 0ustar nileshnilesh//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.go0000644000175000017500000000100114575704331020672 0ustar nileshnilesh//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/LICENSE0000644000175000017500000010451514575704331013410 0ustar nileshnilesh 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.go0000644000175000017500000000553414575704331014200 0ustar nileshnileshpackage 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.md0000644000175000017500000000136114575704331013610 0ustar nileshnilesh# 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/0000755000175000017500000000000014575704331013642 5ustar nileshnileshamfora-1.10.0/config/keybindings.go0000644000175000017500000001640014575704331016500 0ustar nileshnileshpackage 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.go0000644000175000017500000003354014575704331015622 0ustar nileshnileshpackage 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.sh0000755000175000017500000000031614575704331015625 0ustar nileshnilesh#!/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.go0000644000175000017500000003475714575704331015456 0ustar nileshnilesh// 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.go0000644000175000017500000003160114575704331015274 0ustar nileshnileshpackage 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.yml0000644000175000017500000000153414575704331015331 0ustar nileshnileshproject_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/0000755000175000017500000000000014575704331013654 5ustar nileshnileshamfora-1.10.0/logger/logger.go0000644000175000017500000000136514575704331015467 0ustar nileshnileshpackage 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.mod0000644000175000017500000000453514575704331013512 0ustar nileshnileshmodule 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.desktop0000644000175000017500000000035514575704331015240 0ustar nileshnilesh[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/Makefile0000644000175000017500000000200014575704331014025 0ustar nileshnileshGITV != 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.toml0000644000175000017500000003343014575704331016164 0ustar nileshnilesh# 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.md0000644000175000017500000003770314575704331014220 0ustar nileshnilesh# 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/0000755000175000017500000000000014575704331014203 5ustar nileshnileshamfora-1.10.0/renderer/page.go0000644000175000017500000001066014575704331015451 0ustar nileshnileshpackage 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.go0000644000175000017500000003551714575704331016353 0ustar nileshnilesh// 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.yml0000644000175000017500000000124114575704331014757 0ustar nileshnileshlinters: 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/0000755000175000017500000000000014575704331014365 5ustar nileshnileshamfora-1.10.0/bookmarks/xbel.go0000644000175000017500000000245414575704331015653 0ustar nileshnileshpackage 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.go0000644000175000017500000001061314575704331016705 0ustar nileshnileshpackage 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.png0000644000175000017500000005017614575704331014054 0ustar nileshnileshPNG  IHDR}~iCCPICC profile(}=H@_S"+HP,8jP! :\!4iHR\ׂUg]\AIEJ_Rhq?{ܽjiV鶙tfE 2YIJwQܟ[Z 30muMObY%>'5ď\W<~wY!3#&VLx8j: iU[b_K\98 ""lDiIX KRȵFyAv[+71%c@| ]VqcǩgJoKU`JC=uCS`ɐMٕ4\x?o}@ת[}@JHZdrI#bKGDC pHYs  tIME!:I5 IDATx{urcApPAE-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._ٳAA{}5\y^jժUѶm[h=1`t݃</Y5{lCM6CK=@Q>0@=^Llٲ%7nl F׮] tO=1wxɚM6~zCt /i}vC:ZKyҢC ^` /Xx ^`ᅌ)))1qa ^RAu|ӤIC Zha{`%M7nsAPڴic{`%mN;4C};߉M{XxI?(jgq!4j۶mL2 (JÇ~{䉒$Ic ֮] Ee޼yѫW/t݃</YתUx C>{Xx!_~1c (vmމ=t4Ss΍QFŢE _"Fu5 @=3SYfō7h . x8묳D=t;˗ǬY3_T94hPt53C@=M6ۣ_qUWSOo/%%Q^hҤI4kLC3QiiiYxԩS{?[@F,Z(6ol{`K$?{Xx!bɒ%AlذC ٷqFC }Ct /d 4@=B]Ȩw}C ٷrJC |MCt /d;cdԌ3 C K/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?,D޽SNѱ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رc 9;q6oޜ> y'ѣG<ɪU\=stO(8k׮du]WѲdԩҥKjIii 9Y;{I}KڵkW3kW_}5ٵk 3{^=/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$VsGt o1Z`Aҷo_)'I$k֬I***dN޽?=qtOM%I$AFC/Ѱa7s5Z6N9=@tcSSSڹsg?ػ׭[7 u5=j7555q= `q1~S=›~׿u?Os5=,OEbذa{7W.\ݻw7 ?D= Bt{ދ1c@|߈իW{޽uָ '?IA|eeeqF6mbÆ |={O$C Ė-[b1ŝ>7=t=??o?t>|x3=h֬Y4l0JJJm۶Xvm\2͛?!CW8#-[F?򿫮ccŌ3?^z_{ȳ>DDޝ[o5yד]vڹsgk%rKRZZq?eeewߝ{} صkWhѢK9 =qt=f˗/Oڵk7Odݺu<+++{q&=P_֯_<#y/eee;#D{{7oߞ3&o dŊ}+VH.rXsW'W5295*ٶm 9裏~nݒ9s$555Yy;wL|ɼ t-yv H$3gNңGx?ItqtO֦z+o~ʳjժwIN?t^I5Ko-Z$L{{^u9śslٲ%J&N898'ON6oޜk֭[o983KtqtOֆӧ{Mvܙ7"4ebя~ر#/;wLdڴi{8{XxŚ5kr;<^zر#;\'Ko׀䡇Wen{Yx ^MMM2i$p .̎0or]v%w_NsWfMLtOtqtO,EW^5\7/$۶mK.2h8qbR]]׀;v$'O^~e=s=,{:9s\QQ|1 6$Æ svZd9~ rn3gtvZ8=\A^Ν?0==#'ٳg1Ss;cҤI`|q 'ޯ_ss΍3g==>UUU C )]$ׯOz᧕gƍ NСCs2nݺ5TtqtOtK3WUξ͛W3"8{qfϞ]׀\c=&d{{~ʝ^s5E3ǝ;w&Ǐw!w=8&L(wi8{y$I #~i^bEm۶hfoGN|QnzwCE|V^vXTUUe>|/*==#""iUDTWWǝwޙ~*zh|;`7L:qA]wݕǾ;F / v-^xhڴi4?BQVV ,VZs۸qc}ѱdɒ?Y{{Û$I<9y쫯(~uM7Oq7e#"5k_}N?q==]xqt%[VVzhѢhgrh׮2^۰aC~QYY~E{{);r_}QG?"m۶w|;ѼyRum===/[UUݺu+VdW\mڴ)~ߎ+VgqbѢEEY==ӽݓ;/bN5פ"]tQFN墋.JE#"ڵk'NVVVܹs}鞅7}ќ<?}kz#FŦ{{)ڗ4%z/B4h 5޼ys!ߔǫjsuuu/g-[|/<==K}sƍKU#"7n/ ".KU#"ׯ^xaN˻tO@t›:555#O8T~  JKKSm;w߁]rej{(uwx,Xǽ S[|z^ziѰaø rOuO@t›"/BN7W/iȵ{.f̘;KTc9&U~=== oTWWߟN5nٲ%&O "R.s9ym۶n޺{{gMK5eeeѶm}q͘1#fϞ "6h߾}k.[YY?Ӡ{{YpaN"4h/u%\; ۻk~W՜]j,Quuuo>*++دJ;5_ToFtw~:N:ᥗ^BTZZk׮M͝G==KS>IjZ*'я˔;wL0!6mڔ9oFDTUUʕ+S3g==KSR[l/3gƃ> _C.4y-z#FDFR1{/︺ç?~|,X 3hРA~9y4y-z\l۵kWvmdWv W^yelٲ%3իWNO͌uO@t/MK»m۶xGrBzꩧ'x"~򓟤zrHNKſt^cs+X"=\Ws]tQ̛7/ϿUVl鞅֭^:gݤImꫯ;B!֭Kso޼y{ժU{{|rϐx ~aWp s΍cǎ=f͚}]== oXlYh?1ƍ oZRƍs鞅xsŝIof1Ujy=\Ǟ?^J$IܵkWi&gkcǎW^Qt͚51bĈ (F/F׮]S|A9{b6^YƍsaÆ7nC-s9'{M_ίe{{ޢѣ.L&LiӦJC̛7/ƌk֬)ZRR}MetO= or?Ø0aBΐAO>dymڴIetO= oٴiS;o6mܸ1.첸{]! ~ƨQbʕEg] a͚5o~O!fΜgqF,^X {鞅(>/-\0F/]!Ν|͛7c(=ӽw›廱}?ѫWXh%C?\Aˇk;Yxmܸ1of#СC]}߈[o5oߞg>\=@t[]rǎ; WEH~}/>ü;U^N?\M­[ 7_|!m_|qlذ!/?\|j{gk oV?*:WAH|0ƌy¯{Yx 3D.M/ O{no555r{e\I*e kׯ_͛7mܸ1.O~DDg?Y IDATo>ZDZk׮i^Y={ャ=ֆ g֬YqgƲerY&CyyyF_t/MKۡC n֢?nܸ*|ܹs㬳y_>v{Yx _YYY?7x#㏱iӦKE_+VcXpaкuk=@t[;D䩧?*.r/vٳ"V^N$~|x{gOĺu2޺uk\uUgc￟]vm<9tO= okٲe^|K.fuuu\q]w{췿m?>6mڔ\dI^<-Z{—/W_}Vyv;#n&W/`=1qزeKVoڠ{YxkKf>}z֟p@UVV7Z;˛7h =KT,y;%IsOw}F@<1iҤؾ}{?#o[.]tOt/-pʫM7RqrjҥyfΜ{8{^\<ڶmWs=[ iGn@Ό7.~}g<3{&F 6OYzlٲS?u%6q<,YW?0ҥK^=k9;ţYfyϏs~sΘ2eJ̚5ˏـ/<>=}bѢEy\-[=н'z'3uعswӦM[nӧOSF$sΘ:uj^=SN9%JJJtOt/-r+{yr^z#k$yꩧv;gΜM9螗4Ν;4uصk> .2?RѣcʕSGyw)5+н4ueߴiS^hΜ9I$IMMMrM7I8yƎl߾Sfˏ}8GVTTᗾd۶myM8q'^knݚ 6,>#G.8{^Ҝ"׿u{|r`|k_ŋ>f̘w}t@.U G\I&Ŷm??,7(T>h$Ibԩdɒx{i֬Y;gTTTo?|\wuyquYѲe}~t@,)3duZt7ʕ+cܸqy1?Sj??{iV$I'oD׮]}jQnb…y-\0~н4KN:E߾}}j9/%zhj?7{W^=g %._~j螅7e=Xy8R?= o~Ѯ];}"׮]޽{{gMFW\P.S?= oʜtI>En{gM]}=zxY螅7իcǎP.hРAYxNP d{СC?W@3fL螅7:,_Et@;+H#9=۰aø}%~ѨQ#=#J$I>GǎW@VZFDh"Lbo}=|8 -]4:vh{D;Ӿ} @]{{qywC1[ošj{;KǎN@D_tp&³l2n=>о}я~d;}=>;cݺuq1Ē%K  ł I=>;e˖q-@뮻D_t 6lX : Ԑ!Cb{'OK/E~   qOѷoߘadg?YL[|y 4ȟlȰ3gN{KB;4 {GE_t oY2⤓N2=,ٴk׮~frUWŨQ Bt}wx;rAdѣƍ޽k@zq뭷\5k!dw-[4=j4-[D֭0j[ozAQ+CK.} y7 At o!/!a͕^x2䡇j=ݣV=u8 ~(//7=;{`aK,1=,o@꫆{7,X`6sLC=›m  ߰aAa͖O?mYrJC=›-W6,Yl!a͖w}dņ{7[S%@̙3t@f_CȒ_uV=c$Iç۱cGl2  K}h߾A ֭},{ At o]̻a͂JC2{7  z-C=›i~}g6=,o@˱e=›)5553@|{7S6o+V0شi!a w[t 7^~t@͛7g->^G;֭3=,@_t@ ?@Yv!ao;W6=,;rgņ{7S w,Yv2=,P|_t ol۶_= o񩮮6=t+ԮC=› ;v0Jt@f{`~=.t oTLj=f͚@~{7Zha9T~}C=› M41)--n=,r@t@fJƍ  Gڶmk{Xx3e7iٲ!aV ;[6=,ҪU+Cȑ>t@f䎟ta͛͠@|s3=,ҴiSCwu@fP bС@It o,4ib{Xx3SNeÇuL:C  k{Xx3]ve;w6=,aXtݳƍȑ#  : C=›  0,>|xlҥ!dɉ'h{XxC%~!aD=冠{7[4iG6,pwQt otIaǏ t@fSn  ü;zڵ!aͶ-Z駟nt@СC  CFM65=,pG@x$=,9ԩS'CȐ#<t@JVs1 ҥ!aͥSO=jW\pAaͥ={@-; At oۄ̛#a 41c@-8p`o t@c=jguH޼еkWC%}1=,u1rH,0aB4i t@擣>ѐ!C At o9Cbza{XxMݺu ҅^Z2=,c1tꩧ|չsݻA^z|~Ř1c `}ѦM=›ώ;8CCčpGyyA/=[6lw `7~ѱcG=[ dώ=[v]+vS~ At o_~\x;h۶Aa-$ 0pgBӹs}BS^8 \zѪU+=[=XCÇ7=,cǎqի!a-؁֩sAk65kf{Xx Y r){еn: o{=zb/}۸qA{+  "N8C=[, H>"{ <;s t@.]& g{Xxnuѣ Hk&Zha{Xxѱk@jzꩆ{XxqW:7u@/ˆ B=,ŮgϞ1`RO4=,Ů~1n8R+m48 H|+{޴hӦML0 ׷oիA7MFa@wM;=@QG7 4=@M b.,ڵkg{Xxە30t=,iնm[oG޽ t o9s饗zt=,i׳g:tAEśv{a%ի]tAEn2@#$ICmذ!:tUUUW_}58@#Û#͛7뮻 7rڵA{a;S (x~ԭ[ =@u9;< Vvc5t=,8s (X&MR@Gs1ѣG Ґ!C t /QF1a e];v4t=?K*++4> =@[恲2eAcѳgO@gҗd@袋^zvaرc {]t? =@{JJJb佉'`{Xx}}A׆j{Xx3׏/ 5y8蠃 t 6 D toi޼yL4 SQQ]v5t=,N;sG:}СC8qAycѯ_?@˾׾f@޸ˣA}׽{5jA9׮];:t /S'Ǝk@M<96mj{Xx=}SÆ 3t=,Ԯǥ^j@xqA$IWUUU'-Zd@ֽѥK@ݣ ÛJKK{AY7n8Gݣ[{hݺAY/G>} to8[o5 kFGuA{a%;߄2nܸWA{a%cĉd3t=,d׿uC2+ ^[nq1]tA^$?|2kR@K+Fi@+--/~ܨW^\tEԺo9Zha{$Ic(,۷o/~PkV\mڴ1t=;AԚnI=@(:?0 .4 `-^8:ud{wx &M2`}}t=;lƍqGƊ+ kZt =@(:f͚_o^?~|t =@(J?ˣ0=6o޼իA{Q-po6`=:zi{-wxի:Ν3t=;Eࠃn 6r8 toXlYt ?)h{5wxDs%[ %]{Xx),gu!.^ GNbĉ|o{Xx)<{!hĉѰaC@K9+0# 2t=,ѣGW^ye4j =@Rv\rAӻwu7MI&sQGy#Hh{Xx)Op:qhڴA{ax;ƌc"1l0@KRO= ojO!%tt,Dש]tA@ )7XxS稣c.])7XxSɮS'oɓ')7Xx騣 . )7Xx n(Z~ ٳCSn==~ nSn==ѣG?~A@|?_4==QRRMֆ O>dÀ<`޽Apռyo_EA@`/'?>ϟoC-Ν;!wxDquɓEtt;|[СCc֬Y9;D tt;|FW_m~>pT]]gqF<YRZZ/<0@@`/g_~L0 nv /Я_0ȂWb{{`%ի\rA@xѼys /ҫW7nA@;N9݃ZM# .ݻd?Ç7==GuW^yA@ <8N>d݃Z/{lҥQ^^nP˞~8餓 tt!^bIDATj;C9$n6Ztgo{{PeY&tUUU_c9 @@^iݺuq />}2wxk6l>}Ē%K  ,d;͛7lJ݃ q}y:th̞=0`/,Y$9݃ p}Ҹq4iA^[Ett2^Yuuuy_0`7ƢEuֆ/~1a=pw>Xx)sL; `7t-Fa{{a^LY`Aya/ @@ qqUW|СC tt^j;S8pAAK[vmqQYYi+ѻwo݃,qZתU K.$ze{{EUUU1hР7oa@D,Z(:wl{{EqDĔ)SDttr^2f1rx' TKEuֆY/ӠA8qAjwubԯ_ַeRnbĈ9%d… {A<1|p݃q֭[L< HaÆŐ!C ttr^bŊѾ}{ 50`AAKVk._ H1cD t =1wxɚ 6DcѢEAQ[`t݃</YӼy~`kVC OKVmݺ5F3g4 ˣ]vy^QF} (J?E=t,qcƌ1J.]30@=#^LNkqGEc/ C KNt=Z( :4kgB #C*N>XKp``hQq;9+ 3*"XvȐZ4j ]{pߟn{~n0{$^ĉeeet݃K\zTkhhRCAy%bg R?,Yb@=H /d=!HG}TC ugΜ0QTT`cABy%̙[n5O>{8xw5oR$֯_o@=H84ǏbCxot݃Kb,\0 Amڴ)*** )Dbٲe122b 7/_n@=H/$E]w6˖-3{^gbb"֯_ Q꫸ )ᅗ),,۷DygD=tƖ-[ A",]46nh@=H4XqUW{7{2^xIŋG[[!Ȫ7ڵk )䅗Do55===QRRb@=H!/$_?!Ȋm۶A@=H1/$DTWWǻk fct݃tKƎ; ڿߪU{13$n6CA8vX,ZLΨ4{^R8x C0(//7{/VAc0->Xt!C x%U͛mmm`ZܹSC x%uΜ9k(bt݃ᅗԙ3gN477j߾}^Ⱦ˗?gMEEETWW=trOI?YL{cZ]vY _ijjk{j?cxe ǏǕW^i@=A^xIs?l{>{8x!2L466$jjj 9'cǎŢE _t݃慗P\\O=!Kꢢ{3N>k׮^ccɒ%t݃煗 .4jQTTd@=pB:/cǎ8p1S~1C 699mmmk.c[1C 巴}Ń>h 5kDAAAC +D}}!G9s̉ {K3; 7`={Ė-[s1{8x!>(--5g/6m2{8x!{{?wԩhll.c q뭷А1C ~)z0288uuuqIcׯt݃$;̨ÇGyy!Y=\u]t/IoooX伎[ {8x ⋨c0+>|8zC݃,=ĉQ[[+*k֬O>{{%W>}:6oG5Ά /48xEO?t`V۷1@@ /fxx8 C0}QZZj==a^x6===Fx^r@A@K7D_m==pK.#@Dtt䒫L{{%X"ϟo_A@Kn c׮]`V۹sg,^{{%lذ!>C0+UWWǽk==Ȓ30] bQXXA5hooytt䜩)30ݦ?DkkAY qGYYYtO=p2LLLXLNNw^̝;{? ^p8x ^p8x ^p8x ^p/8x ^p/8x ^mC<IENDB`amfora-1.10.0/README.md0000644000175000017500000001635714575704331013670 0ustar nileshnilesh# Amfora amphora logo
Image modified from: amphora by Alvaro Cabrera from the Noun Project
[![go reportcard](https://goreportcard.com/badge/github.com/makeworld-the-better-one/amfora)](https://goreportcard.com/report/github.com/makeworld-the-better-one/amfora) [![license GPLv3](https://img.shields.io/github/license/makeworld-the-better-one/amfora)](https://www.gnu.org/licenses/gpl-3.0.en.html) Demo GIF ###### Recording of v1.0.0 Amfora aims to be the best looking [Gemini](https://geminiquickst.art/) client with the most features... all in the terminal. It does not support Gopher or other non-Web protocols - check out [Bombadillo](http://bombadillo.colorfield.space/) for that. It also aims to be completely cross platform, with full Windows support. If you're on Windows, I would not recommend using the default terminal software. Use [Windows Terminal](https://www.microsoft.com/en-us/p/windows-terminal/9n0dx20hk701) instead, and make sure it [works with UTF-8](https://akr.am/blog/posts/using-utf-8-in-the-windows-terminal). Note that some of the application colors might not display correctly on Windows, but all functionality will still work. It fully passes Sean Conman's client torture test, as well as the Egsam one. ## Project Status Amfora is in maintenance mode. When possible, I’ll make/merge bug fixes, and maybe slowly merge feature PRs by others. See my [blog post](https://www.makeworld.space/2023/08/bye_gemini.html) for details. ## Installation ### Binary Download a binary from the [releases](https://github.com/makeworld-the-better-one/amfora/releases) page. On Unix-based systems you will have to make the file executable with `chmod +x `. You can rename the file to just `amfora` for easy access, and move it to `/usr/local/bin/`. On Windows, make sure you click "Advanced > Run anyway" after double-clicking, or something like that. Unix systems can install the desktop entry file to get Amfora to appear when they search for applications: ```bash curl -sSL https://raw.githubusercontent.com/makeworld-the-better-one/amfora/master/amfora.desktop -o ~/.local/share/applications/amfora.desktop update-desktop-database ~/.local/share/applications ``` Make sure to click "Watch" in the top right, then "Custom" > "Releases" to get notified about new releases! ### Linux Packaging status Amfora is packaged in many Linux distros. It's also on [Scoop](https://scoop.sh/) for Windows users. ### macOS (Homebrew) If you use [Homebrew](https://brew.sh/), you can install Amfora with: ``` brew install amfora ``` You can update it with: ``` brew upgrade amfora ``` ### macOS (MacPorts) On macOS, Amfora can also be installed through [MacPorts](https://www.macports.org): ``` sudo port install amfora ``` You can update it with: ``` sudo port selfupdate sudo port upgrade amfora ``` **NOTE:** this installation source is community-maintained. More information [here](https://ports.macports.org/port/amfora/). ### Termux If you're using [Termux](https://termux.com/) on Android you can't just run Amfora like normal. After installing Amfora, run `pkg install proot`. Then run `termux-chroot` before running the Amfora binary. You can exit out of the chroot after closing Amfora. See [here](https://stackoverflow.com/q/38959067/7361270) for why this is needed. ### From Source This section is for advanced users who want to install the latest (possibly unstable) version of Amfora.
Click to expand **Requirements:** - Go 1.15 or later - GNU Make Please note the Makefile does not intend to support Windows, and so there may be issues. ```shell git clone https://github.com/makeworld-the-better-one/amfora cd amfora # git checkout v1.2.3 # Optionally pin to a specific version instead of the latest commit make # Might be gmake on macOS sudo make install # If you want to install the binary for all users ``` Because you installed with the Makefile, running `amfora -v` will tell you exactly what commit the binary was built from. Arch Linux users can also install the latest commit of Amfora from the AUR. It has the package name `amfora-git`, and is maintained by @lovetocode999 ``` yay -S amfora-git ``` MacOS users can also use [Homebrew](https://brew.sh/) to install the latest commit of Amfora: ``` brew install --HEAD amfora ``` You can update it with: ``` brew upgrade --fetch-HEAD amfora ```
## Features / Roadmap Features in *italics* are in the master branch, but not in the latest release. - [x] URL browsing with TOFU and error handling - [x] Tabbed browsing - [x] Support ANSI color codes on pages, even for Windows - [x] Styled page content (headings, links) - [x] Basic forward/backward history, for each tab - [x] Input (Status Code 10 & 11) - [x] Multiple charset support (over 55) - [x] Built-in search (uses geminispace.info by default) - [x] Bookmarks - [x] Download pages and arbitrary data - [x] Theming - Check out the [user contributed themes](https://github.com/makeworld-the-better-one/amfora/tree/master/contrib/themes)! - [x] Proxying - Schemes like Gopher or HTTP can be proxied through a Gemini server - [x] Client certificate support - [ ] Full client certificate UX within the client - Create transient and permanent certs within the client, per domain - Manage and browse them - Similar to [Kristall](https://github.com/MasterQ32/kristall) - https://lists.orbitalfox.eu/archives/gemini/2020/001400.html - [x] Subscriptions - Subscribing to RSS, Atom, and [JSON Feeds](https://jsonfeed.org/) are all supported - So is subscribing to a page, to know when it changes - [x] Open non-text files in another application - [x] Ability to stream content instead of downloading it first - [x] *Highlighting of preformatted code blocks that list a language in the alt text* - [ ] Stream support - [ ] Table of contents for pages - [ ] Search in pages with Ctrl-F - [ ] Persistent history ## Usage & Configuration Please see [the wiki](https://github.com/makeworld-the-better-one/amfora/wiki) for an introduction on how to use Amfora and configure it. ## Libraries Amfora ❤️ open source! - [cview](https://code.rocketnine.space/tslocum/cview) for the TUI - It's a fork of [tview](https://github.com/rivo/tview) with PRs merged and active support - It uses [tcell](https://github.com/gdamore/tcell) for low level terminal operations - [Viper](https://github.com/spf13/viper) for configuration and TOFU storing - [go-gemini](https://github.com/makeworld-the-better-one/go-gemini), my forked and updated Gemini client/server library - [progressbar](https://github.com/schollz/progressbar) - [go-humanize](https://github.com/dustin/go-humanize) - [gofeed](https://github.com/mmcdole/gofeed) - [chroma](https://github.com/alecthomas/chroma) for source code syntax highlighting - [clipboard](https://github.com/atotto/clipboard) - [termenv](https://github.com/muesli/termenv) ## License This project is licensed under the GPL v3.0. See the [LICENSE](./LICENSE) file for details. amfora-1.10.0/webbrowser/0000755000175000017500000000000014575704331014556 5ustar nileshnileshamfora-1.10.0/webbrowser/open_browser_unix.go0000644000175000017500000000354514575704331020663 0ustar nileshnilesh//go:build linux || freebsd || netbsd || openbsd // +build linux freebsd netbsd openbsd //nolint:goerr113 package webbrowser import ( "fmt" "os" "os/exec" ) // Open opens `url` in default system browser. It tries to do so in two // ways (xdg-open and $BROWSER). It only works if there is a display // server working. // // bouncepaw: I tried to support TTYs as well. The idea was to open // a browser in foreground and return back to amfora after the browser // is closed. While all browsers I tested opened correctly (w3m, lynx), // I couldn't make it restore amfora correctly. The screen always ended // up distorted. None of my stunts with altscreen buffers helped. func Open(url string) (string, error) { var ( // In prev versions there was also Xorg executable checked for. // I don't see any reason to check for it. xorgDisplay = os.Getenv("DISPLAY") waylandDisplay = os.Getenv("WAYLAND_DISPLAY") xdgOpenPath, xdgOpenNotFoundErr = exec.LookPath("xdg-open") envBrowser = os.Getenv("BROWSER") ) switch { case xorgDisplay == "" && waylandDisplay == "": return "", fmt.Errorf("no display server was found") case xdgOpenNotFoundErr == nil: // Prefer xdg-open over $BROWSER // Use start rather than run or output in order // to make browser running in background. proc := exec.Command(xdgOpenPath, url) if err := proc.Start(); err != nil { return "", err } //nolint:errcheck go proc.Wait() // Prevent zombies, see #219 return "Opened in system default web browser", nil case envBrowser != "": proc := exec.Command(envBrowser, url) if err := proc.Start(); err != nil { return "", err } //nolint:errcheck go proc.Wait() // Prevent zombies, see #219 return "Opened in system default web browser", nil default: return "", fmt.Errorf("could not determine system browser") } } amfora-1.10.0/webbrowser/open_browser_other.go0000644000175000017500000000057014575704331021014 0ustar nileshnilesh//go:build !linux && !darwin && !windows && !freebsd && !netbsd && !openbsd // +build !linux,!darwin,!windows,!freebsd,!netbsd,!openbsd package webbrowser import "fmt" // Open opens `url` in default system browser, but not on this OS. func Open(url string) (string, error) { return "", fmt.Errorf("unsupported OS for default HTTP handling. Set a command in the config") } amfora-1.10.0/webbrowser/open_browser_darwin.go0000644000175000017500000000056414575704331021162 0ustar nileshnilesh//go:build darwin // +build darwin package webbrowser import "os/exec" // Open opens `url` in default system browser. func Open(url string) (string, error) { proc := exec.Command("open", url) err := proc.Start() if err != nil { return "", err } //nolint:errcheck go proc.Wait() // Prevent zombies, see #219 return "Opened in system default web browser", nil } amfora-1.10.0/webbrowser/open_browser_windows.go0000644000175000017500000000100614575704331021360 0ustar nileshnilesh//go:build windows && (!linux || !darwin || !freebsd || !netbsd || !openbsd) // +build windows // +build !linux !darwin !freebsd !netbsd !openbsd package webbrowser import "os/exec" // Open opens `url` in default system browser. func Open(url string) (string, error) { proc := exec.Command("rundll32", "url.dll,FileProtocolHandler", url) err := proc.Start() if err != nil { return "", err } //nolint:errcheck go proc.Wait() // Prevent zombies, see #219 return "Opened in system default web browser", nil } amfora-1.10.0/webbrowser/README.md0000644000175000017500000000053714575704331016042 0ustar nileshnilesh# `package webbrowser` The code in this folder is adapted from Bombadillo, you can see the original [here](https://tildegit.org/sloum/bombadillo/src/branch/master/http). Many thanks to Sloum and the rest of the team! The code is simple, and I have changed it, but in any case there should be no licensing issues because both repos are under GPL v3. amfora-1.10.0/subscriptions/0000755000175000017500000000000014575704331015304 5ustar nileshnileshamfora-1.10.0/subscriptions/structs.go0000644000175000017500000000427314575704331017350 0ustar nileshnileshpackage subscriptions import ( "sync" "time" "github.com/mmcdole/gofeed" ) /* Example stored JSON. { "feeds": { "url1": , "url2": , }, "pages": { "url1": { "hash": , "changed":