Emoji favicon support - fixes #62

This commit is contained in:
makeworld 2020-08-05 13:31:59 -04:00
parent edd128e7c5
commit 0bc5939600
12 changed files with 221 additions and 38 deletions

View File

@ -7,11 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Emoji favicons can now be seen if `emoji_favicons` is enabled in the config (#62)
### Changed
### Fixed
- Two digit (and higher) link texts are now in line with one digit ones (#60)
- Race condition when reloading pages, could have caused the cache to still be used
## [1.4.0] - 2020-07-28

View File

@ -97,7 +97,7 @@ Features in *italics* are in the master branch, but not in the latest release.
- [x] Theming
- [ ] Subscribe to RSS and Atom feeds and display them
- Subscribing to page changes, similar to how Spacewalk works, will also be supported
- [ ] Emoji favicons
- [x] *Emoji favicons*
- See `gemini://mozz.us/files/rfc_gemini_favicon.gmi` for details
- [ ] Stream support
- [ ] Full client certificate UX within the client

58
cache/favicons.go vendored Normal file
View File

@ -0,0 +1,58 @@
package cache
import (
"fmt"
"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()
}
func AllFavicons() string {
favMu.RLock()
defer favMu.RUnlock()
return fmt.Sprintf("%v", favicons)
}

2
cache/redir.go vendored
View File

@ -31,8 +31,8 @@ func AddRedir(og, redir string) {
// ClearRedirs removes all redirects from the cache.
func ClearRedirs() {
redirMu.Lock()
defer redirMu.Unlock()
redirUrls = make(map[string]string)
redirMu.Unlock()
}
// Redirect takes the provided URL and returns a redirected version, if a redirect

View File

@ -153,6 +153,7 @@ func Init() error {
viper.SetDefault("a-general.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("cache.max_size", 0)
viper.SetDefault("cache.max_pages", 20)

View File

@ -12,6 +12,7 @@ var defaultConf = []byte(`# This is the default config file.
# example.com:123
[a-general]
# Press Ctrl-H to access it
home = "gemini://gemini.circumlunar.space"
# What command to run to open a HTTP URL. Set to "default" to try to guess the browser,
@ -19,21 +20,36 @@ home = "gemini://gemini.circumlunar.space"
# If a command is set, than the URL will be added (in quotes) to the end of the command.
# A space will be prepended if necessary.
http = "default"
search = "gemini://gus.guru/search" # Any URL that will accept a query string can be put here
color = true # Whether colors will be used in the terminal
bullets = true # Whether to replace list asterisks with unicode bullets
# Any URL that will accept a query string can be put here
search = "gemini://gus.guru/search"
# Whether colors will be used in the terminal
color = true
# Whether to replace list asterisks with unicode bullets
bullets = true
# A number from 0 to 1, indicating what percentage of the terminal width the left margin should take up.
left_margin = 0.15
max_width = 100 # The max number of columns to wrap a page's text to. Preformatted blocks are not wrapped.
# The max number of columns to wrap a page's text to. Preformatted blocks are not wrapped.
max_width = 100
# '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.
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
# Whether to replace tab numbers with emoji favicons, which are cached.
emoji_favicons = false
# Options for page cache - which is only for text/gemini pages
# Increase the cache size to speed up browsing at the expense of memory
[cache]
@ -41,15 +57,16 @@ page_max_time = 10
max_size = 0 # Size in bytes
max_pages = 30 # The maximum number of pages the cache will store
[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".
# 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.
# DO NOT use the names from that site, just the hex codes.
# Definitions:
# bg = background

View File

@ -486,24 +486,7 @@ func CloseTab() {
}
tabPages.SwitchToPage(strconv.Itoa(curTab)) // Go to previous page
// Rewrite the tab display
tabRow.Clear()
if viper.GetBool("a-general.color") {
for i := 0; i < NumTabs(); i++ {
fmt.Fprintf(tabRow, `["%d"][%s] %d [%s][""]|`,
i,
config.GetColorString("tab_num"),
i+1,
config.GetColorString("tab_divider"),
)
}
} else {
for i := 0; i < NumTabs(); i++ {
fmt.Fprintf(tabRow, `["%d"] %d [""]|`, i, i+1)
}
}
tabRow.Highlight(strconv.Itoa(curTab)).ScrollToHighlight()
rewriteTabRow()
// Restore previous tab's state
tabs[curTab].applyAll()
@ -550,8 +533,10 @@ func Reload() {
return
}
go cache.RemovePage(tabs[curTab].page.Url)
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) // goURL is not used bc history shouldn't be added to
if t == tabs[curTab] {
// Display the bottomBar state that handleURL set

View File

@ -1,16 +1,22 @@
package display
import (
"bytes"
"fmt"
"io"
"net/url"
"os/exec"
"strconv"
"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/webbrowser"
"github.com/makeworld-the-better-one/go-gemini"
"github.com/makeworld-the-better-one/go-isemoji"
"github.com/spf13/viper"
"gitlab.com/tslocum/cview"
)
@ -106,8 +112,16 @@ func setPage(t *tab, p *structs.Page) {
// Make sure the page content is fitted to the terminal every time it's displayed
reformatPage(p)
// Change page on screen
oldFav := t.page.Favicon
t.page = p
go func() {
parsed, _ := url.Parse(p.Url)
handleFavicon(t, parsed.Host, oldFav)
}()
// Change page on screen
t.view.SetText(p.Content)
t.view.Highlight("") // Turn off highlights, other funcs may restore if necessary
t.view.ScrollToBeginning()
@ -144,6 +158,81 @@ func handleHTTP(u string, showInfo bool) {
App.Draw()
}
// handleFavicon handles getting and displaying a favicon.
// `old` is the previous favicon for the tab.
func handleFavicon(t *tab, host, old string) {
defer func() {
// Update display if needed
if t.page.Favicon != old && isValidTab(t) {
rewriteTabRow()
}
}()
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
rewriteTabRow()
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/plain") {
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)
}
// 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.
@ -336,3 +425,32 @@ func handleURL(t *tab, u string) (string, bool) {
go dlChoice("That file could not be displayed. What would you like to do?", u, res)
return ret("", false)
}
// rewriteTabRow clears the tabRow and writes all the tabs number/favicons into it.
func rewriteTabRow() {
tabRow.Clear()
if viper.GetBool("a-general.color") {
for i := 0; i < NumTabs(); i++ {
char := strconv.Itoa(i + 1)
if tabs[i].page.Favicon != "" {
char = tabs[i].page.Favicon
}
fmt.Fprintf(tabRow, `["%d"][%s] %s [%s][""]|`,
i,
config.GetColorString("tab_num"),
char,
config.GetColorString("tab_divider"),
)
}
} else {
for i := 0; i < NumTabs(); i++ {
char := strconv.Itoa(i + 1)
if tabs[i].page.Favicon != "" {
char = tabs[i].page.Favicon
}
fmt.Fprintf(tabRow, `["%d"] %s [""]|`, i, char)
}
}
tabRow.Highlight(strconv.Itoa(curTab)).ScrollToHighlight()
App.Draw()
}

4
go.mod
View File

@ -7,6 +7,7 @@ require (
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/gdamore/tcell v1.3.1-0.20200608133353-cb1e5d6fa606
github.com/makeworld-the-better-one/go-gemini v0.7.0
github.com/makeworld-the-better-one/go-isemoji v1.0.0
github.com/makeworld-the-better-one/progressbar/v3 v3.3.5-0.20200710151429-125743e22b4f
github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/mapstructure v1.3.1 // indirect
@ -16,10 +17,9 @@ require (
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.7.0
github.com/stretchr/testify v1.6.0
github.com/stretchr/testify v1.6.1
gitlab.com/tslocum/cview v1.4.8-0.20200713214710-cc7796c4ca44
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1 // indirect
golang.org/x/text v0.3.3
gopkg.in/ini.v1 v1.57.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200603094226-e3079894b1e8 // indirect
)

10
go.sum
View File

@ -126,6 +126,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.7.0 h1:TCerE47eYHLXj6RQDjfd5HdGVbcVqpBC6OoPBlyY7q4=
github.com/makeworld-the-better-one/go-gemini v0.7.0/go.mod h1:P7/FbZ+IEIbA/d+A0Y3w2GNgD8SA2AcNv7aDGJbaWG4=
github.com/makeworld-the-better-one/go-isemoji v1.0.0 h1:W3O4+qwtXeT8PUDzcQ1UjxiupQWgc/oJHpqwrllx3xM=
github.com/makeworld-the-better-one/go-isemoji v1.0.0/go.mod h1:FBjkPl9rr0G4vlZCc+Mr+QcnOfGCTbGWYW8/1sp06I0=
github.com/makeworld-the-better-one/progressbar/v3 v3.3.5-0.20200710151429-125743e22b4f h1:YEUlTs5gb35UlBLTgqrub9axWTYB3d7/8TxrkJDZpRI=
github.com/makeworld-the-better-one/progressbar/v3 v3.3.5-0.20200710151429-125743e22b4f/go.mod h1:X6sxWNi9PBgQybpR4fpXPVD5fm7svLqZTQ5DJuERIoM=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
@ -203,8 +205,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho=
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
@ -345,8 +347,8 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200603094226-e3079894b1e8 h1:jL/vaozO53FMfZLySWM+4nulF3gQEC6q5jH90LPomDo=
gopkg.in/yaml.v3 v3.0.0-20200603094226-e3079894b1e8/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/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=

View File

@ -67,7 +67,6 @@ func MakePage(url string, res *gemini.Response, width, leftMargin int) (*structs
_, err := io.CopyN(buf, res.Body, viper.GetInt64("a-general.page_max_size")+1)
res.Body.Close()
rawText := buf.Bytes()
if err == nil {
// Content was larger than max size
return nil, ErrTooLarge
@ -86,14 +85,14 @@ func MakePage(url string, res *gemini.Response, width, leftMargin int) (*structs
// Convert content first
var utfText string
if isUTF8(params["charset"]) {
utfText = string(rawText)
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, errors.New("unsupported encoding")
}
utfText, err = encoding.NewDecoder().String(string(rawText))
utfText, err = encoding.NewDecoder().String(buf.String())
if err != nil {
return nil, err
}

View File

@ -29,6 +29,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
}
// Size returns an approx. size of a Page in bytes.