Merge pull request 'release2.3.3 -> develop' (#203) from release2.3.3 into develop

Reviewed-on: #203
This commit is contained in:
Sloom Sloum Sluom IV 2020-11-20 04:26:34 +00:00
commit a8456f7e98
17 changed files with 181 additions and 58 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
bombadillo bombadillo
*.asciinema *.asciinema
*.swp

View File

@ -25,10 +25,10 @@ Changes are implemented to the default branch when:
### Process for introducing a new change ### 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. 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. The PR is reviewed.
1. If the PR is approved, it is merged. 1. If the PR is approved, it is merged.
1. The version number is incremented, along with any other release activity. 1. The version number is incremented, along with any other release activity.

View File

@ -13,17 +13,9 @@ test : GOCMD := go1.11.13
# %:z - so settle for %z. # %:z - so settle for %z.
BUILD_TIME := ${shell date "+%Y-%m-%dT%H:%M%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 .PHONY: build
build: build:
${GOCMD} build ${LDFLAGS} -o ${BINARY} ${GOCMD} build -o ${BINARY}
.PHONY: install .PHONY: install
install: install-bin install-man install-desktop clean install: install-bin install-man install-desktop clean

View File

@ -1 +1 @@
2.3.2 2.3.3

View File

@ -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. Navigates to the document set by the \fIhomeurl\fP setting. \fIh\fP can be entered, rather than the full \fIhome\fP.
.TP .TP
.B .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 * purge *
Deletes all pinned gemini server certificates. \fIp\fP can be used instead of the full \fIpurge\fP. Deletes all pinned gemini server certificates. \fIp\fP can be used instead of the full \fIpurge\fP.
.TP .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. Sets the value for a given configuration setting. \fIs\fP can be used instead of the full \fIset\fP.
.TP .TP
.B .B
version
Shows the current Bombadillo version number.
.TP
.B
write . 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. 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 .TP
@ -246,7 +258,7 @@ The path to the directory that \fBbombadillo\fP should write files to. This must
.TP .TP
.B .B
searchengine 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 .TP
.B .B
telnetcommand telnetcommand
@ -259,6 +271,8 @@ Can toggle between visual modes. Valid values are \fInormal\fP, \fIcolor\fP, and
.B .B
timeout timeout
The number of seconds after which connections to gopher or gemini servers should time out if the server has not responded. The number of seconds after which connections to gopher or gemini servers should time out if the server has not responded.
.TP
.B
webmode 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. 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.

View File

@ -258,6 +258,7 @@ func (c *client) TakeControlInput() {
} }
err = c.NextSearchItem(0) err = c.NextSearchItem(0)
if err != nil { if err != nil {
c.PageState.History[c.PageState.Position].WrapContent(c.Width-1,(c.Options["theme"] == "color"))
c.Draw() c.Draw()
} }
case ':', ' ': case ':', ' ':
@ -344,23 +345,41 @@ func (c *client) simpleCommand(action string) {
case "SEARCH": case "SEARCH":
c.search("", "", "?") c.search("", "", "?")
case "HELP", "?": 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: default:
c.SetMessage(fmt.Sprintf("Unknown action %q", action), true) c.SetMessage(syntaxErrorMessage(action), true)
c.DrawMessage() c.DrawMessage()
} }
} }
func (c *client) doCommand(action string, values []string) { 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 { switch action {
case "CHECK", "C": case "C", "CHECK":
c.displayConfigValue(values[0]) 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": case "PURGE", "P":
err := c.Certs.Purge(values[0]) err := c.Certs.Purge(values[0])
if err != nil { if err != nil {
@ -403,26 +422,23 @@ func (c *client) doCommand(action string, values []string) {
fn = "index" fn = "index"
} }
c.saveFile(u, fn) c.saveFile(u, fn)
default: default:
c.SetMessage(fmt.Sprintf("Unknown action %q", action), true) c.SetMessage(syntaxErrorMessage(action), true)
c.DrawMessage() c.DrawMessage()
} }
} }
func (c *client) doCommandAs(action string, values []string) { 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 { switch action {
case "ADD", "A": 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) msg, err := c.BookMarks.Add(values)
if err != nil { if err != nil {
c.SetMessage(err.Error(), true) c.SetMessage(err.Error(), true)
@ -441,8 +457,18 @@ func (c *client) doCommandAs(action string, values []string) {
c.Draw() c.Draw()
} }
case "SEARCH": case "SEARCH":
if len(values) < 2 {
c.SetMessage(syntaxErrorMessage(action), true)
c.DrawMessage()
return
}
c.search(strings.Join(values, " "), "", "") c.search(strings.Join(values, " "), "", "")
case "SET", "S": case "SET", "S":
if len(values) < 2 {
c.SetMessage(syntaxErrorMessage(action), true)
c.DrawMessage()
return
}
if _, ok := c.Options[values[0]]; ok { if _, ok := c.Options[values[0]]; ok {
val := strings.Join(values[1:], " ") val := strings.Join(values[1:], " ")
if !validateOpt(values[0], val) { 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.SetMessage(fmt.Sprintf("Unable to set %s, it does not exist", values[0]), true)
c.DrawMessage() c.DrawMessage()
default: default:
c.SetMessage(fmt.Sprintf("Unknown command structure"), true) c.SetMessage(syntaxErrorMessage(action), true)
c.DrawMessage() c.DrawMessage()
} }
} }
@ -523,7 +549,7 @@ func (c *client) doLinkCommandAs(action, target string, values []string) {
out = append(out, values...) out = append(out, values...)
c.doCommandAs(action, out) c.doCommandAs(action, out)
default: default:
c.SetMessage(fmt.Sprintf("Unknown command structure"), true) c.SetMessage(syntaxErrorMessage(action), true)
c.DrawMessage() c.DrawMessage()
} }
} }
@ -630,6 +656,15 @@ func (c *client) doLinkCommand(action, target string) {
link := links[num] link := links[num]
c.SetMessage(fmt.Sprintf("[%d] %s", num+1, link), false) c.SetMessage(fmt.Sprintf("[%d] %s", num+1, link), false)
c.DrawMessage() 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": case "WRITE", "W":
links := c.PageState.History[c.PageState.Position].Links links := c.PageState.History[c.PageState.Position].Links
if len(links) < num || num < 1 { if len(links) < num || num < 1 {
@ -655,7 +690,7 @@ func (c *client) doLinkCommand(action, target string) {
} }
c.saveFile(u, fn) c.saveFile(u, fn)
default: default:
c.SetMessage("Unknown command structure", true) c.SetMessage(syntaxErrorMessage(action), true)
c.DrawMessage() c.DrawMessage()
} }
@ -977,6 +1012,7 @@ func (c *client) handleGemini(u Url) {
case 2: case 2:
// Success // Success
if capsule.MimeMaj == "text" || (c.Options["showimages"] == "true" && capsule.MimeMaj == "image") { if capsule.MimeMaj == "text" || (c.Options["showimages"] == "true" && capsule.MimeMaj == "image") {
u.Mime = capsule.MimeMin
pg := MakePage(u, capsule.Content, capsule.Links) pg := MakePage(u, capsule.Content, capsule.Links)
pg.FileType = capsule.MimeMaj pg.FileType = capsule.MimeMaj
pg.WrapContent(c.Width-1, (c.Options["theme"] == "color")) pg.WrapContent(c.Width-1, (c.Options["theme"] == "color"))
@ -1203,6 +1239,13 @@ func findAvailableFileName(fpath, fname string) (string, error) {
return savePath, nil 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 { func updateTimeouts(timeoutString string) error {
sec, err := strconv.Atoi(timeoutString) sec, err := strconv.Atoi(timeoutString)
if err != nil { if err != nil {

View File

@ -72,7 +72,7 @@ func (s *scanner) scanText() Token {
"S", "SET", "R", "RELOAD", "SEARCH", "S", "SET", "R", "RELOAD", "SEARCH",
"Q", "QUIT", "B", "BOOKMARKS", "H", "Q", "QUIT", "B", "BOOKMARKS", "H",
"HOME", "?", "HELP", "C", "CHECK", "HOME", "?", "HELP", "C", "CHECK",
"P", "PURGE": "P", "PURGE", "JUMP", "J", "VERSION":
return Token{Action, capInput} return Token{Action, capInput}
} }

View File

@ -94,10 +94,10 @@ func (p *Parser) parseAction() (*Command, error) {
case Value: case Value:
cm.Target = t.val cm.Target = t.val
cm.Type = DOLINK cm.Type = DOLINK
case Word: case Word, Action:
cm.Value = append(cm.Value, t.val) cm.Value = append(cm.Value, t.val)
cm.Type = DO cm.Type = DO
case Action, Whitespace: case Whitespace:
return nil, fmt.Errorf("Found %q (%d), expected value", t.val, t.kind) return nil, fmt.Errorf("Found %q (%d), expected value", t.val, t.kind)
} }
t = p.scan() t = p.scan()

View File

@ -102,6 +102,7 @@ func Getch() rune {
func GetLine(prefix string) (string, error) { func GetLine(prefix string) (string, error) {
termios.SetLineMode() termios.SetLineMode()
defer termios.SetCharMode()
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
fmt.Print(prefix) fmt.Print(prefix)
@ -110,7 +111,6 @@ func GetLine(prefix string) (string, error) {
return "", err return "", err
} }
termios.SetCharMode()
return text[:len(text)-1], nil return text[:len(text)-1], nil
} }

View File

@ -78,7 +78,7 @@ func (t *TofuDigest) Match(host, localCert string, cState *tls.ConnectionState)
return fmt.Errorf("EXP") 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) return fmt.Errorf("Certificate error: %s", err)
} }
@ -107,7 +107,7 @@ func (t *TofuDigest) newCert(host string, cState *tls.ConnectionState) error {
continue 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)) reasons.WriteString(fmt.Sprintf("Cert [%d] hostname does not match", index+1))
continue continue
} }
@ -361,6 +361,7 @@ func parseGemini(b, currentUrl string) (string, []string) {
links := make([]string, 0, 10) links := make([]string, 0, 10)
inPreBlock := false inPreBlock := false
spacer := " "
outputIndex := 0 outputIndex := 0
for i, ln := range splitContent { for i, ln := range splitContent {
@ -371,7 +372,7 @@ func parseGemini(b, currentUrl string) (string, []string) {
alt := strings.TrimSpace(ln) alt := strings.TrimSpace(ln)
if len(alt) > 3 { if len(alt) > 3 {
alt = strings.TrimSpace(alt[3:]) alt = strings.TrimSpace(alt[3:])
splitContent[outputIndex] = fmt.Sprintf("[ %s ]", alt) splitContent[outputIndex] = fmt.Sprintf("%s[ALT][ %s ]", spacer, alt)
outputIndex++ outputIndex++
} }
} else if isPreBlockDeclaration { } else if isPreBlockDeclaration {
@ -401,7 +402,12 @@ func parseGemini(b, currentUrl string) (string, []string) {
if inPreBlock && (BlockBehavior == "alt" || BlockBehavior == "neither") { if inPreBlock && (BlockBehavior == "alt" || BlockBehavior == "neither") {
continue 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++ outputIndex++
} }
} }

View File

@ -144,7 +144,8 @@ func parseMap(text string) (string, []string) {
} else { } else {
link := buildLink(line[2], line[3], string(line[0][0]), line[1]) link := buildLink(line[2], line[3], string(line[0][0]), line[1])
links = append(links, link) 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 splitContent[i] = linktext
} }
} }

