2020-07-21 11:30:32 -04:00
|
|
|
package display
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"net/url"
|
2021-05-14 21:00:44 -04:00
|
|
|
"reflect"
|
2020-11-05 10:38:40 -05:00
|
|
|
"strings"
|
2021-05-14 21:00:44 -04:00
|
|
|
"unsafe"
|
2020-07-21 11:30:32 -04:00
|
|
|
|
2021-04-07 12:56:32 -04:00
|
|
|
"code.rocketnine.space/tslocum/cview"
|
2020-12-19 19:41:25 -05:00
|
|
|
"github.com/makeworld-the-better-one/go-gemini"
|
2020-07-21 11:30:32 -04:00
|
|
|
"github.com/spf13/viper"
|
2020-12-19 19:41:25 -05:00
|
|
|
"golang.org/x/text/unicode/norm"
|
2020-07-21 11:30:32 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
// This file contains funcs that are small, self-contained utilities.
|
|
|
|
|
2021-02-17 14:17:13 -05:00
|
|
|
// makeContentLayout returns a flex that contains the given TextView
|
2021-02-27 18:17:49 -05:00
|
|
|
// along with the provided left margin, as well as a single empty
|
2021-02-17 14:17:13 -05:00
|
|
|
// line at the top, for a top margin.
|
2021-02-27 18:17:49 -05:00
|
|
|
func makeContentLayout(tv *cview.TextView, leftMargin int) *cview.Flex {
|
2021-02-17 14:17:13 -05:00
|
|
|
// Create horizontal flex with the left margin as an empty space
|
|
|
|
horiz := cview.NewFlex()
|
|
|
|
horiz.SetDirection(cview.FlexColumn)
|
2021-02-27 18:17:49 -05:00
|
|
|
if leftMargin > 0 {
|
|
|
|
horiz.AddItem(nil, leftMargin, 0, false)
|
|
|
|
}
|
2021-02-17 14:17:13 -05:00
|
|
|
horiz.AddItem(tv, 0, 1, true)
|
|
|
|
|
|
|
|
// Create a vertical flex with the other one and a top margin
|
|
|
|
vert := cview.NewFlex()
|
|
|
|
vert.SetDirection(cview.FlexRow)
|
|
|
|
vert.AddItem(nil, 1, 0, false)
|
|
|
|
vert.AddItem(horiz, 0, 1, true)
|
|
|
|
|
|
|
|
return vert
|
2020-11-05 10:38:40 -05:00
|
|
|
}
|
|
|
|
|
2021-02-17 14:17:13 -05:00
|
|
|
// makeTabLabel takes a string and adds spacing to it, making it
|
|
|
|
// suitable for display as a tab label.
|
|
|
|
func makeTabLabel(s string) string {
|
|
|
|
return " " + s + " "
|
|
|
|
}
|
|
|
|
|
|
|
|
// tabNumber gets the index of the tab in the tabs slice. It returns -1
|
|
|
|
// if the tab is not in that slice.
|
|
|
|
func tabNumber(t *tab) int {
|
2020-07-21 11:30:32 -04:00
|
|
|
tempTabs := tabs
|
|
|
|
for i := range tempTabs {
|
|
|
|
if tempTabs[i] == t {
|
2021-02-17 14:17:13 -05:00
|
|
|
return i
|
2020-07-21 11:30:32 -04:00
|
|
|
}
|
|
|
|
}
|
2021-02-17 14:17:13 -05:00
|
|
|
return -1
|
|
|
|
}
|
|
|
|
|
|
|
|
// escapeMeta santizes a META string for use within a cview modal.
|
|
|
|
func escapeMeta(meta string) string {
|
|
|
|
return cview.Escape(strings.ReplaceAll(meta, "\n", ""))
|
|
|
|
}
|
|
|
|
|
|
|
|
// isValidTab indicates whether the passed tab is still being used, even if it's not currently displayed.
|
|
|
|
func isValidTab(t *tab) bool {
|
|
|
|
return tabNumber(t) != -1
|
2020-07-21 11:30:32 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
func leftMargin() int {
|
|
|
|
return int(float64(termW) * viper.GetFloat64("a-general.left_margin"))
|
|
|
|
}
|
|
|
|
|
|
|
|
func textWidth() int {
|
|
|
|
if termW <= 0 {
|
|
|
|
// This prevent a flash of 1-column text on startup, when the terminal
|
|
|
|
// width hasn't been initialized.
|
|
|
|
return viper.GetInt("a-general.max_width")
|
|
|
|
}
|
|
|
|
|
|
|
|
rightMargin := leftMargin()
|
|
|
|
if leftMargin() > 10 {
|
|
|
|
// 10 is the max right margin
|
|
|
|
rightMargin = 10
|
|
|
|
}
|
|
|
|
|
|
|
|
max := termW - leftMargin() - rightMargin
|
|
|
|
if max < viper.GetInt("a-general.max_width") {
|
|
|
|
return max
|
|
|
|
}
|
|
|
|
return viper.GetInt("a-general.max_width")
|
|
|
|
}
|
|
|
|
|
2021-06-25 19:03:41 -04:00
|
|
|
// resolveRelLink returns an absolute link for the given relative link.
|
2020-07-21 11:30:32 -04:00
|
|
|
// It also returns an error if it could not resolve the links, which should be displayed
|
|
|
|
// to the user.
|
2021-06-25 19:03:41 -04:00
|
|
|
func (t *tab) resolveRelLink(next string) (string, error) {
|
2021-05-13 16:38:53 -04:00
|
|
|
if !t.hasContent() || t.isAnAboutPage() {
|
2020-07-21 11:30:32 -04:00
|
|
|
return next, nil
|
|
|
|
}
|
|
|
|
|
2021-06-25 19:03:41 -04:00
|
|
|
prevParsed, _ := url.Parse(t.page.URL)
|
2020-07-21 11:30:32 -04:00
|
|
|
nextParsed, err := url.Parse(next)
|
|
|
|
if err != nil {
|
2020-08-27 17:57:19 -04:00
|
|
|
return "", errors.New("link URL could not be parsed") //nolint:goerr113
|
2020-07-21 11:30:32 -04:00
|
|
|
}
|
|
|
|
return prevParsed.ResolveReference(nextParsed).String(), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// normalizeURL attempts to make URLs that are different strings
|
|
|
|
// but point to the same place all look the same.
|
|
|
|
//
|
|
|
|
// Example: gemini://gus.guru:1965/ and //gus.guru/.
|
|
|
|
// This function will take both output the same URL each time.
|
|
|
|
//
|
2020-12-19 19:41:25 -05:00
|
|
|
// It will also percent-encode invalid characters, and decode chars
|
|
|
|
// that don't need to be encoded. It will also apply Unicode NFC
|
|
|
|
// normalization.
|
|
|
|
//
|
2020-07-21 11:30:32 -04:00
|
|
|
// The string passed must already be confirmed to be a URL.
|
|
|
|
// Detection of a search string vs. a URL must happen elsewhere.
|
|
|
|
//
|
|
|
|
// It only works with absolute URLs.
|
|
|
|
func normalizeURL(u string) string {
|
2020-12-19 19:41:25 -05:00
|
|
|
u = norm.NFC.String(u)
|
|
|
|
|
|
|
|
tmp, err := gemini.GetPunycodeURL(u)
|
2020-07-21 11:30:32 -04:00
|
|
|
if err != nil {
|
|
|
|
return u
|
|
|
|
}
|
2020-12-19 19:41:25 -05:00
|
|
|
u = tmp
|
|
|
|
parsed, _ := url.Parse(u)
|
2020-07-21 11:30:32 -04:00
|
|
|
|
|
|
|
if parsed.Scheme == "" {
|
|
|
|
// Always add scheme
|
|
|
|
parsed.Scheme = "gemini"
|
|
|
|
} else if parsed.Scheme != "gemini" {
|
|
|
|
// Not a gemini URL, nothing to do
|
|
|
|
return u
|
|
|
|
}
|
|
|
|
|
|
|
|
parsed.User = nil // No passwords in Gemini
|
|
|
|
parsed.Fragment = "" // No fragments either
|
|
|
|
if parsed.Port() == "1965" {
|
|
|
|
// Always remove default port
|
2021-02-17 09:25:02 -05:00
|
|
|
hostname := parsed.Hostname()
|
|
|
|
if strings.Contains(hostname, ":") {
|
|
|
|
parsed.Host = "[" + parsed.Hostname() + "]"
|
|
|
|
} else {
|
|
|
|
parsed.Host = parsed.Hostname()
|
|
|
|
}
|
2020-07-21 11:30:32 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Add slash to the end of a URL with just a domain
|
|
|
|
// gemini://example.com -> gemini://example.com/
|
|
|
|
if parsed.Path == "" {
|
|
|
|
parsed.Path = "/"
|
2020-12-19 19:41:25 -05:00
|
|
|
} else {
|
|
|
|
// Decode and re-encode path
|
|
|
|
// This removes needless encoding, like that of ASCII chars
|
|
|
|
// And encodes anything that wasn't but should've been
|
|
|
|
parsed.RawPath = strings.ReplaceAll(url.PathEscape(parsed.Path), "%2F", "/")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Do the same to the query string
|
|
|
|
un, err := gemini.QueryUnescape(parsed.RawQuery)
|
|
|
|
if err == nil {
|
|
|
|
parsed.RawQuery = gemini.QueryEscape(un)
|
2020-07-21 11:30:32 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
return parsed.String()
|
|
|
|
}
|
2020-12-24 16:25:39 -05:00
|
|
|
|
|
|
|
// fixUserURL will take a user-typed URL and add a gemini scheme to it if
|
|
|
|
// necessary. It is not the same as normalizeURL, and that func should still
|
|
|
|
// be used, afterward.
|
|
|
|
//
|
|
|
|
// For example "example.com" will become "gemini://example.com", but
|
|
|
|
// "//example.com" will be left untouched.
|
|
|
|
func fixUserURL(u string) string {
|
|
|
|
if !strings.HasPrefix(u, "//") && !strings.HasPrefix(u, "gemini://") && !strings.Contains(u, "://") {
|
|
|
|
// Assume it's a Gemini URL
|
|
|
|
u = "gemini://" + u
|
|
|
|
}
|
|
|
|
return u
|
|
|
|
}
|
2021-05-14 21:00:44 -04:00
|
|
|
|
|
|
|
func stringToBytes(s string) []byte {
|
|
|
|
// Sources:
|
|
|
|
// https://stackoverflow.com/a/59210739
|
|
|
|
// https://groups.google.com/g/golang-nuts/c/Zsfk-VMd_fU/m/O1ru4fO-BgAJ
|
|
|
|
return (*[0x7fff0000]byte)(unsafe.Pointer((*reflect.StringHeader)(unsafe.Pointer(&s)).Data))[:len(s):len(s)]
|
|
|
|
}
|