amfora-1.9.2/0000755000175000017500000000000014154702677012333 5ustar nileshnileshamfora-1.9.2/bookmarks/0000755000175000017500000000000014154702677014323 5ustar nileshnileshamfora-1.9.2/bookmarks/xbel.go0000644000175000017500000000245414154702677015611 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.9.2/bookmarks/bookmarks.go0000644000175000017500000001061314154702677016643 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.9.2/go.sum0000644000175000017500000011472114154702677013474 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.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 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 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE= github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo= github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/tcell/v2 v2.2.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU= github.com/gdamore/tcell/v2 v2.3.3 h1:RKoI6OcqYrr/Do8yHZklecdGzDTJH9ACKdfECbRdw3M= github.com/gdamore/tcell/v2 v2.3.3/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lucasb-eyer/go-colorful v1.0.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.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/makeworld-the-better-one/go-gemini v0.12.1 h1:cWHvCHL31Caq3Rm9elCFFoQeyrn92Kv7KummsVxCOFg= github.com/makeworld-the-better-one/go-gemini v0.12.1/go.mod h1:F+3x+R1xeYK90jMtBq+U+8Sh64r2dHleDZ/en3YgSmg= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/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.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.3.1 h1:cCBH2gTD2K0OtLlv/Y5H01VQCqmlDxz30kS5Y5bqfLA= github.com/mitchellh/mapstructure v1.3.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mmcdole/gofeed v1.1.2 h1:7I5su6dO5/Rg2LEKS5ofPISVbi2vfxO2SNVSA/QN1y4= github.com/mmcdole/gofeed v1.1.2/go.mod h1:QQO3maftbOu+hiVOGOZDRLymqGQCos4zxbA4j89gMrE= github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf h1:sWGE2v+hO0Nd4yFU/S/mDBM5plIU8v/Qhfz41hkDIAI= github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf/go.mod h1:pasqhqstspkosTneA62Nc+2p9SOBBYAPbnmRRWPQ0V8= 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 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/muesli/termenv v0.9.0 h1:wnbOaGz+LUR3jNT0zOzinPnyDaCZUQRZj9GxK8eRVl8= github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.8.0 h1:Keo9qb7iRJs2voHvunFtuuYFsbWeOBh8/P9v/kVMFtw= github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rkoesters/xdg v0.0.0-20181125232953-edd15b846f9b h1:8NiY6v9/IlFU8osj1L7kqzRbrG6e3izRQQjGze1Q1R0= github.com/rkoesters/xdg v0.0.0-20181125232953-edd15b846f9b/go.mod h1:T1HolqzmdHnJIH6p7A9LDuvYGQgEHx9ijX3vKgDKU60= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/schollz/progressbar/v3 v3.8.0 h1:BKyefEMgFBDbo+JaeqHcm/9QdSj8qG8sUY+6UppGpnw= github.com/schollz/progressbar/v3 v3.8.0/go.mod h1:Y9mmL2knZj3LUaBDyBEzFdPrymIr08hnlFMZmfxwbx4= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.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= github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/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-20201216054612-986b41b23924 h1:QsnDpLLOKwHBBDa8nDws4DYNc/ryVW2vCpxCs09d4PY= golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210223095934-7937bea0104d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea h1:+WiDlPBBaO+h9vPNZi8uJ3k4BkKQB7Iow3aqwHVA5hI= golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 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 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w= golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= 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.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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-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= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= amfora-1.9.2/.golangci.yml0000644000175000017500000000124114154702677014715 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.9.2/amfora.go0000644000175000017500000000537714154702677014143 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.9.2" 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) } client.Init() 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.9.2/logo.png0000644000175000017500000005017614154702677014012 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.9.2/cache/0000755000175000017500000000000014154702677013376 5ustar nileshnileshamfora-1.9.2/cache/redir_test.go0000644000175000017500000000100614154702677016066 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.9.2/cache/page_test.go0000644000175000017500000000301214154702677015674 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.9.2/cache/redir.go0000644000175000017500000000240714154702677015035 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.9.2/cache/page.go0000644000175000017500000000636214154702677014650 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.9.2/README.md0000644000175000017500000001554214154702677013621 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. ## 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 - [ ] 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) - [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.9.2/rr/0000755000175000017500000000000014154702677012756 5ustar nileshnileshamfora-1.9.2/rr/README.md0000644000175000017500000000365014154702677014241 0ustar nileshnilesh# package `rr`, aka `RestartReader` This package exists just to hold the `RestartReader` type. It wraps `io.ReadCloser` and implements it. It holds the data from every `Read` in a `[]byte` buffer, and allows you to call `.Restart()`, causing subsequent `Read` calls to start from the beginning again. See [#140](https://github.com/makeworld-the-better-one/amfora/issues/140) for why this was needed. Other projects are encouraged to copy this code if it's useful to them, and this package may move out of Amfora if I end up using it in multiple projects. ## License If you prefer, you can consider the code in this package, and this package only, to be licensed under the MIT license instead. So the code in this package is dual-licensed. You can use the LICENSE file in the root of this repo, or the license text below.
Click to see MIT license terms ``` Copyright (c) 2020 makeworld Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ```
amfora-1.9.2/rr/rr.go0000644000175000017500000000312414154702677013730 0ustar nileshnileshpackage rr import ( "errors" "io" ) var ErrClosed = errors.New("RestartReader: closed") type RestartReader struct { r io.ReadCloser buf []byte // Where in the buffer we are. If it's equal to len(buf) then the reader // should be used. i int64 } func (rr *RestartReader) Read(p []byte) (n int, err error) { if rr.buf == nil { return 0, ErrClosed } if rr.i >= int64(len(rr.buf)) { // Read new data tmp := make([]byte, len(p)) n, err = rr.r.Read(tmp) if n > 0 { rr.buf = append(rr.buf, tmp[:n]...) copy(p, tmp[:n]) } rr.i = int64(len(rr.buf)) return } // Reading from buffer bufSize := len(rr.buf[rr.i:]) if len(p) > bufSize { // It wants more data then what's in the buffer tmp := make([]byte, len(p)-bufSize) n, err = rr.r.Read(tmp) if n > 0 { rr.buf = append(rr.buf, tmp[:n]...) } copy(p, rr.buf[rr.i:]) n += bufSize rr.i = int64(len(rr.buf)) return } // All the required data is in the buffer end := rr.i + int64(len(p)) copy(p, rr.buf[rr.i:end]) rr.i = end n = len(p) err = nil return } // Restart causes subsequent Read calls to read from the beginning, instead // of where they left off. func (rr *RestartReader) Restart() { rr.i = 0 } // Close clears the buffer and closes the underlying io.ReadCloser, returning // its error. func (rr *RestartReader) Close() error { rr.buf = nil return rr.r.Close() } // NewRestartReader creates and initializes a new RestartReader that reads from // the provided io.ReadCloser. func NewRestartReader(r io.ReadCloser) *RestartReader { return &RestartReader{ r: r, buf: make([]byte, 0), } } amfora-1.9.2/rr/rr_test.go0000644000175000017500000000176514154702677015000 0ustar nileshnileshpackage rr import ( "io/ioutil" "strings" "testing" "github.com/stretchr/testify/assert" ) var r1 *RestartReader func reset() { r1 = NewRestartReader(ioutil.NopCloser(strings.NewReader("1234567890"))) } func TestRead(t *testing.T) { reset() p := make([]byte, 1) n, err := r1.Read(p) assert.Equal(t, 1, n, "should read one byte") assert.Equal(t, nil, err, "should be no error") assert.Equal(t, []byte{'1'}, p, "should have read one byte, '1'") } //nolint func TestRestart(t *testing.T) { reset() p := make([]byte, 4) r1.Read(p) r1.Restart() p = make([]byte, 5) n, err := r1.Read(p) assert.Equal(t, []byte("12345"), p, "should read the first 5 bytes again") assert.Equal(t, 5, n, "should have read 4 bytes") assert.Equal(t, nil, err, "err should be nil") r1.Restart() p = make([]byte, 4) n, err = r1.Read(p) assert.Equal(t, []byte("1234"), p, "should read the first 4 bytes again") assert.Equal(t, 4, n, "should have read 4 bytes") assert.Equal(t, nil, err, "err should be nil") } amfora-1.9.2/default-config.toml0000644000175000017500000003114114154702677016117 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://gemini.circumlunar.space" # 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 to replace list asterisks with unicode bullets bullets = true # Whether to show link after link text show_link = false # A number from 0 to 1, indicating what percentage of the terminal width the left margin should take up. left_margin = 0.15 # 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. # 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 domain name equal to path to client cert # "example.com" = 'mycert.crt' [auth.keys] # Client certificate keys # Set domain name equal to path to key for the client cert above # "example.com" = 'mycert.key' [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) [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' # [[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 [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 # 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.9.2/structs/0000755000175000017500000000000014154702677014042 5ustar nileshnileshamfora-1.9.2/structs/structs.go0000644000175000017500000000337514154702677016110 0ustar nileshnilesh//nolint:lll package structs import "time" type Mediatype string const ( TextGemini Mediatype = "text/gemini" TextPlain Mediatype = "text/plain" TextAnsi Mediatype = "text/x-ansi" ) type PageMode int const ( ModeOff PageMode = iota // Regular mode ModeLinkSelect // When the enter key is pressed, allow for tab-based link navigation ModeSearch // When a keyword is being searched in a page - TODO: NOT USED YET ) // Page is for storing UTF-8 text/gemini pages, as well as text/plain pages. type Page struct { URL string Mediatype Mediatype // Used for rendering purposes, generalized RawMediatype string // The actual mediatype sent by the server Raw string // The raw response, as received over the network Content string // The processed content, NOT raw. Uses cview color tags. It will also have a left margin. Links []string // URLs, for each region in the content. Row int // Vertical scroll position Column int // Horizontal scroll position - does not map exactly to a cview.TextView because it includes left margin size changes, see #197 TermWidth int // The terminal width when the Content was set, to know when reformatting should happen. Selected string // The current text or link selected SelectedID string // The cview region ID for the selected text/link Mode PageMode MadeAt time.Time // When the page was made. Zero value indicates it should stay in cache forever. } // Size returns an approx. size of a Page in bytes. func (p *Page) Size() int { n := len(p.Raw) + len(p.Content) + len(p.URL) + len(p.Selected) + len(p.SelectedID) for i := range p.Links { n += len(p.Links[i]) } return n } amfora-1.9.2/config/0000755000175000017500000000000014154702677013600 5ustar nileshnileshamfora-1.9.2/config/config.go0000644000175000017500000003276114154702677015405 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://gemini.circumlunar.space") 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.bullets", true) viper.SetDefault("a-general.show_link", false) viper.SetDefault("a-general.left_margin", 0.15) viper.SetDefault("a-general.max_width", 100) 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("url-handlers.other", "default") 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.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")) // Setup theme configTheme := viper.Sub("theme") if configTheme != nil { 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) } 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) } } 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.9.2/config/default.go0000644000175000017500000003125114154702677015555 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://gemini.circumlunar.space" # 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 to replace list asterisks with unicode bullets bullets = true # Whether to show link after link text show_link = false # A number from 0 to 1, indicating what percentage of the terminal width the left margin should take up. left_margin = 0.15 # 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. # 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 domain name equal to path to client cert # "example.com" = 'mycert.crt' [auth.keys] # Client certificate keys # Set domain name equal to path to key for the client cert above # "example.com" = 'mycert.key' [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) [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' # [[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 [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 # 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.9.2/config/default.sh0000755000175000017500000000031614154702677015563 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.9.2/config/keybindings.go0000644000175000017500000001620214154702677016436 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 ) 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", } // 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.9.2/config/theme.go0000644000175000017500000003160114154702677015232 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.9.2/amfora.desktop0000644000175000017500000000035514154702677015176 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.9.2/subscriptions/0000755000175000017500000000000014154702677015242 5ustar nileshnileshamfora-1.9.2/subscriptions/entries.go0000644000175000017500000000727614154702677017256 0ustar nileshnileshpackage subscriptions import ( "net/url" "sort" "strings" "time" ) // This file contains funcs for creating PageEntries, which // are consumed by display/subscriptions.go // getURL returns a URL to be used in a PageEntry, from a // list of URLs for that item. It prefers gemini URLs, then // HTTP(S), then by order. func getURL(urls []string) string { if len(urls) == 0 { return "" } var firstHTTP string for _, u := range urls { if strings.HasPrefix(u, "gemini://") { return u } if (strings.HasPrefix(u, "http://") || strings.HasPrefix(u, "https://")) && firstHTTP == "" { // First HTTP(S) URL in the list firstHTTP = u } } if firstHTTP != "" { return firstHTTP } return urls[0] } // GetPageEntries returns the current list of PageEntries // for use in rendering a page. // The contents of the returned entries will never change, // so this function needs to be called again to get updates. // It always returns sorted entries - by post time, from newest to oldest. func GetPageEntries() *PageEntries { var pe PageEntries data.RLock() for _, feed := range data.Feeds { for _, item := range feed.Items { if item.Links == nil || len(item.Links) == 0 { // Ignore items without links continue } // Set pub var pub time.Time // Try to use updated time first, then published if item.UpdatedParsed != nil && !item.UpdatedParsed.IsZero() { pub = *item.UpdatedParsed } else if item.PublishedParsed != nil && !item.PublishedParsed.IsZero() { pub = *item.PublishedParsed } else { // No time on the post, use now pub = time.Now() } // Set prefix // Prefer using the feed title over anything else. // Many feeds in Gemini only have this due to gemfeed's default settings. prefix := feed.Title if prefix == "" { // feed.Title was empty if item.Author != nil { // Prefer using the item author over the feed author prefix = item.Author.Name } else { if feed.Author != nil { prefix = feed.Author.Name } else { prefix = "[author unknown]" } } } else { // There's already a title, so add the author (if exists) to // the end of the title in parentheses. // Don't add the author if it's the same as the title. if item.Author != nil && item.Author.Name != prefix { // Prefer using the item author over the feed author prefix += " (" + item.Author.Name + ")" } else if feed.Author != nil && feed.Author.Name != prefix { prefix += " (" + feed.Author.Name + ")" } } pe.Entries = append(pe.Entries, &PageEntry{ Prefix: prefix, Title: item.Title, URL: getURL(item.Links), Published: pub, }) } } for u, page := range data.Pages { parsed, _ := url.Parse(u) // Path is title title := parsed.Path if strings.HasPrefix(title, "/~") && title != "/~" { // A user dir title = title[2:] // Remove beginning slash and tilde // Remove trailing slash if the root of a user dir is being tracked if strings.Count(title, "/") <= 1 && title[len(title)-1] == '/' { title = title[:len(title)-1] } } else if strings.HasPrefix(title, "/users/") && title != "/users/" { // "/users/" is removed for aesthetics when tracking hosted users title = strings.TrimPrefix(title, "/users/") title = strings.TrimPrefix(title, "~") // Remove leading tilde // Remove trailing slash if the root of a user dir is being tracked if strings.Count(title, "/") <= 1 && title[len(title)-1] == '/' { title = title[:len(title)-1] } } pe.Entries = append(pe.Entries, &PageEntry{ Prefix: parsed.Host, Title: title, URL: u, Published: page.Changed, }) } data.RUnlock() sort.Sort(&pe) return &pe } amfora-1.9.2/subscriptions/subscriptions.go0000644000175000017500000002427714154702677020514 0ustar nileshnileshpackage subscriptions import ( "crypto/sha256" "encoding/json" "errors" "fmt" "io" "io/ioutil" "mime" urlPkg "net/url" "os" "path" "reflect" "strings" "sync" "time" "github.com/makeworld-the-better-one/amfora/client" "github.com/makeworld-the-better-one/amfora/config" "github.com/makeworld-the-better-one/go-gemini" "github.com/mmcdole/gofeed" "github.com/spf13/viper" ) var ( ErrSaving = errors.New("couldn't save JSON to disk") ErrNotSuccess = errors.New("status 20 not returned") ErrNotFeed = errors.New("not a valid feed") ErrTooManyRedirects = errors.New("redirected more than 5 times") ) var writeMu = sync.Mutex{} // Prevent concurrent writes to subscriptions.json file // LastUpdated is the time when the in-memory data was last updated. // It can be used to know if the subscriptions page should be regenerated. var LastUpdated time.Time // Init should be called after config.Init. func Init() error { f, err := os.Open(config.SubscriptionPath) if err == nil { // File exists and could be opened fi, err := f.Stat() if err == nil && fi.Size() > 0 { // File is not empty jsonBytes, err := ioutil.ReadAll(f) f.Close() if err != nil { return fmt.Errorf("read subscriptions.json error: %w", err) } err = json.Unmarshal(jsonBytes, &data) if err != nil { return fmt.Errorf("subscriptions.json 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 subscriptions.json error: %w", err) } if data.Feeds == nil { data.Feeds = make(map[string]*gofeed.Feed) } if data.Pages == nil { data.Pages = make(map[string]*pageJSON) } LastUpdated = time.Now() if viper.GetInt("subscriptions.update_interval") > 0 { // Update subscriptions every so often go func() { for { updateAll() time.Sleep(time.Duration(viper.GetInt("subscriptions.update_interval")) * time.Second) } }() } else { // User disabled automatic updates // So just update once at the beginning go updateAll() } return nil } // IsSubscribed returns true if the URL is already subscribed to, // whether a feed or page. func IsSubscribed(url string) bool { data.feedMu.RLock() for u := range data.Feeds { if url == u { data.feedMu.RUnlock() return true } } data.feedMu.RUnlock() data.pageMu.RLock() for u := range data.Pages { if url == u { data.pageMu.RUnlock() return true } } data.pageMu.RUnlock() return false } // GetFeed returns a Feed object and a bool indicating whether the passed // content was actually recognized as a feed. func GetFeed(mediatype, filename string, r io.Reader) (*gofeed.Feed, bool) { if r == nil { return nil, false } // Check mediatype and filename if mediatype != "application/atom+xml" && mediatype != "application/rss+xml" && mediatype != "application/json+feed" && filename != "atom.xml" && filename != "feed.xml" && filename != "feed.json" && !strings.HasSuffix(filename, ".atom") && !strings.HasSuffix(filename, ".rss") && !strings.HasSuffix(filename, ".xml") { // No part of the above is true return nil, false } feed, err := gofeed.NewParser().Parse(r) if feed == nil { return nil, false } return feed, err == nil } func writeJSON() error { writeMu.Lock() defer writeMu.Unlock() data.Lock() jsonBytes, err := json.MarshalIndent(&data, "", " ") data.Unlock() if err != nil { return err } err = ioutil.WriteFile(config.SubscriptionPath, jsonBytes, 0666) if err != nil { return err } return nil } // AddFeed stores a feed. // It can be used to update a feed for a URL, although the package // will handle that on its own. func AddFeed(url string, feed *gofeed.Feed) error { if feed == nil { panic("feed is nil") } // Remove any unused fields to save memory and disk space feed.Image = nil feed.Generator = "" feed.Categories = nil feed.DublinCoreExt = nil feed.ITunesExt = nil feed.Custom = nil feed.Link = "" feed.Links = nil for _, item := range feed.Items { item.Description = "" item.Content = "" item.Image = nil item.Categories = nil item.Enclosures = nil item.DublinCoreExt = nil item.ITunesExt = nil item.Extensions = nil item.Custom = nil item.Link = "" // Links is used instead } data.feedMu.Lock() oldFeed, ok := data.Feeds[url] if !ok || !reflect.DeepEqual(feed, oldFeed) { // Feeds are different, or there was never an old one LastUpdated = time.Now() data.Feeds[url] = feed data.feedMu.Unlock() err := writeJSON() if err != nil { return ErrSaving } } else { data.feedMu.Unlock() } return nil } // AddPage stores a page to track for changes. // It can be used to update the page as well, although the package // will handle that on its own. func AddPage(url string, r io.Reader) error { if r == nil { return nil } h := sha256.New() if _, err := io.Copy(h, r); err != nil { return err } newHash := fmt.Sprintf("%x", h.Sum(nil)) data.pageMu.Lock() _, ok := data.Pages[url] if !ok || data.Pages[url].Hash != newHash { // Page content is different, or it didn't exist LastUpdated = time.Now() data.Pages[url] = &pageJSON{ Hash: newHash, Changed: time.Now().UTC(), } data.pageMu.Unlock() err := writeJSON() if err != nil { return ErrSaving } } else { data.pageMu.Unlock() } return nil } // getResource returns a URL and Response for the given URL. // It will follow up to 5 redirects, and if there is a permanent // redirect it will return the new URL. Otherwise the URL will // stay the same. THe returned URL will never be empty. // // If there is over 5 redirects the error will be ErrTooManyRedirects. // ErrNotSuccess, as well as other fetch errors will also be returned. func getResource(url string) (string, *gemini.Response, error) { res, err := client.Fetch(url) if err != nil { if res != nil { res.Body.Close() } return url, nil, err } status := gemini.CleanStatus(res.Status) if status == gemini.StatusSuccess { // No redirects return url, res, nil } parsed, err := urlPkg.Parse(url) if err != nil { return url, nil, err } i := 0 redirs := make([]int, 0) urls := make([]*urlPkg.URL, 0) // Loop through redirects for (status == gemini.StatusRedirectPermanent || status == gemini.StatusRedirectTemporary) && i < 5 { redirs = append(redirs, status) urls = append(urls, parsed) tmp, err := parsed.Parse(res.Meta) if err != nil { // Redirect URL returned by the server is invalid return url, nil, err } parsed = tmp // Make the new request res, err := client.Fetch(parsed.String()) if err != nil { if res != nil { res.Body.Close() } return url, nil, err } i++ } // Two possible options here: // - Never redirected, got error on start // - No more redirects, other status code // - Too many redirects if i == 0 { // Never redirected or succeeded return url, res, ErrNotSuccess } if i < 5 { // The server stopped redirecting after <5 redirects if status == gemini.StatusSuccess { // It ended by succeeding for j := range redirs { if redirs[j] == gemini.StatusRedirectTemporary { if j == 0 { // First redirect is temporary return url, res, nil } // There were permanent redirects before this one // Return the URL of the latest permanent redirect return urls[j-1].String(), res, nil } } // They were all permanent redirects return urls[len(urls)-1].String(), res, nil } // It stopped because there was a non-redirect, non-success response return url, res, ErrNotSuccess } // Too many redirects, return original return url, nil, ErrTooManyRedirects } func updateFeed(url string) { newURL, res, err := getResource(url) if err != nil { return } mediatype, _, err := mime.ParseMediaType(res.Meta) if err != nil { return } filename := path.Base(newURL) feed, ok := GetFeed(mediatype, filename, res.Body) if !ok { return } err = AddFeed(newURL, feed) if url != newURL && err == nil { // URL has changed, remove old one Remove(url) //nolint:errcheck } } func updatePage(url string) { newURL, res, err := getResource(url) if err != nil { return } err = AddPage(newURL, res.Body) if url != newURL && err == nil { // URL has changed, remove old one Remove(url) //nolint:errcheck } } // updateAll updates all subscriptions using workers. // It only returns once all the workers are done. func updateAll() { worker := func(jobs <-chan [2]string, wg *sync.WaitGroup) { // Each job is: [2]string{, "url"} // where is "feed" or "page" defer wg.Done() for j := range jobs { if j[0] == "feed" { updateFeed(j[1]) } else if j[0] == "page" { updatePage(j[1]) } } } var wg sync.WaitGroup data.RLock() numJobs := len(data.Feeds) + len(data.Pages) jobs := make(chan [2]string, numJobs) if numJobs == 0 { data.RUnlock() return } numWorkers := viper.GetInt("subscriptions.workers") if numWorkers < 1 { numWorkers = 1 } // Start workers, waiting for jobs for w := 0; w < numWorkers; w++ { wg.Add(1) go func() { worker(jobs, &wg) }() } // Get map keys in a slice feedKeys := make([]string, len(data.Feeds)) i := 0 for k := range data.Feeds { feedKeys[i] = k i++ } pageKeys := make([]string, len(data.Pages)) i = 0 for k := range data.Pages { pageKeys[i] = k i++ } data.RUnlock() for j := 0; j < numJobs; j++ { if j < len(feedKeys) { jobs <- [2]string{"feed", feedKeys[j]} } else { // In the Pages jobs <- [2]string{"page", pageKeys[j-len(feedKeys)]} } } close(jobs) wg.Wait() } // AllURLs returns all the subscribed-to URLS. func AllURLS() []string { data.RLock() defer data.RUnlock() urls := make([]string, len(data.Feeds)+len(data.Pages)) i := 0 for k := range data.Feeds { urls[i] = k i++ } for k := range data.Pages { urls[i] = k i++ } return urls } // Remove removes a subscription from memory and from the disk. // The URL must be provided. It will do nothing if the URL is // not an actual subscription. // // It returns any errors that occurred when saving to disk. func Remove(u string) error { data.Lock() // Just delete from both instead of using a loop to find it delete(data.Feeds, u) delete(data.Pages, u) data.Unlock() return writeJSON() } amfora-1.9.2/subscriptions/structs.go0000644000175000017500000000427314154702677017306 0ustar nileshnileshpackage subscriptions import ( "sync" "time" "github.com/mmcdole/gofeed" ) /* Example stored JSON. { "feeds": { "url1": , "url2": , }, "pages": { "url1": { "hash": , "changed":