diff --git a/.gitignore b/.gitignore index cb9380e..6f40a17 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ bombadillo *.asciinema +*.swp diff --git a/DEVELOPING.md b/DEVELOPING.md index 4b05842..6ecdc8b 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -25,10 +25,10 @@ Changes are implemented to the default branch when: ### Process for introducing a new change -Please refer to our [notes on contributing](README.md#contributing) to get an understanding of how new changes are initiated, the type of changes accepted and the review process. +Before you begin, please refer to our [notes on contributing](README.md#contributing) to get an understanding of how new changes are initiated, the type of changes accepted and the review process. 1. Create a new feature branch based on the **develop** branch. -1. Raise a pull request (PR) targeting the **develop** branch. +1. Raise a pull request (PR) targeting the current release branch (confirm this in the issue comments before proceeding). 1. The PR is reviewed. 1. If the PR is approved, it is merged. 1. The version number is incremented, along with any other release activity. diff --git a/Makefile b/Makefile index ddbc4bc..cca7657 100644 --- a/Makefile +++ b/Makefile @@ -13,17 +13,9 @@ test : GOCMD := go1.11.13 # %:z - so settle for %z. BUILD_TIME := ${shell date "+%Y-%m-%dT%H:%M%z"} -# If VERSION is empty or not defined use the contents of the VERSION file -VERSION := ${shell git describe --exact-match 2> /dev/null} -ifndef VERSION - VERSION := ${shell cat ./VERSION} -endif - -LDFLAGS := -ldflags "-s -X main.version=${VERSION} -X main.build=${BUILD_TIME}" - .PHONY: build build: - ${GOCMD} build ${LDFLAGS} -o ${BINARY} + ${GOCMD} build -o ${BINARY} .PHONY: install install: install-bin install-man install-desktop clean diff --git a/VERSION b/VERSION index f90b1af..0bee604 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.3.2 +2.3.3 diff --git a/bombadillo.1 b/bombadillo.1 index a66816d..f1e9839 100644 --- a/bombadillo.1 +++ b/bombadillo.1 @@ -181,6 +181,14 @@ home Navigates to the document set by the \fIhomeurl\fP setting. \fIh\fP can be entered, rather than the full \fIhome\fP. .TP .B +jump +Navigates to the previous page in history from the current page. Useful for keeping the current page in your history while still browsing. \fIj\fP can be used instead of the full \fIjump\fP. +.TP +.B +jump [history location] +Navigates to the given history location. The history location should be an integer between 0 and 20. \fIj\fP can be used instead of the full \fIjump\fP. +.TP +.B purge * Deletes all pinned gemini server certificates. \fIp\fP can be used instead of the full \fIpurge\fP. .TP @@ -209,6 +217,10 @@ set [setting name] [value] Sets the value for a given configuration setting. \fIs\fP can be used instead of the full \fIset\fP. .TP .B +version +Shows the current Bombadillo version number. +.TP +.B write . Writes the current document to a file. The file is named by the last component of the url path. If the last component is blank or \fI/\fP a default name will be used. The file saves to the directory set by the \fIsavelocation\fP setting. \fIw\fP can be entered rather than the full \fIwrite\fP. .TP @@ -246,7 +258,7 @@ The path to the directory that \fBbombadillo\fP should write files to. This must .TP .B searchengine -The url to use for the LINE COMMANDs \fI?\fP and \fIsearch\fP. Should be a valid search path that terms may be appended to. +The url to use for the LINE COMMAND \fIsearch\fP. Should be a valid search path that terms may be appended to. .TP .B telnetcommand @@ -259,6 +271,8 @@ Can toggle between visual modes. Valid values are \fInormal\fP, \fIcolor\fP, and .B timeout The number of seconds after which connections to gopher or gemini servers should time out if the server has not responded. +.TP +.B webmode Controls behavior when following web links. The following values are valid: \fInone\fP will disable following web links, \fIgui\fP will have the browser attempt to open web links in a user's default graphical web browser; \fIlynx\fP, \fIw3m\fP, and \fIelinks\fP will have the browser attempt to use the selected terminal web browser to handle the rendering of web pages and will display the pages directly in Bombadillo. diff --git a/client.go b/client.go index 1765951..b1795cb 100644 --- a/client.go +++ b/client.go @@ -258,6 +258,7 @@ func (c *client) TakeControlInput() { } err = c.NextSearchItem(0) if err != nil { + c.PageState.History[c.PageState.Position].WrapContent(c.Width-1,(c.Options["theme"] == "color")) c.Draw() } case ':', ' ': @@ -344,23 +345,41 @@ func (c *client) simpleCommand(action string) { case "SEARCH": c.search("", "", "?") case "HELP", "?": - go c.Visit(helplocation) + c.Visit(helplocation) + case "JUMP", "J": + err := c.PageState.CopyHistory(-1) + if err != nil { + c.SetMessage(err.Error(), false) + c.DrawMessage() + } else { + c.Draw() + } + case "VERSION": + ver := version + if ver == "" { + ver = "Improperly compiled, no version information" + } + c.SetMessage("Bombadillo version: " + ver, false) + c.DrawMessage() default: - c.SetMessage(fmt.Sprintf("Unknown action %q", action), true) + c.SetMessage(syntaxErrorMessage(action), true) c.DrawMessage() } } func (c *client) doCommand(action string, values []string) { - if length := len(values); length != 1 { - c.SetMessage(fmt.Sprintf("Expected 1 argument, received %d", len(values)), true) - c.DrawMessage() - return - } - switch action { - case "CHECK", "C": + case "C", "CHECK": c.displayConfigValue(values[0]) + c.DrawMessage() + case "HELP", "?": + if val, ok := ERRS[values[0]]; ok { + c.SetMessage("Usage: " + val, false) + } else { + msg := fmt.Sprintf("%q is not a valid command; help syntax: %s", values[0], ERRS[action]) + c.SetMessage(msg, false) + } + c.DrawMessage() case "PURGE", "P": err := c.Certs.Purge(values[0]) if err != nil { @@ -403,26 +422,23 @@ func (c *client) doCommand(action string, values []string) { fn = "index" } c.saveFile(u, fn) - default: - c.SetMessage(fmt.Sprintf("Unknown action %q", action), true) + c.SetMessage(syntaxErrorMessage(action), true) c.DrawMessage() } } func (c *client) doCommandAs(action string, values []string) { - if len(values) < 2 { - c.SetMessage(fmt.Sprintf("Expected 2+ arguments, received %d", len(values)), true) - c.DrawMessage() - return - } - - if values[0] == "." { - values[0] = c.PageState.History[c.PageState.Position].Location.Full - } - switch action { case "ADD", "A": + if len(values) < 2 { + c.SetMessage(syntaxErrorMessage(action), true) + c.DrawMessage() + return + } + if values[0] == "." { + values[0] = c.PageState.History[c.PageState.Position].Location.Full + } msg, err := c.BookMarks.Add(values) if err != nil { c.SetMessage(err.Error(), true) @@ -441,8 +457,18 @@ func (c *client) doCommandAs(action string, values []string) { c.Draw() } case "SEARCH": + if len(values) < 2 { + c.SetMessage(syntaxErrorMessage(action), true) + c.DrawMessage() + return + } c.search(strings.Join(values, " "), "", "") case "SET", "S": + if len(values) < 2 { + c.SetMessage(syntaxErrorMessage(action), true) + c.DrawMessage() + return + } if _, ok := c.Options[values[0]]; ok { val := strings.Join(values[1:], " ") if !validateOpt(values[0], val) { @@ -473,7 +499,7 @@ func (c *client) doCommandAs(action string, values []string) { c.SetMessage(fmt.Sprintf("Unable to set %s, it does not exist", values[0]), true) c.DrawMessage() default: - c.SetMessage(fmt.Sprintf("Unknown command structure"), true) + c.SetMessage(syntaxErrorMessage(action), true) c.DrawMessage() } } @@ -523,7 +549,7 @@ func (c *client) doLinkCommandAs(action, target string, values []string) { out = append(out, values...) c.doCommandAs(action, out) default: - c.SetMessage(fmt.Sprintf("Unknown command structure"), true) + c.SetMessage(syntaxErrorMessage(action), true) c.DrawMessage() } } @@ -630,6 +656,15 @@ func (c *client) doLinkCommand(action, target string) { link := links[num] c.SetMessage(fmt.Sprintf("[%d] %s", num+1, link), false) c.DrawMessage() + case "JUMP", "J": + num-- + err = c.PageState.CopyHistory(num) + if err != nil { + c.SetMessage(err.Error(), false) + c.DrawMessage() + } else { + c.Draw() + } case "WRITE", "W": links := c.PageState.History[c.PageState.Position].Links if len(links) < num || num < 1 { @@ -655,7 +690,7 @@ func (c *client) doLinkCommand(action, target string) { } c.saveFile(u, fn) default: - c.SetMessage("Unknown command structure", true) + c.SetMessage(syntaxErrorMessage(action), true) c.DrawMessage() } @@ -977,6 +1012,7 @@ func (c *client) handleGemini(u Url) { case 2: // Success if capsule.MimeMaj == "text" || (c.Options["showimages"] == "true" && capsule.MimeMaj == "image") { + u.Mime = capsule.MimeMin pg := MakePage(u, capsule.Content, capsule.Links) pg.FileType = capsule.MimeMaj pg.WrapContent(c.Width-1, (c.Options["theme"] == "color")) @@ -1203,6 +1239,13 @@ func findAvailableFileName(fpath, fname string) (string, error) { return savePath, nil } +func syntaxErrorMessage(action string) string { + if val, ok := ERRS[action]; ok { + return fmt.Sprintf("Incorrect syntax. Try: %s", val) + } + return fmt.Sprintf("Unknown command %q", action) +} + func updateTimeouts(timeoutString string) error { sec, err := strconv.Atoi(timeoutString) if err != nil { diff --git a/cmdparse/lexer.go b/cmdparse/lexer.go index 8b882c3..448bbbf 100644 --- a/cmdparse/lexer.go +++ b/cmdparse/lexer.go @@ -72,7 +72,7 @@ func (s *scanner) scanText() Token { "S", "SET", "R", "RELOAD", "SEARCH", "Q", "QUIT", "B", "BOOKMARKS", "H", "HOME", "?", "HELP", "C", "CHECK", - "P", "PURGE": + "P", "PURGE", "JUMP", "J", "VERSION": return Token{Action, capInput} } diff --git a/cmdparse/parser.go b/cmdparse/parser.go index dc5870a..39e08ee 100644 --- a/cmdparse/parser.go +++ b/cmdparse/parser.go @@ -94,10 +94,10 @@ func (p *Parser) parseAction() (*Command, error) { case Value: cm.Target = t.val cm.Type = DOLINK - case Word: + case Word, Action: cm.Value = append(cm.Value, t.val) cm.Type = DO - case Action, Whitespace: + case Whitespace: return nil, fmt.Errorf("Found %q (%d), expected value", t.val, t.kind) } t = p.scan() diff --git a/cui/cui.go b/cui/cui.go index c3ee14a..1ed29d7 100644 --- a/cui/cui.go +++ b/cui/cui.go @@ -102,6 +102,7 @@ func Getch() rune { func GetLine(prefix string) (string, error) { termios.SetLineMode() + defer termios.SetCharMode() reader := bufio.NewReader(os.Stdin) fmt.Print(prefix) @@ -110,7 +111,6 @@ func GetLine(prefix string) (string, error) { return "", err } - termios.SetCharMode() return text[:len(text)-1], nil } diff --git a/gemini/gemini.go b/gemini/gemini.go index db02ca5..482cfae 100644 --- a/gemini/gemini.go +++ b/gemini/gemini.go @@ -78,7 +78,7 @@ func (t *TofuDigest) Match(host, localCert string, cState *tls.ConnectionState) return fmt.Errorf("EXP") } - if err := cert.VerifyHostname(host); err != nil { + if err := cert.VerifyHostname(host); err != nil && cert.Subject.CommonName != host { return fmt.Errorf("Certificate error: %s", err) } @@ -107,7 +107,7 @@ func (t *TofuDigest) newCert(host string, cState *tls.ConnectionState) error { continue } - if err := cert.VerifyHostname(host); err != nil { + if err := cert.VerifyHostname(host); err != nil && cert.Subject.CommonName != host { reasons.WriteString(fmt.Sprintf("Cert [%d] hostname does not match", index+1)) continue } @@ -361,6 +361,7 @@ func parseGemini(b, currentUrl string) (string, []string) { links := make([]string, 0, 10) inPreBlock := false + spacer := " " outputIndex := 0 for i, ln := range splitContent { @@ -371,7 +372,7 @@ func parseGemini(b, currentUrl string) (string, []string) { alt := strings.TrimSpace(ln) if len(alt) > 3 { alt = strings.TrimSpace(alt[3:]) - splitContent[outputIndex] = fmt.Sprintf("[ %s ]", alt) + splitContent[outputIndex] = fmt.Sprintf("%s[ALT][ %s ]", spacer, alt) outputIndex++ } } else if isPreBlockDeclaration { @@ -401,7 +402,12 @@ func parseGemini(b, currentUrl string) (string, []string) { if inPreBlock && (BlockBehavior == "alt" || BlockBehavior == "neither") { continue } - splitContent[outputIndex] = ln + var leader, tail string = "", "" + if len(ln) > 0 && ln[0] == '#' { + leader = "\033[1m" + tail = "\033[0m" + } + splitContent[outputIndex] = fmt.Sprintf("%s%s%s%s", spacer, leader, ln, tail) outputIndex++ } } diff --git a/gopher/gopher.go b/gopher/gopher.go index fbe579f..adf36e9 100644 --- a/gopher/gopher.go +++ b/gopher/gopher.go @@ -144,7 +144,8 @@ func parseMap(text string) (string, []string) { } else { link := buildLink(line[2], line[3], string(line[0][0]), line[1]) links = append(links, link) - linktext := fmt.Sprintf("(%s) %2d %s", getType(string(line[0][0])), len(links), title) + linkNum := fmt.Sprintf("[%d]",len(links)) + linktext := fmt.Sprintf("%s %5s %s", getType(string(line[0][0])), linkNum, title) splitContent[i] = linktext } } diff --git a/help.go b/help.go new file mode 100644 index 0000000..d42e75c --- /dev/null +++ b/help.go @@ -0,0 +1,31 @@ +package main + +// ERRS maps commands to their syntax error message +var ERRS = map[string]string{ + "A": "`a [target] [name...]`", + "ADD": "`add [target] [name...]`", + "D": "`d [bookmark-id]`", + "DELETE": "`delete [bookmark-id]`", + "B": "`b [[bookmark-id]]`", + "BOOKMARKS": "`bookmarks [[bookmark-id]]`", + "C": "`c [link_id]` or `c [setting]`", + "CHECK": "`check [link_id]` or `check [setting]`", + "H": "`h`", + "HOME": "`home`", + "J": "`j [[history_position]]`", + "JUMP": "`jump [[history_position]]`", + "P": "`p [host]`", + "PURGE": "`purge [host]`", + "Q": "`q`", + "QUIT": "`quit`", + "R": "`r`", + "RELOAD": "`reload`", + "SEARCH": "`search [[keyword(s)...]]`", + "S": "`s [setting] [value]`", + "SET": "`set [setting] [value]`", + "W": "`w [target]`", + "WRITE": "`write [target]`", + "VERSION": "`version`", + "?": "`? [[command]]`", + "HELP": "`help [[command]]`", +} diff --git a/http/http_render.go b/http/http_render.go index f68ee2d..58a8813 100644 --- a/http/http_render.go +++ b/http/http_render.go @@ -33,7 +33,7 @@ func Visit(webmode, url string, width int) (Page, error) { return Page{}, fmt.Errorf("Invalid webmode setting") } c, err := exec.Command(webmode, "-dump", w, fmt.Sprintf("%d", width), url).Output() - if err != nil { + if err != nil && c == nil { return Page{}, err } return parseLinks(string(c)), nil diff --git a/main.go b/main.go index 82c27da..bee9c41 100644 --- a/main.go +++ b/main.go @@ -35,8 +35,7 @@ import ( "tildegit.org/sloum/bombadillo/gemini" ) -var version string -var build string +var version string = "2.3.3" var bombadillo *client var helplocation string = "gopher://bombadillo.colorfield.space:70/1/user-guide.map" @@ -211,7 +210,7 @@ func main() { flag.Usage = printHelp flag.Parse() if *getVersion { - fmt.Printf("Bombadillo %s - build %s\n", version, build) + fmt.Printf("Bombadillo %s\n", version) os.Exit(0) } args := flag.Args() diff --git a/page.go b/page.go index ad44ab2..ac31168 100644 --- a/page.go +++ b/page.go @@ -72,11 +72,17 @@ func (p *Page) WrapContent(width int, color bool) { } width = min(width, 100) counter := 0 - spacer := " " + spacer := "" var content strings.Builder var esc strings.Builder escape := false content.Grow(len(p.RawContent)) + + if p.Location.Mime == "1" { // gopher document + spacer = " " + } else if strings.HasSuffix(p.Location.Mime, "gemini") { //gemini document + spacer = " " + } for _, ch := range []rune(p.RawContent) { if escape { if color { @@ -91,8 +97,8 @@ func (p *Page) WrapContent(width int, color bool) { } continue } - if ch == '\n' { - content.WriteRune(ch) + if ch == '\n' || ch == '\u0085' || ch == '\u2028' || ch == '\u2029' { + content.WriteRune('\n') counter = 0 } else if ch == '\t' { if counter+4 < width { @@ -125,10 +131,8 @@ func (p *Page) WrapContent(width int, color bool) { } else { content.WriteRune('\n') counter = 0 - if p.Location.Mime == "1" { - content.WriteString(spacer) - counter += len(spacer) - } + content.WriteString(spacer) + counter += len(spacer) content.WriteRune(ch) counter++ } diff --git a/page_test.go b/page_test.go index ef41a80..b5e8f94 100644 --- a/page_test.go +++ b/page_test.go @@ -61,6 +61,26 @@ func Test_WrapContent_Wrapped_Line_Length(t *testing.T) { false, }, }, + { + "Unicode line endings that should not wrap", + "LF\u000A" + + "CR+LF\u000D\u000A" + + "NEL\u0085" + + "LS\u2028" + + "PS\u2029", + []string{ + "LF", + "CR+LF", + "NEL", + "LS", + "PS", + "", + }, + args{ + 10, + false, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pages.go b/pages.go index 9fa6606..e4ef964 100644 --- a/pages.go +++ b/pages.go @@ -38,7 +38,7 @@ func (p *Pages) NavigateHistory(qty int) error { } // Add gets passed a Page, which gets added to the history -// arrayr. Add also updates the current length and position +// array. Add also updates the current length and position // of the Pages struct to which it belongs. Add also shifts // off array items if necessary. func (p *Pages) Add(pg Page) { @@ -92,6 +92,18 @@ func (p *Pages) Render(termHeight, termWidth int, color bool) []string { return p.History[p.Position].WrappedContent[pos:] } +func (p *Pages) CopyHistory(pos int) error { + if p.Length < 2 || pos > p.Position { + return fmt.Errorf("There are not enough history locations available") + } + if pos < 0 { + pos = p.Position-1 + } + + p.Add(p.History[pos]) + return nil +} + //------------------------------------------------\\ // + + + F U N C T I O N S + + + \\ //--------------------------------------------------\\