31
help.go Normal file
View File

@ -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]]`",
}

View File

@ -33,7 +33,7 @@ func Visit(webmode, url string, width int) (Page, error) {
return Page{}, fmt.Errorf("Invalid webmode setting") return Page{}, fmt.Errorf("Invalid webmode setting")
} }
c, err := exec.Command(webmode, "-dump", w, fmt.Sprintf("%d", width), url).Output() 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 Page{}, err
} }
return parseLinks(string(c)), nil return parseLinks(string(c)), nil

View File

@ -35,8 +35,7 @@ import (
"tildegit.org/sloum/bombadillo/gemini" "tildegit.org/sloum/bombadillo/gemini"
) )
var version string var version string = "2.3.3"
var build string
var bombadillo *client var bombadillo *client
var helplocation string = "gopher://bombadillo.colorfield.space:70/1/user-guide.map" var helplocation string = "gopher://bombadillo.colorfield.space:70/1/user-guide.map"
@ -211,7 +210,7 @@ func main() {
flag.Usage = printHelp flag.Usage = printHelp
flag.Parse() flag.Parse()
if *getVersion { if *getVersion {
fmt.Printf("Bombadillo %s - build %s\n", version, build) fmt.Printf("Bombadillo %s\n", version)
os.Exit(0) os.Exit(0)
} }
args := flag.Args() args := flag.Args()

18
page.go
View File

@ -72,11 +72,17 @@ func (p *Page) WrapContent(width int, color bool) {
} }
width = min(width, 100) width = min(width, 100)
counter := 0 counter := 0
spacer := " " spacer := ""
var content strings.Builder var content strings.Builder
var esc strings.Builder var esc strings.Builder
escape := false escape := false
content.Grow(len(p.RawContent)) 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) { for _, ch := range []rune(p.RawContent) {
if escape { if escape {
if color { if color {
@ -91,8 +97,8 @@ func (p *Page) WrapContent(width int, color bool) {
} }
continue continue
} }
if ch == '\n' { if ch == '\n' || ch == '\u0085' || ch == '\u2028' || ch == '\u2029' {
content.WriteRune(ch) content.WriteRune('\n')
counter = 0 counter = 0
} else if ch == '\t' { } else if ch == '\t' {
if counter+4 < width { if counter+4 < width {
@ -125,10 +131,8 @@ func (p *Page) WrapContent(width int, color bool) {
} else { } else {
content.WriteRune('\n') content.WriteRune('\n')
counter = 0 counter = 0
if p.Location.Mime == "1" { content.WriteString(spacer)
content.WriteString(spacer) counter += len(spacer)
counter += len(spacer)
}
content.WriteRune(ch) content.WriteRune(ch)
counter++ counter++
} }

View File

@ -61,6 +61,26 @@ func Test_WrapContent_Wrapped_Line_Length(t *testing.T) {
false, 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {

View File

@ -38,7 +38,7 @@ func (p *Pages) NavigateHistory(qty int) error {
} }
// Add gets passed a Page, which gets added to the history // 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 // of the Pages struct to which it belongs. Add also shifts
// off array items if necessary. // off array items if necessary.
func (p *Pages) Add(pg Page) { 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:] 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 + + + \\ // + + + F U N C T I O N S + + + \\
//--------------------------------------------------\\ //--------------------------------------------------\\