From bf84185bf50aceead34bf4303998c812a78e7bdb Mon Sep 17 00:00:00 2001 From: Elijah Gregg Date: Wed, 24 Feb 2021 08:26:53 -0700 Subject: [PATCH] =?UTF-8?q?Revert=20"=F0=9F=94=A5=20Removed=20favicon=20su?= =?UTF-8?q?pport=20-=20fixes=20#199"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 - README.md | 3 ++ cache/favicon.go | 51 ++++++++++++++++++++++++++++ config/config.go | 1 + config/default.go | 3 ++ default-config.toml | 3 ++ display/bookmarks.go | 13 ++++++-- display/display.go | 2 ++ display/handlers.go | 79 ++++++++++++++++++++++++++++++++++++++++++++ display/private.go | 5 +++ go.mod | 2 +- go.sum | 2 ++ structs/structs.go | 1 + 13 files changed, 161 insertions(+), 5 deletions(-) create mode 100644 cache/favicon.go diff --git a/CHANGELOG.md b/CHANGELOG.md index cb643efc..cac2cf69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Changed -- Favicon support removed (#199) - 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 from v1.8.0) (#197) diff --git a/README.md b/README.md index 81ff6616..2f7d3452 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,9 @@ Features in *italics* are in the master branch, but not in the latest release. - [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] Emoji favicons + - See `gemini://mozz.us/files/rfc_gemini_favicon.gmi` for details + - Disabled by default, enable in config - [x] Proxying - Schemes like Gopher or HTTP can be proxied through a Gemini server - [x] Client certificate support diff --git a/cache/favicon.go b/cache/favicon.go new file mode 100644 index 00000000..674a8f9d --- /dev/null +++ b/cache/favicon.go @@ -0,0 +1,51 @@ +package cache + +import ( + "sync" +) + +// Functions for caching emoji favicons. +// See gemini://mozz.us/files/rfc_gemini_favicon.gmi for details. + +var favicons = make(map[string]string) // domain to emoji +var favMu = sync.RWMutex{} + +var KnownNoFavicon = "no" + +// AddFavicon will add an emoji to the cache under that host. +// It does not verify that the string passed is actually an emoji. +// You can pass KnownNoFavicon as the emoji when a host doesn't have a valid favicon. +func AddFavicon(host, emoji string) { + favMu.Lock() + favicons[host] = emoji + favMu.Unlock() +} + +// ClearFavicons removes all favicons from the cache +func ClearFavicons() { + favMu.Lock() + favicons = make(map[string]string) + favMu.Unlock() +} + +// GetFavicon returns the favicon string for the host. +// It returns an empty string if there is no favicon cached. +// It might also return KnownNoFavicon to indicate that that host does not have +// a favicon at all. +func GetFavicon(host string) string { + favMu.RLock() + defer favMu.RUnlock() + return favicons[host] +} + +func NumFavicons() int { + favMu.RLock() + defer favMu.RUnlock() + return len(favicons) +} + +func RemoveFavicon(host string) { + favMu.Lock() + delete(favicons, host) + favMu.Unlock() +} diff --git a/config/config.go b/config/config.go index 42b391f2..d9ba488d 100644 --- a/config/config.go +++ b/config/config.go @@ -202,6 +202,7 @@ func Init() error { viper.SetDefault("a-general.temp_downloads", "") viper.SetDefault("a-general.page_max_size", 2097152) viper.SetDefault("a-general.page_max_time", 10) + viper.SetDefault("a-general.emoji_favicons", false) viper.SetDefault("a-general.scrollbar", "auto") viper.SetDefault("keybindings.bind_reload", []string{"R", "Ctrl-R"}) viper.SetDefault("keybindings.bind_home", "Backspace") diff --git a/config/default.go b/config/default.go index 6de65f1c..13e6e3ed 100644 --- a/config/default.go +++ b/config/default.go @@ -70,6 +70,9 @@ 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 +# Whether to replace tab numbers with emoji favicons, which are cached. +emoji_favicons = false + # 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" diff --git a/default-config.toml b/default-config.toml index 2fb70316..b4ebc180 100644 --- a/default-config.toml +++ b/default-config.toml @@ -67,6 +67,9 @@ 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 +# Whether to replace tab numbers with emoji favicons, which are cached. +emoji_favicons = false + # 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" diff --git a/display/bookmarks.go b/display/bookmarks.go index 116bbe34..c873fd72 100644 --- a/display/bookmarks.go +++ b/display/bookmarks.go @@ -85,8 +85,9 @@ func bkmkInit() { // 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) { +// It returns the bookmark name and the bookmark action: +// 1, 0, -1 for add/update, cancel, and remove +func openBkmkModal(name string, exists bool, favicon string) (string, int) { // Basically a copy of Input() // Reset buttons before input field, to make sure the input is in focus @@ -101,7 +102,9 @@ func openBkmkModal(name string, exists bool) (string, bkmkAction) { // Remove and re-add input field - to clear the old text bkmkModal.GetForm().Clear(false) - + if favicon != "" && !exists { + name = favicon + " " + name + } bkmkModalText = name bkmkModal.GetForm().AddInputField("Name: ", name, 0, nil, func(text string) { @@ -158,9 +161,13 @@ func addBookmark() { } name, exists := bookmarks.Get(p.URL) // Open a bookmark modal with the current name of the bookmark, if it exists +<<<<<<< HEAD newName, action := openBkmkModal(name, exists) //nolint:exhaustive +======= + newName, action := openBkmkModal(name, exists, p.Favicon) +>>>>>>> 5d4d349 (Revert "🔥 Removed favicon support - fixes #199") switch action { case add: bookmarks.Add(p.URL, newName) diff --git a/display/display.go b/display/display.go index 268b5b43..a3d4f65e 100644 --- a/display/display.go +++ b/display/display.go @@ -549,8 +549,10 @@ func Reload() { return } + parsed, _ := url.Parse(tabs[curTab].page.URL) go func(t *tab) { cache.RemovePage(tabs[curTab].page.URL) + cache.RemoveFavicon(parsed.Host) 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 diff --git a/display/handlers.go b/display/handlers.go index 8f633704..281a56ad 100644 --- a/display/handlers.go +++ b/display/handlers.go @@ -1,12 +1,15 @@ package display import ( + "bytes" "errors" + "io" "mime" "net" "net/url" "os/exec" "path" + "strconv" "strings" "github.com/makeworld-the-better-one/amfora/cache" @@ -18,6 +21,7 @@ import ( "github.com/makeworld-the-better-one/amfora/subscriptions" "github.com/makeworld-the-better-one/amfora/webbrowser" "github.com/makeworld-the-better-one/go-gemini" + "github.com/makeworld-the-better-one/go-isemoji" "github.com/spf13/viper" ) @@ -86,6 +90,81 @@ func handleOther(u string) { App.Draw() } +// handleFavicon handles getting and displaying a favicon. +func handleFavicon(t *tab, host string) { + defer func() { + // Update display if needed + if t.page.Favicon != "" && isValidTab(t) { + browser.SetTabLabel(strconv.Itoa(tabNumber(t)), makeTabLabel(t.page.Favicon)) + App.Draw() + } + }() + + if !viper.GetBool("a-general.emoji_favicons") { + // Not enabled + return + } + if t.page.Favicon != "" { + return + } + if host == "" { + return + } + + fav := cache.GetFavicon(host) + if fav == cache.KnownNoFavicon { + // It's been cached that this host doesn't have a favicon + return + } + if fav != "" { + t.page.Favicon = fav + return + } + + // No favicon cached + res, err := client.Fetch("gemini://" + host + "/favicon.txt") + if err != nil { + if res != nil { + res.Body.Close() + } + cache.AddFavicon(host, cache.KnownNoFavicon) + return + } + defer res.Body.Close() + + if res.Status != 20 { + cache.AddFavicon(host, cache.KnownNoFavicon) + return + } + if !strings.HasPrefix(res.Meta, "text/") && res.Meta != "" { + // Not a textual page + cache.AddFavicon(host, cache.KnownNoFavicon) + return + } + // It's a regular plain response + + buf := new(bytes.Buffer) + _, err = io.CopyN(buf, res.Body, 29+2+1) // 29 is the max emoji length, +2 for CRLF, +1 so that the right size will EOF + if err == nil { + // Content was too large + cache.AddFavicon(host, cache.KnownNoFavicon) + return + } else if err != io.EOF { + // Some network reading error + // No favicon is NOT known, could be a temporary error + return + } + // EOF, which is what we want. + emoji := strings.TrimRight(buf.String(), "\r\n") + if !isemoji.IsEmoji(emoji) { + cache.AddFavicon(host, cache.KnownNoFavicon) + return + } + // Valid favicon found + t.page.Favicon = emoji + cache.AddFavicon(host, emoji) +} + // 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. diff --git a/display/private.go b/display/private.go index 90cd8bd8..455cc168 100644 --- a/display/private.go +++ b/display/private.go @@ -117,6 +117,11 @@ func setPage(t *tab, p *structs.Page) { ) App.Draw() + go func() { + parsed, _ := url.Parse(p.URL) + handleFavicon(t, parsed.Host) + }() + // Setup display App.SetFocus(t.view) diff --git a/go.mod b/go.mod index 09f37bbf..e2c0c015 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/gdamore/tcell/v2 v2.1.1-0.20210125004847-19e17097d8fe github.com/google/go-cmp v0.5.0 // indirect github.com/makeworld-the-better-one/go-gemini v0.11.0 + github.com/makeworld-the-better-one/go-isemoji v1.1.0 github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/mapstructure v1.3.1 // indirect github.com/mmcdole/gofeed v1.1.0 @@ -24,7 +25,6 @@ require ( golang.org/x/text v0.3.5 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/ini.v1 v1.62.0 // indirect - gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect ) replace github.com/mmcdole/gofeed => github.com/makeworld-the-better-one/gofeed v1.1.1-0.20201123002655-c0c6354134fe diff --git a/go.sum b/go.sum index 829d7894..946275db 100644 --- a/go.sum +++ b/go.sum @@ -136,6 +136,8 @@ github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzR github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/makeworld-the-better-one/go-gemini v0.11.0 h1:MNGiULJFvcqls9oCy40tE897hDeKvNmEK9i5kRucgQk= github.com/makeworld-the-better-one/go-gemini v0.11.0/go.mod h1:F+3x+R1xeYK90jMtBq+U+8Sh64r2dHleDZ/en3YgSmg= +github.com/makeworld-the-better-one/go-isemoji v1.1.0 h1:wZBHOKB5zAIgaU2vaWnXFDDhatebB8TySrNVxjVV84g= +github.com/makeworld-the-better-one/go-isemoji v1.1.0/go.mod h1:FBjkPl9rr0G4vlZCc+Mr+QcnOfGCTbGWYW8/1sp06I0= github.com/makeworld-the-better-one/gofeed v1.1.1-0.20201123002655-c0c6354134fe h1:i3b9Qy5z23DcXRnrsMYcM5s9Ng5VIidM1xZd+szuTsY= github.com/makeworld-the-better-one/gofeed v1.1.1-0.20201123002655-c0c6354134fe/go.mod h1:QQO3maftbOu+hiVOGOZDRLymqGQCos4zxbA4j89gMrE= github.com/makeworld-the-better-one/progressbar/v3 v3.3.5-0.20201220005701-b036c4d38568 h1:fod4pD+rsU73WIUxl8Kpo35LDuOx0uxzlprBKbm84vw= diff --git a/structs/structs.go b/structs/structs.go index 1f07aa9a..d389a09b 100644 --- a/structs/structs.go +++ b/structs/structs.go @@ -33,6 +33,7 @@ type Page struct { Selected string // The current text or link selected SelectedID string // The cview region ID for the selected text/link Mode PageMode + Favicon string MadeAt time.Time // When the page was made. Zero value indicates it should stay in cache forever. }