From cb151f75aa3cdfcd61d5e84bd18dcc81d038f682 Mon Sep 17 00:00:00 2001 From: sloum Date: Sat, 9 May 2020 11:04:06 -0700 Subject: [PATCH 01/13] Handles cert expirations silently --- gemini/gemini.go | 17 +++++++++++++---- main.go | 17 ++++++++++++++++- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/gemini/gemini.go b/gemini/gemini.go index e13cb59..b83bee2 100644 --- a/gemini/gemini.go +++ b/gemini/gemini.go @@ -49,8 +49,8 @@ func (t *TofuDigest) Purge(host string) error { return fmt.Errorf("Invalid host %q", host) } -func (t *TofuDigest) Add(host, hash string) { - t.certs[strings.ToLower(host)] = hash +func (t *TofuDigest) Add(host, hash string, time int64) { + t.certs[strings.ToLower(host)] = fmt.Sprintf("%s|%d", hash, time) } func (t *TofuDigest) Exists(host string) bool { @@ -70,9 +70,10 @@ func (t *TofuDigest) Find(host string) (string, error) { func (t *TofuDigest) Match(host string, cState *tls.ConnectionState) error { host = strings.ToLower(host) now := time.Now() + localCert := strings.SplitN(t.certs[host], "|", -1)[0] for _, cert := range cState.PeerCertificates { - if t.certs[host] != hashCert(cert.Raw) { + if localCert != hashCert(cert.Raw) { continue } @@ -118,7 +119,7 @@ func (t *TofuDigest) newCert(host string, cState *tls.ConnectionState) error { continue } - t.Add(host, hashCert(cert.Raw)) + t.Add(host, hashCert(cert.Raw), cert.NotAfter.Unix()) return nil } @@ -132,6 +133,14 @@ func (t *TofuDigest) IniDump() string { var out strings.Builder out.WriteString("[CERTS]\n") for k, v := range t.certs { + vals := strings.SplitN(v, "|", -1) + now := time.Now() + if len(vals) > 1 { + ts, err := strconv.ParseInt(vals[1], 10, 64) + if err != nil || now.Unix() > ts { + continue + } + } out.WriteString(k) out.WriteString("=") out.WriteString(v) diff --git a/main.go b/main.go index 53918f7..1266eeb 100644 --- a/main.go +++ b/main.go @@ -25,8 +25,10 @@ import ( "os" "os/signal" "path/filepath" + "strconv" "strings" "syscall" + "time" "tildegit.org/sloum/bombadillo/config" "tildegit.org/sloum/bombadillo/cui" @@ -132,7 +134,20 @@ func loadConfig() { } for _, v := range settings.Certs { - bombadillo.Certs.Add(v.Key, v.Value) + // Remove expired certs + vals := strings.SplitN(v.Value, "|", -1) + if len(vals) < 2 { + continue + } + ts, err := strconv.ParseInt(vals[1], 10, 64) + now := time.Now() + if err != nil || now.Unix() > ts { + continue + } + // Satisfied that the cert is not expired + // or malformed: add to the current client + // instance + bombadillo.Certs.Add(v.Key, vals[0], ts) } } From 00313442d4c5dd8770c077ff06ea7e9ae341c670 Mon Sep 17 00:00:00 2001 From: sloum Date: Sat, 9 May 2020 16:18:38 -0700 Subject: [PATCH 02/13] Hopefully an improvement to the initial way of dealing with expired certs --- gemini/gemini.go | 45 ++++++++++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/gemini/gemini.go b/gemini/gemini.go index b83bee2..5b3cdb1 100644 --- a/gemini/gemini.go +++ b/gemini/gemini.go @@ -67,10 +67,8 @@ func (t *TofuDigest) Find(host string) (string, error) { return "", fmt.Errorf("Invalid hostname, no key saved") } -func (t *TofuDigest) Match(host string, cState *tls.ConnectionState) error { - host = strings.ToLower(host) +func (t *TofuDigest) Match(host, localCert string, cState *tls.ConnectionState) error { now := time.Now() - localCert := strings.SplitN(t.certs[host], "|", -1)[0] for _, cert := range cState.PeerCertificates { if localCert != hashCert(cert.Raw) { @@ -126,6 +124,33 @@ func (t *TofuDigest) newCert(host string, cState *tls.ConnectionState) error { return fmt.Errorf(reasons.String()) } +func (t *TofuDigest) GetCertAndTimestamp(host string) (string, int64, error) { + certTs, err := t.Find(host) + if err != nil { + return "", -1, err + } + certTsSplit := strings.SplitN(certTs, "|", -1) + if len(certTsSplit) < 2 { + _ = t.Purge(host) + return certTsSplit[0], -1, fmt.Errorf("Invalid certstring, no delimiter") + } + ts, err := strconv.ParseInt(certTsSplit[1], 10, 64) + if err != nil { + _ = t.Purge(host) + return certTsSplit[0], -1, err + } + now := time.Now() + if ts < now.Unix() { + // Ignore error return here since an error would indicate + // the host does not exist and we have already checked for + // that and the desired outcome of the action is that the + // host will no longer exist, so we are good either way + _ = t.Purge(host) + return "", -1, fmt.Errorf("Expired cert") + } + return certTsSplit[0], ts, nil +} + func (t *TofuDigest) IniDump() string { if len(t.certs) < 1 { return "" @@ -133,14 +158,6 @@ func (t *TofuDigest) IniDump() string { var out strings.Builder out.WriteString("[CERTS]\n") for k, v := range t.certs { - vals := strings.SplitN(v, "|", -1) - now := time.Now() - if len(vals) > 1 { - ts, err := strconv.ParseInt(vals[1], 10, 64) - if err != nil || now.Unix() > ts { - continue - } - } out.WriteString(k) out.WriteString("=") out.WriteString(v) @@ -185,9 +202,11 @@ func Retrieve(host, port, resource string, td *TofuDigest) (string, error) { return "", fmt.Errorf("Insecure, no certificates offered by server") } - if td.Exists(host) { + localCert, localTs, err := td.GetCertAndTimestamp(host) + + if localTs > 0 { // See if we have a matching cert - err := td.Match(host, &connState) + err := td.Match(host, localCert, &connState) if err != nil && err.Error() != "EXP" { // If there is no match and it isnt because of an expiration // just return the error From c58b40def2b655a99cf3281ca7f36aad84ed6db2 Mon Sep 17 00:00:00 2001 From: sloum Date: Thu, 14 May 2020 08:28:39 -0700 Subject: [PATCH 03/13] Removes goroutines in search that were causing input issues on gemini cgi scripts --- client.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client.go b/client.go index 5322c08..bb580f1 100644 --- a/client.go +++ b/client.go @@ -711,11 +711,11 @@ func (c *client) search(query, url, question string) { } switch u.Scheme { case "gopher": - go c.Visit(fmt.Sprintf("%s\t%s", u.Full, entry)) + c.Visit(fmt.Sprintf("%s\t%s", u.Full, entry)) case "gemini": // TODO url escape the entry variable escapedEntry := entry - go c.Visit(fmt.Sprintf("%s?%s", u.Full, escapedEntry)) + c.Visit(fmt.Sprintf("%s?%s", u.Full, escapedEntry)) case "http", "https": c.Visit(u.Full) default: @@ -996,7 +996,7 @@ func (c *client) handleGemini(u Url) { c.saveFileFromData(capsule.Content, filename) } case 3: - c.SetMessage(fmt.Sprintf("Follow redirect (y/n): %s?", capsule.Content), false) + c.SetMessage(fmt.Sprintf("Follow redirect? (y/n): %s", capsule.Content), false) c.DrawMessage() ch := cui.Getch() if ch == 'y' || ch == 'Y' { From a23e0026fa31954d347414d6f36b146ef7359d9a Mon Sep 17 00:00:00 2001 From: sloum Date: Fri, 15 May 2020 17:32:08 -0700 Subject: [PATCH 04/13] Adds command aliasing to vim keys for forward and back --- bombadillo.1 | 5 +++-- client.go | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/bombadillo.1 b/bombadillo.1 index ef3e479..b337a8e 100644 --- a/bombadillo.1 +++ b/bombadillo.1 @@ -59,7 +59,7 @@ Displaying web content directly in \fBbombadillo\fP requires lynx, w3m or elinks These commands work as a single keypress anytime \fBbombadillo\fP is not taking in a line based command or when the user is being prompted for action. This is the default command mode of \fBbombadillo\fP. .TP .B -b +b, h Navigate back one place in your document history. .TP .B @@ -71,7 +71,7 @@ d Scroll down an amount corresponding to 75% of your terminal window height in the current document. .TP .B -f +f, l Navigate forward one place in your document history. .TP .B @@ -95,6 +95,7 @@ n Jump to next found text item. .TP .B +N Jump to previous found text item. .TP .B diff --git a/client.go b/client.go index bb580f1..f9a3198 100644 --- a/client.go +++ b/client.go @@ -168,15 +168,15 @@ func (c *client) TakeControlInput() { } else { c.goToLink(string(input)) } - case 'j', 'J': + case 'j': // scroll down one line c.ClearMessage() c.Scroll(1) - case 'k', 'K': + case 'k': // scroll up one line c.ClearMessage() c.Scroll(-1) - case 'q', 'Q': + case 'q': // quit bombadillo cui.Exit(0, "") case 'g': @@ -197,7 +197,7 @@ func (c *client) TakeControlInput() { c.ClearMessage() distance := c.Height - c.Height/4 c.Scroll(-distance) - case 'b': + case 'b', 'h': // go back c.ClearMessage() err := c.PageState.NavigateHistory(-1) @@ -222,7 +222,7 @@ func (c *client) TakeControlInput() { // open the bookmarks browser c.BookMarks.ToggleOpen() c.Draw() - case 'f', 'F': + case 'f', 'l': // go forward c.ClearMessage() err := c.PageState.NavigateHistory(1) From 322002ba66602f0c5fc8396f9b1f20eead51a1a3 Mon Sep 17 00:00:00 2001 From: sloum Date: Fri, 15 May 2020 22:19:09 -0700 Subject: [PATCH 05/13] Adds a max width of 100, anything more gets weird to read. Also adds a slight optimization. --- page.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/page.go b/page.go index 4ca0842..13cf641 100644 --- a/page.go +++ b/page.go @@ -66,11 +66,13 @@ func (p *Page) RenderImage(width int) { // of the Page struct width a string slice // of the wrapped data func (p *Page) WrapContent(width int, color bool) { + width = min(width, 100) if p.FileType == "image" { p.RenderImage(width) return } counter := 0 + spacer := " " var content strings.Builder var esc strings.Builder escape := false @@ -124,7 +126,6 @@ func (p *Page) WrapContent(width int, color bool) { content.WriteRune('\n') counter = 0 if p.Location.Mime == "1" { - spacer := " " content.WriteString(spacer) counter += len(spacer) } @@ -188,3 +189,10 @@ func MakePage(url Url, content string, links []string) Page { p := Page{make([]string, 0), content, links, url, 0, make([]int, 0), "", 0, "", 40, false} return p } + +func min(a, b int) int { + if a < b { + return a + } + return b +} From 2bb8272cf9008fdb285c795168bb9e5b5413c2df Mon Sep 17 00:00:00 2001 From: sloum Date: Fri, 15 May 2020 22:22:32 -0700 Subject: [PATCH 06/13] Combines a few features into a release branch --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index c043eea..276cbf9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.2.1 +2.3.0 From 909131cda84a46626fb9d177a3f4c8aaa26fbc66 Mon Sep 17 00:00:00 2001 From: sloum Date: Fri, 15 May 2020 22:25:56 -0700 Subject: [PATCH 07/13] Lets images be full width --- page.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/page.go b/page.go index 13cf641..6973a7c 100644 --- a/page.go +++ b/page.go @@ -66,11 +66,11 @@ func (p *Page) RenderImage(width int) { // of the Page struct width a string slice // of the wrapped data func (p *Page) WrapContent(width int, color bool) { - width = min(width, 100) if p.FileType == "image" { p.RenderImage(width) return } + width = min(width, 100) counter := 0 spacer := " " var content strings.Builder From b23b9b31219ca6e689ccd064dbec975203f93309 Mon Sep 17 00:00:00 2001 From: sloum Date: Sun, 17 May 2020 21:36:49 -0700 Subject: [PATCH 08/13] Fixes the workflow for allowing alt text and handling preformatted blocks --- client.go | 4 +++- defaults.go | 1 + gemini/gemini.go | 26 +++++++++++++++++++------- main.go | 7 ++++++- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/client.go b/client.go index 5322c08..6ed3a95 100644 --- a/client.go +++ b/client.go @@ -459,7 +459,7 @@ func (c *client) doCommandAs(action string, values []string) { } case "SEARCH": c.search(strings.Join(values, " "), "", "") - case "SET", "S": + case "SET", "S": // TODO make the geminiblocks value work if _, ok := c.Options[values[0]]; ok { val := strings.Join(values[1:], " ") if !validateOpt(values[0], val) { @@ -470,6 +470,8 @@ func (c *client) doCommandAs(action string, values []string) { c.Options[values[0]] = lowerCaseOpt(values[0], val) if values[0] == "tlskey" || values[0] == "tlscertificate" { c.Certs.LoadCertificate(c.Options["tlscertificate"], c.Options["tlskey"]) + } else if values[0] == "geminiblocks" { + gemini.BlockBehavior = c.Options[values[0]] } else if values[0] == "configlocation" { c.SetMessage("Cannot set READ ONLY setting 'configlocation'", true) c.DrawMessage() diff --git a/defaults.go b/defaults.go index c4ca227..fc47a64 100644 --- a/defaults.go +++ b/defaults.go @@ -47,6 +47,7 @@ var defaultOptions = map[string]string{ "configlocation": xdgConfigPath(), "defaultscheme": "gopher", // "gopher", "gemini", "http", "https" + "geminiblocks": "block", // "block", "alt", "neither", "both" "homeurl": "gopher://bombadillo.colorfield.space:70/1/user-guide.map", "savelocation": homePath(), "searchengine": "gopher://gopher.floodgap.com:70/7/v2/vs", diff --git a/gemini/gemini.go b/gemini/gemini.go index e13cb59..05bc7c3 100644 --- a/gemini/gemini.go +++ b/gemini/gemini.go @@ -24,6 +24,8 @@ type TofuDigest struct { ClientCert tls.Certificate } +var BlockBehavior = "block" + //------------------------------------------------\\ // + + + R E C E I V E R S + + + \\ //--------------------------------------------------\\ @@ -339,16 +341,23 @@ func parseGemini(b, rootUrl, currentUrl string) (string, []string) { splitContent := strings.Split(b, "\n") links := make([]string, 0, 10) + inPreBlock := false + outputIndex := 0 for i, ln := range splitContent { splitContent[i] = strings.Trim(ln, "\r\n") - if ln == "```" { - // By continuing we create a variance between i and outputIndex - // the other branches here will write to the outputIndex rather - // than i, thus removing these lines while itterating without - // needing mroe allocations. - continue - } else if len([]rune(ln)) > 3 && ln[:2] == "=>" { + isPreBlockDeclaration := strings.HasPrefix(ln, "```") + if isPreBlockDeclaration && !inPreBlock && (BlockBehavior == "both" || BlockBehavior == "alt") { + inPreBlock = !inPreBlock + alt := strings.TrimSpace(ln) + if len(alt) > 3 { + alt = strings.TrimSpace(alt[3:]) + splitContent[outputIndex] = fmt.Sprintf("[ %s ]", alt) + outputIndex++ + } + } else if isPreBlockDeclaration { + inPreBlock = !inPreBlock + } else if len([]rune(ln)) > 3 && ln[:2] == "=>" && !inPreBlock { var link, decorator string subLn := strings.Trim(ln[2:], "\r\n\t \a") splitPoint := strings.IndexAny(subLn, " \t") @@ -370,6 +379,9 @@ func parseGemini(b, rootUrl, currentUrl string) (string, []string) { splitContent[outputIndex] = fmt.Sprintf("%-5s %s", linknum, decorator) outputIndex++ } else { + if inPreBlock && (BlockBehavior == "alt" || BlockBehavior == "neither") { + continue + } splitContent[outputIndex] = ln outputIndex++ } diff --git a/main.go b/main.go index 53918f7..92f4d13 100644 --- a/main.go +++ b/main.go @@ -30,6 +30,7 @@ import ( "tildegit.org/sloum/bombadillo/config" "tildegit.org/sloum/bombadillo/cui" + "tildegit.org/sloum/bombadillo/gemini" _ "tildegit.org/sloum/bombadillo/gemini" ) @@ -66,6 +67,7 @@ func validateOpt(opt, val string) bool { "theme": []string{"normal", "inverse", "color"}, "defaultscheme": []string{"gopher", "gemini", "http", "https"}, "showimages": []string{"true", "false"}, + "geminiblocks": []string{"block", "neither", "alt", "both"}, } opt = strings.ToLower(opt) @@ -84,7 +86,7 @@ func validateOpt(opt, val string) bool { func lowerCaseOpt(opt, val string) string { switch opt { - case "webmode", "theme", "defaultscheme", "showimages": + case "webmode", "theme", "defaultscheme", "showimages", "geminiblocks": return strings.ToLower(val) default: return val @@ -121,6 +123,9 @@ func loadConfig() { if _, ok := bombadillo.Options[lowerkey]; ok { if validateOpt(lowerkey, v.Value) { bombadillo.Options[lowerkey] = v.Value + if lowerkey == "geminiblocks" { + gemini.BlockBehavior = v.Value + } } else { bombadillo.Options[lowerkey] = defaultOptions[lowerkey] } From 9f1eb632bc375de9f1ec86137179f8b460db2461 Mon Sep 17 00:00:00 2001 From: sloum Date: Sun, 17 May 2020 22:01:31 -0700 Subject: [PATCH 09/13] Adds geminiblocks to the man page --- bombadillo.1 | 4 ++++ main.go | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/bombadillo.1 b/bombadillo.1 index ef3e479..42f40df 100644 --- a/bombadillo.1 +++ b/bombadillo.1 @@ -232,6 +232,10 @@ defaultscheme The scheme that should be used when no scheme is present in a given URL. \fIgopher\fP, \fIgemini\fP, \fIhttp\fP, and \fIhttps\fP are valid values. .TP .B +geminiblocks +Determines how to treat preformatted text blocks in text/gemini documents. \fIblock\fP will show the contents of the block, \fIalt\fP will show any available alt text for the block, \fIboth\fP will show both the content and the alt text, and \fIneither\fP will show neither. +.TP +.B homeurl The url that \fBbombadillo\fP navigates to when the program loads or when the \fIhome\fP or \fIh\fP LINE COMMAND is issued. This should be a valid url. If a scheme/protocol is not included, gopher will be assumed. .TP diff --git a/main.go b/main.go index 92f4d13..7216d0c 100644 --- a/main.go +++ b/main.go @@ -31,7 +31,6 @@ import ( "tildegit.org/sloum/bombadillo/config" "tildegit.org/sloum/bombadillo/cui" "tildegit.org/sloum/bombadillo/gemini" - _ "tildegit.org/sloum/bombadillo/gemini" ) var version string From b9057508d9b79138f0297b688512523cce4f4309 Mon Sep 17 00:00:00 2001 From: sloum Date: Mon, 18 May 2020 20:10:02 -0700 Subject: [PATCH 10/13] Reworks how relative URLs are handled for gemini --- client.go | 19 ++++++++++++++----- gemini/gemini.go | 30 +++++++++++++++++++++++++----- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/client.go b/client.go index 5322c08..b74e2c0 100644 --- a/client.go +++ b/client.go @@ -977,8 +977,10 @@ func (c *client) handleGemini(u Url) { go saveConfig() switch capsule.Status { case 1: + // Query c.search("", u.Full, capsule.Content) case 2: + // Success if capsule.MimeMaj == "text" || (c.Options["showimages"] == "true" && capsule.MimeMaj == "image") { pg := MakePage(u, capsule.Content, capsule.Links) pg.FileType = capsule.MimeMaj @@ -996,14 +998,21 @@ func (c *client) handleGemini(u Url) { c.saveFileFromData(capsule.Content, filename) } case 3: - c.SetMessage(fmt.Sprintf("Follow redirect (y/n): %s?", capsule.Content), false) - c.DrawMessage() - ch := cui.Getch() - if ch == 'y' || ch == 'Y' { + // Redirect + lowerRedirect := strings.ToLower(capsule.Content) + lowerOriginal := strings.ToLower(u.Full) + if strings.Replace(lowerRedirect, lowerOriginal, "", 1) == "/" { c.Visit(capsule.Content) } else { - c.SetMessage("Redirect aborted", false) + c.SetMessage(fmt.Sprintf("Follow redirect (y/n): %s?", capsule.Content), false) c.DrawMessage() + ch := cui.Getch() + if ch == 'y' || ch == 'Y' { + c.Visit(capsule.Content) + } else { + c.SetMessage("Redirect aborted", false) + c.DrawMessage() + } } } } diff --git a/gemini/gemini.go b/gemini/gemini.go index e13cb59..e90fe98 100644 --- a/gemini/gemini.go +++ b/gemini/gemini.go @@ -381,18 +381,38 @@ func handleRelativeUrl(u, root, current string) string { if len(u) < 1 { return u } + currentIsDir := (current[len(current)-1] == '/') if u[0] == '/' { return fmt.Sprintf("%s%s", root, u) + } else if strings.HasPrefix(u, "../") { + currentDir := strings.LastIndex(current, "/") + if currentIsDir { + upOne := strings.LastIndex(current[:currentDir], "/") + dirRoot := current[:upOne] + return dirRoot + u[2:] + } + return current[:currentDir] + u[2:] + } + + if strings.HasPrefix(u, "./") { + if len(u) == 2 { + return current + } + u = u[2:] + } + + if currentIsDir { + indPrevDir := strings.LastIndex(current[:len(current)-1], "/") + if indPrevDir < 9 { + return current + u + } + return current[:indPrevDir+1] + u } ind := strings.LastIndex(current, "/") - if ind < 10 { - return fmt.Sprintf("%s/%s", root, u) - } - current = current[:ind+1] - return fmt.Sprintf("%s%s", current, u) + return current + u } func hashCert(cert []byte) string { From 0257ca92b1e47e5028e2a55990398efd2907eb7a Mon Sep 17 00:00:00 2001 From: sloum Date: Tue, 19 May 2020 14:40:56 -0700 Subject: [PATCH 11/13] Adds clarification to man page and removes old comment --- bombadillo.1 | 2 +- client.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bombadillo.1 b/bombadillo.1 index 42f40df..bd5367e 100644 --- a/bombadillo.1 +++ b/bombadillo.1 @@ -233,7 +233,7 @@ The scheme that should be used when no scheme is present in a given URL. \fIgoph .TP .B geminiblocks -Determines how to treat preformatted text blocks in text/gemini documents. \fIblock\fP will show the contents of the block, \fIalt\fP will show any available alt text for the block, \fIboth\fP will show both the content and the alt text, and \fIneither\fP will show neither. +Determines how to treat preformatted text blocks in text/gemini documents. \fIblock\fP will show the contents of the block, \fIalt\fP will show any available alt text for the block, \fIboth\fP will show both the content and the alt text, and \fIneither\fP will show neither. Unlike other settings, a change to this value will require a fresh page load to see the change. .TP .B homeurl diff --git a/client.go b/client.go index 6ed3a95..cfd2e41 100644 --- a/client.go +++ b/client.go @@ -459,7 +459,7 @@ func (c *client) doCommandAs(action string, values []string) { } case "SEARCH": c.search(strings.Join(values, " "), "", "") - case "SET", "S": // TODO make the geminiblocks value work + case "SET", "S": if _, ok := c.Options[values[0]]; ok { val := strings.Join(values[1:], " ") if !validateOpt(values[0], val) { From e3e2afc4fcd6bdac096ef33c20bc165570599bbe Mon Sep 17 00:00:00 2001 From: Hannu Hartikainen Date: Sat, 23 May 2020 11:49:30 +0300 Subject: [PATCH 12/13] Replace calls to `stty` with syscalls - Move termios-related functionality to its own package - Reimplement stty calls using ioctl calls - Add per-platform constants for linux and non-linux because the ioctl enums are different. The enums in Darwin seem to come from BSD and so I'm assuming they might also work for other operating systems. If not, we'll need to add other build-constrained const files. This code has been tested on Darwin and will be tested on Linux before merging. --- client.go | 20 ++----------- cui/cui.go | 37 ++++++----------------- termios/consts_linux.go | 10 +++++++ termios/consts_nonlinux.go | 10 +++++++ termios/termios.go | 60 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 92 insertions(+), 45 deletions(-) create mode 100644 termios/consts_linux.go create mode 100644 termios/consts_nonlinux.go create mode 100644 termios/termios.go diff --git a/client.go b/client.go index 9c13b45..8640e31 100644 --- a/client.go +++ b/client.go @@ -4,7 +4,6 @@ import ( "fmt" "io/ioutil" "os" - "os/exec" "path/filepath" "regexp" "strconv" @@ -19,6 +18,7 @@ import ( "tildegit.org/sloum/bombadillo/http" "tildegit.org/sloum/bombadillo/local" "tildegit.org/sloum/bombadillo/telnet" + "tildegit.org/sloum/bombadillo/termios" ) //------------------------------------------------\\ @@ -43,14 +43,7 @@ type client struct { //--------------------------------------------------\\ func (c *client) GetSizeOnce() { - cmd := exec.Command("stty", "size") - cmd.Stdin = os.Stdin - out, err := cmd.Output() - if err != nil { - cui.Exit(5, "Fatal error: Unable to retrieve terminal size") - } - var h, w int - _, _ = fmt.Sscan(string(out), &h, &w) + var w, h = termios.GetWindowSize() c.Height = h c.Width = w } @@ -61,14 +54,7 @@ func (c *client) GetSize() { c.Draw() for { - cmd := exec.Command("stty", "size") - cmd.Stdin = os.Stdin - out, err := cmd.Output() - if err != nil { - cui.Exit(5, "Fatal error: Unable to retrieve terminal size") - } - var h, w int - _, _ = fmt.Sscan(string(out), &h, &w) + var w, h = termios.GetWindowSize() if h != c.Height || w != c.Width { c.Height = h c.Width = w diff --git a/cui/cui.go b/cui/cui.go index 676fcc6..c3ee14a 100644 --- a/cui/cui.go +++ b/cui/cui.go @@ -5,6 +5,8 @@ import ( "fmt" "os" "os/exec" + + "tildegit.org/sloum/bombadillo/termios" ) var Shapes = map[string]string{ @@ -55,16 +57,17 @@ func Exit(exitCode int, msg string) { // InitTerm sets the terminal modes appropriate for Bombadillo func InitTerm() { - SetCharMode() - Tput("smcup") // use alternate screen - Tput("rmam") // turn off line wrapping + termios.SetCharMode() + Tput("smcup") // use alternate screen + Tput("rmam") // turn off line wrapping + fmt.Print("\033[?25l") // hide cursor } // CleanupTerm reverts changs to terminal mode made by InitTerm func CleanupTerm() { moveCursorToward("down", 500) moveCursorToward("right", 500) - SetLineMode() + termios.SetLineMode() fmt.Print("\n") fmt.Print("\033[?25h") // reenables cursor blinking @@ -98,7 +101,7 @@ func Getch() rune { } func GetLine(prefix string) (string, error) { - SetLineMode() + termios.SetLineMode() reader := bufio.NewReader(os.Stdin) fmt.Print(prefix) @@ -107,32 +110,10 @@ func GetLine(prefix string) (string, error) { return "", err } - SetCharMode() + termios.SetCharMode() return text[:len(text)-1], nil } -func SetCharMode() { - cmd := exec.Command("stty", "cbreak", "-echo") - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - err := cmd.Run() - if err != nil { - panic(err) - } - - fmt.Print("\033[?25l") -} - -func SetLineMode() { - cmd := exec.Command("stty", "-cbreak", "echo") - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - err := cmd.Run() - if err != nil { - panic(err) - } -} - func Tput(opt string) { cmd := exec.Command("tput", opt) cmd.Stdin = os.Stdin diff --git a/termios/consts_linux.go b/termios/consts_linux.go new file mode 100644 index 0000000..ae6e076 --- /dev/null +++ b/termios/consts_linux.go @@ -0,0 +1,10 @@ +// +build linux + +package termios + +import "syscall" + +const ( + getTermiosIoctl = syscall.TCGETS + setTermiosIoctl = syscall.TCSETS +) diff --git a/termios/consts_nonlinux.go b/termios/consts_nonlinux.go new file mode 100644 index 0000000..ca0daf7 --- /dev/null +++ b/termios/consts_nonlinux.go @@ -0,0 +1,10 @@ +// +build !linux + +package termios + +import "syscall" + +const ( + getTermiosIoctl = syscall.TIOCGETA + setTermiosIoctl = syscall.TIOCSETAF +) diff --git a/termios/termios.go b/termios/termios.go new file mode 100644 index 0000000..9e0a5e7 --- /dev/null +++ b/termios/termios.go @@ -0,0 +1,60 @@ +package termios + +import ( + "os" + "runtime" + "syscall" + "unsafe" +) + +type winsize struct { + Row uint16 + Col uint16 + Xpixel uint16 + Ypixel uint16 +} + +var fd = os.Stdin.Fd() + +func ioctl(fd, request, argp uintptr) error { + if _, _, e := syscall.Syscall(syscall.SYS_IOCTL, fd, request, argp); e != 0 { + return e + } + return nil +} + +func GetWindowSize() (int, int) { + var value winsize + ioctl(fd, syscall.TIOCGWINSZ, uintptr(unsafe.Pointer(&value))) + return int(value.Col), int(value.Row) +} + +func getTermios() syscall.Termios { + var value syscall.Termios + err := ioctl(fd, getTermiosIoctl, uintptr(unsafe.Pointer(&value))) + if err != nil { + panic(err) + } + return value +} + +func setTermios(termios syscall.Termios) { + err := ioctl(fd, setTermiosIoctl, uintptr(unsafe.Pointer(&termios))) + if err != nil { + panic(err) + } + runtime.KeepAlive(termios) +} + +func SetCharMode() { + t := getTermios() + t.Lflag = t.Lflag ^ syscall.ICANON + t.Lflag = t.Lflag ^ syscall.ECHO + setTermios(t) +} + +func SetLineMode() { + var t = getTermios() + t.Lflag = t.Lflag | (syscall.ICANON | syscall.ECHO) + setTermios(t) +} From 0d8b3e015b9479cf2e3edc5fad4c9687719d0b14 Mon Sep 17 00:00:00 2001 From: sloum Date: Sun, 24 May 2020 07:45:34 -0700 Subject: [PATCH 13/13] Fixes querystring chaining issue --- client.go | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/client.go b/client.go index 5322c08..922b3fe 100644 --- a/client.go +++ b/client.go @@ -676,7 +676,7 @@ func (c *client) doLinkCommand(action, target string) { } -func (c *client) search(query, url, question string) { +func (c *client) search(query, uri, question string) { var entry string var err error if query == "" { @@ -700,22 +700,32 @@ func (c *client) search(query, url, question string) { } else { entry = query } - if url == "" { - url = c.Options["searchengine"] + if uri == "" { + uri = c.Options["searchengine"] } - u, err := MakeUrl(url) + u, err := MakeUrl(uri) if err != nil { - c.SetMessage("The search url is not a valid url", true) + c.SetMessage("The search url is not valid", true) c.DrawMessage() return } + var rootUrl string switch u.Scheme { case "gopher": - go c.Visit(fmt.Sprintf("%s\t%s", u.Full, entry)) + if ind := strings.Index(entry, "\t"); ind >= 0 { + rootUrl = u.Full[:ind] + } else { + rootUrl = u.Full + } + c.Visit(fmt.Sprintf("%s\t%s", rootUrl, entry)) case "gemini": - // TODO url escape the entry variable - escapedEntry := entry - go c.Visit(fmt.Sprintf("%s?%s", u.Full, escapedEntry)) + if ind := strings.Index(entry, "?"); ind >= 0 { + rootUrl = u.Full[:ind] + } else { + rootUrl = u.Full + } + // escapedEntry := url.QueryEscape(entry) // TODO confirm expected behavior re: escaping + c.Visit(fmt.Sprintf("%s?%s", rootUrl, entry)) case "http", "https": c.Visit(u.Full) default: