Merge pull request 'Release 2.3.3 to master' (#204) from develop into master
Reviewed-on: #204
This commit is contained in:
commit
757305db66
|
@ -1,2 +1,3 @@
|
||||||
bombadillo
|
bombadillo
|
||||||
*.asciinema
|
*.asciinema
|
||||||
|
*.swp
|
||||||
|
|
|
@ -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.
|
||||||
|
|
10
Makefile
10
Makefile
|
@ -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
|
||||||
|
|
24
bombadillo.1
24
bombadillo.1
|
@ -33,7 +33,7 @@ Gopher is the default protocol for \fBbombadillo\fP. Any textual item types will
|
||||||
.TP
|
.TP
|
||||||
.B
|
.B
|
||||||
gemini
|
gemini
|
||||||
Gemini is supported, but as a new protocol with an incomplete specification, features may change over time. At present Bombadillo supports TLS with a trust on first use certificate pinning system (similar to SSH). Client certificates are also supported as a configurable option. Gemini maps and other text types are rendered in the browser and non-text types will be downloaded.
|
Gemini is supported, but as a new protocol with an incomplete specification, features may change over time. At present Bombadillo supports TLS with a trust on first use certificate pinning system (similar to SSH). Gemini maps and other text types are rendered in the browser and non-text types will be downloaded.
|
||||||
.TP
|
.TP
|
||||||
.B
|
.B
|
||||||
finger
|
finger
|
||||||
|
@ -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
|
||||||
|
@ -257,12 +269,8 @@ theme
|
||||||
Can toggle between visual modes. Valid values are \fInormal\fP, \fIcolor\fP, and \fIinverse\fP. When set to inverse, the normal mode colors are inverted. Both normal and inverse modes filter out terminal escape sequences. When set to color, Bombadillo will render terminal escape sequences representing colors when it finds them in documents.
|
Can toggle between visual modes. Valid values are \fInormal\fP, \fIcolor\fP, and \fIinverse\fP. When set to inverse, the normal mode colors are inverted. Both normal and inverse modes filter out terminal escape sequences. When set to color, Bombadillo will render terminal escape sequences representing colors when it finds them in documents.
|
||||||
.TP
|
.TP
|
||||||
.B
|
.B
|
||||||
tlscertificate
|
timeout
|
||||||
A path to a tls certificate file on a user's local filesystem. Defaults to NULL. Both \fItlscertificate\fP and \fItlskey\fP must be set for client certificates to work in gemini.
|
The number of seconds after which connections to gopher or gemini servers should time out if the server has not responded.
|
||||||
.TP
|
|
||||||
.B
|
|
||||||
tlskey
|
|
||||||
A path to a tls key that pairs with the tlscertificate setting, on a user's local filesystem. Defaults to NULL. Both \fItlskey\fP and \fItlscertificate\fP must be set for client certificates to work in gemini.
|
|
||||||
.TP
|
.TP
|
||||||
.B
|
.B
|
||||||
webmode
|
webmode
|
||||||
|
|
119
client.go
119
client.go
|
@ -122,8 +122,8 @@ func (c *client) Draw() {
|
||||||
} else {
|
} else {
|
||||||
for i := 0; i < c.Height-3; i++ {
|
for i := 0; i < c.Height-3; i++ {
|
||||||
if i < len(pageContent) {
|
if i < len(pageContent) {
|
||||||
screen.WriteString(pageContent[i])
|
|
||||||
screen.WriteString("\033[0K")
|
screen.WriteString("\033[0K")
|
||||||
|
screen.WriteString(pageContent[i])
|
||||||
screen.WriteString("\n")
|
screen.WriteString("\n")
|
||||||
} else {
|
} else {
|
||||||
screen.WriteString("\033[0K")
|
screen.WriteString("\033[0K")
|
||||||
|
@ -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) {
|
||||||
|
@ -451,10 +477,10 @@ func (c *client) doCommandAs(action string, values []string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.Options[values[0]] = lowerCaseOpt(values[0], val)
|
c.Options[values[0]] = lowerCaseOpt(values[0], val)
|
||||||
if values[0] == "tlskey" || values[0] == "tlscertificate" {
|
if values[0] == "geminiblocks" {
|
||||||
c.Certs.LoadCertificate(c.Options["tlscertificate"], c.Options["tlskey"])
|
|
||||||
} else if values[0] == "geminiblocks" {
|
|
||||||
gemini.BlockBehavior = c.Options[values[0]]
|
gemini.BlockBehavior = c.Options[values[0]]
|
||||||
|
} else if values[0] == "timeout" {
|
||||||
|
updateTimeouts(c.Options[values[0]])
|
||||||
} else if values[0] == "configlocation" {
|
} else if values[0] == "configlocation" {
|
||||||
c.SetMessage("Cannot set READ ONLY setting 'configlocation'", true)
|
c.SetMessage("Cannot set READ ONLY setting 'configlocation'", true)
|
||||||
c.DrawMessage()
|
c.DrawMessage()
|
||||||
|
@ -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"))
|
||||||
|
@ -999,6 +1035,13 @@ func (c *client) handleGemini(u Url) {
|
||||||
if strings.Replace(lowerRedirect, lowerOriginal, "", 1) == "/" {
|
if strings.Replace(lowerRedirect, lowerOriginal, "", 1) == "/" {
|
||||||
c.Visit(capsule.Content)
|
c.Visit(capsule.Content)
|
||||||
} else {
|
} else {
|
||||||
|
if !strings.Contains(capsule.Content, "://") {
|
||||||
|
lnk, lnkErr := gemini.HandleRelativeUrl(capsule.Content, u.Full)
|
||||||
|
if lnkErr == nil {
|
||||||
|
capsule.Content = lnk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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()
|
c.DrawMessage()
|
||||||
ch := cui.Getch()
|
ch := cui.Getch()
|
||||||
|
@ -1195,3 +1238,23 @@ 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 {
|
||||||
|
sec, err := strconv.Atoi(timeoutString)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
timeout := time.Duration(sec) * time.Second
|
||||||
|
|
||||||
|
gopher.Timeout = timeout
|
||||||
|
gemini.TlsTimeout = timeout
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -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}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -54,9 +54,8 @@ var defaultOptions = map[string]string{
|
||||||
"showimages": "true",
|
"showimages": "true",
|
||||||
"telnetcommand": "telnet",
|
"telnetcommand": "telnet",
|
||||||
"theme": "normal", // "normal", "inverted", "color"
|
"theme": "normal", // "normal", "inverted", "color"
|
||||||
"tlscertificate": "",
|
"timeout": "15", // connection timeout for gopher/gemini in seconds
|
||||||
"tlskey": "",
|
"webmode": "none", // "none", "gui", "lynx", "w3m", "elinks"
|
||||||
"webmode": "none", // "none", "gui", "lynx", "w3m", "elinks"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// homePath will return the path to your home directory as a string
|
// homePath will return the path to your home directory as a string
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -22,24 +23,15 @@ type Capsule struct {
|
||||||
|
|
||||||
type TofuDigest struct {
|
type TofuDigest struct {
|
||||||
certs map[string]string
|
certs map[string]string
|
||||||
ClientCert tls.Certificate
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var BlockBehavior = "block"
|
var BlockBehavior string = "block"
|
||||||
|
var TlsTimeout time.Duration = time.Duration(15) * time.Second
|
||||||
|
|
||||||
//------------------------------------------------\\
|
//------------------------------------------------\\
|
||||||
// + + + R E C E I V E R S + + + \\
|
// + + + R E C E I V E R S + + + \\
|
||||||
//--------------------------------------------------\\
|
//--------------------------------------------------\\
|
||||||
|
|
||||||
func (t *TofuDigest) LoadCertificate(cert, key string) {
|
|
||||||
certificate, err := tls.LoadX509KeyPair(cert, key)
|
|
||||||
if err != nil {
|
|
||||||
t.ClientCert = tls.Certificate{}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.ClientCert = certificate
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TofuDigest) Purge(host string) error {
|
func (t *TofuDigest) Purge(host string) error {
|
||||||
host = strings.ToLower(host)
|
host = strings.ToLower(host)
|
||||||
if host == "*" {
|
if host == "*" {
|
||||||
|
@ -86,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,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
|
||||||
}
|
}
|
||||||
|
@ -185,11 +177,7 @@ func Retrieve(host, port, resource string, td *TofuDigest) (string, error) {
|
||||||
InsecureSkipVerify: true,
|
InsecureSkipVerify: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
conf.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
|
conn, err := tls.DialWithDialer(&net.Dialer{Timeout: TlsTimeout}, "tcp", addr, conf)
|
||||||
return &td.ClientCert, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := tls.Dial("tcp", addr, conf)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("TLS Dial Error: %s", err.Error())
|
return "", fmt.Errorf("TLS Dial Error: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
@ -282,7 +270,7 @@ func Fetch(host, port, resource string, td *TofuDigest) ([]byte, error) {
|
||||||
case 5:
|
case 5:
|
||||||
return make([]byte, 0), fmt.Errorf("[5] Permanent Failure.")
|
return make([]byte, 0), fmt.Errorf("[5] Permanent Failure.")
|
||||||
case 6:
|
case 6:
|
||||||
return make([]byte, 0), fmt.Errorf("[6] Client Certificate Required")
|
return make([]byte, 0), fmt.Errorf("[6] Client Certificate Required (Unsupported)")
|
||||||
default:
|
default:
|
||||||
return make([]byte, 0), fmt.Errorf("Invalid response status from server")
|
return make([]byte, 0), fmt.Errorf("Invalid response status from server")
|
||||||
}
|
}
|
||||||
|
@ -331,6 +319,9 @@ func Visit(host, port, resource string, td *TofuDigest) (Capsule, error) {
|
||||||
case 2:
|
case 2:
|
||||||
mimeAndCharset := strings.Split(header[1], ";")
|
mimeAndCharset := strings.Split(header[1], ";")
|
||||||
meta = mimeAndCharset[0]
|
meta = mimeAndCharset[0]
|
||||||
|
if meta == "" {
|
||||||
|
meta = "text/gemini"
|
||||||
|
}
|
||||||
minMajMime := strings.Split(meta, "/")
|
minMajMime := strings.Split(meta, "/")
|
||||||
if len(minMajMime) < 2 {
|
if len(minMajMime) < 2 {
|
||||||
return capsule, fmt.Errorf("Improperly formatted mimetype received from server")
|
return capsule, fmt.Errorf("Improperly formatted mimetype received from server")
|
||||||
|
@ -359,7 +350,7 @@ func Visit(host, port, resource string, td *TofuDigest) (Capsule, error) {
|
||||||
case 5:
|
case 5:
|
||||||
return capsule, fmt.Errorf("[5] Permanent Failure. %s", header[1])
|
return capsule, fmt.Errorf("[5] Permanent Failure. %s", header[1])
|
||||||
case 6:
|
case 6:
|
||||||
return capsule, fmt.Errorf("[6] Client Certificate Required")
|
return capsule, fmt.Errorf("[6] Client Certificate Required (Unsupported)")
|
||||||
default:
|
default:
|
||||||
return capsule, fmt.Errorf("Invalid response status from server")
|
return capsule, fmt.Errorf("Invalid response status from server")
|
||||||
}
|
}
|
||||||
|
@ -370,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 {
|
||||||
|
@ -380,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 {
|
||||||
|
@ -399,7 +391,7 @@ func parseGemini(b, currentUrl string) (string, []string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Index(link, "://") < 0 {
|
if strings.Index(link, "://") < 0 {
|
||||||
link, _ = handleRelativeUrl(link, currentUrl)
|
link, _ = HandleRelativeUrl(link, currentUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
links = append(links, link)
|
links = append(links, link)
|
||||||
|
@ -410,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++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -418,7 +415,7 @@ func parseGemini(b, currentUrl string) (string, []string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleRelativeUrl provides link completion
|
// handleRelativeUrl provides link completion
|
||||||
func handleRelativeUrl(relLink, current string) (string, error) {
|
func HandleRelativeUrl(relLink, current string) (string, error) {
|
||||||
base, err := url.Parse(current)
|
base, err := url.Parse(current)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return relLink, err
|
return relLink, err
|
||||||
|
@ -444,5 +441,5 @@ func MakeCapsule() Capsule {
|
||||||
}
|
}
|
||||||
|
|
||||||
func MakeTofuDigest() TofuDigest {
|
func MakeTofuDigest() TofuDigest {
|
||||||
return TofuDigest{make(map[string]string), tls.Certificate{}}
|
return TofuDigest{make(map[string]string)}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,8 @@ var types = map[string]string{
|
||||||
"T": "TEL",
|
"T": "TEL",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var Timeout time.Duration = time.Duration(15) * time.Second
|
||||||
|
|
||||||
//------------------------------------------------\\
|
//------------------------------------------------\\
|
||||||
// + + + F U N C T I O N S + + + \\
|
// + + + F U N C T I O N S + + + \\
|
||||||
//--------------------------------------------------\\
|
//--------------------------------------------------\\
|
||||||
|
@ -49,7 +51,6 @@ var types = map[string]string{
|
||||||
// be better.
|
// be better.
|
||||||
func Retrieve(host, port, resource string) ([]byte, error) {
|
func Retrieve(host, port, resource string) ([]byte, error) {
|
||||||
nullRes := make([]byte, 0)
|
nullRes := make([]byte, 0)
|
||||||
timeOut := time.Duration(5) * time.Second
|
|
||||||
|
|
||||||
if host == "" || port == "" {
|
if host == "" || port == "" {
|
||||||
return nullRes, errors.New("Incomplete request url")
|
return nullRes, errors.New("Incomplete request url")
|
||||||
|
@ -57,7 +58,7 @@ func Retrieve(host, port, resource string) ([]byte, error) {
|
||||||
|
|
||||||
addr := host + ":" + port
|
addr := host + ":" + port
|
||||||
|
|
||||||
conn, err := net.DialTimeout("tcp", addr, timeOut)
|
conn, err := net.DialTimeout("tcp", addr, Timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nullRes, err
|
return nullRes, err
|
||||||
}
|
}
|
||||||
|
@ -143,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]]`",
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// +build darwin
|
// This will build for osx without a build tag based on the filename
|
||||||
|
|
||||||
package http
|
package http
|
||||||
|
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
// +build linux
|
|
||||||
|
|
||||||
package http
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
)
|
|
||||||
|
|
||||||
// OpenInBrowser checks for the presence of a display server
|
|
||||||
// and environment variables indicating a gui is present. If found
|
|
||||||
// then xdg-open is called on a url to open said url in the default
|
|
||||||
// gui web browser for the system
|
|
||||||
func OpenInBrowser(url string) (string, error) {
|
|
||||||
disp := os.Getenv("DISPLAY")
|
|
||||||
wayland := os.Getenv("WAYLAND_DISPLAY")
|
|
||||||
_, err := exec.LookPath("Xorg")
|
|
||||||
if disp == "" && wayland == "" && err != nil {
|
|
||||||
return "", fmt.Errorf("No gui is available, check 'webmode' setting")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use start rather than run or output in order
|
|
||||||
// to release the process and not block
|
|
||||||
err = exec.Command("xdg-open", url).Start()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return "Opened in system default web browser", nil
|
|
||||||
}
|
|
|
@ -1,11 +1,30 @@
|
||||||
// +build !linux
|
// +build !darwin,!windows
|
||||||
// +build !darwin
|
|
||||||
// +build !windows
|
|
||||||
|
|
||||||
package http
|
package http
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OpenInBrowser checks for the presence of a display server
|
||||||
|
// and environment variables indicating a gui is present. If found
|
||||||
|
// then xdg-open is called on a url to open said url in the default
|
||||||
|
// gui web browser for the system
|
||||||
func OpenInBrowser(url string) (string, error) {
|
func OpenInBrowser(url string) (string, error) {
|
||||||
return "", fmt.Errorf("Unsupported os for 'webmode' 'gui' setting")
|
disp := os.Getenv("DISPLAY")
|
||||||
|
wayland := os.Getenv("WAYLAND_DISPLAY")
|
||||||
|
_, err := exec.LookPath("Xorg")
|
||||||
|
if disp == "" && wayland == "" && err != nil {
|
||||||
|
return "", fmt.Errorf("No gui is available, check 'webmode' setting")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use start rather than run or output in order
|
||||||
|
// to release the process and not block
|
||||||
|
err = exec.Command("xdg-open", url).Start()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return "Opened in system default web browser", nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// +build windows
|
// This will only build for windows based on the filename
|
||||||
|
// no build tag required
|
||||||
package http
|
package http
|
||||||
|
|
||||||
import "os/exec"
|
import "os/exec"
|
||||||
|
|
20
main.go
20
main.go
|
@ -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"
|
||||||
|
@ -82,6 +81,14 @@ func validateOpt(opt, val string) bool {
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opt == "timeout" {
|
||||||
|
_, err := strconv.Atoi(val)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,6 +133,8 @@ func loadConfig() {
|
||||||
bombadillo.Options[lowerkey] = v.Value
|
bombadillo.Options[lowerkey] = v.Value
|
||||||
if lowerkey == "geminiblocks" {
|
if lowerkey == "geminiblocks" {
|
||||||
gemini.BlockBehavior = v.Value
|
gemini.BlockBehavior = v.Value
|
||||||
|
} else if lowerkey == "timeout" {
|
||||||
|
updateTimeouts(v.Value)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
bombadillo.Options[lowerkey] = defaultOptions[lowerkey]
|
bombadillo.Options[lowerkey] = defaultOptions[lowerkey]
|
||||||
|
@ -143,8 +152,8 @@ func loadConfig() {
|
||||||
if len(vals) < 2 {
|
if len(vals) < 2 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ts, err := strconv.ParseInt(vals[1], 10, 64)
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
ts, err := strconv.ParseInt(vals[1], 10, 64)
|
||||||
if err != nil || now.Unix() > ts {
|
if err != nil || now.Unix() > ts {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -158,9 +167,6 @@ func loadConfig() {
|
||||||
func initClient() {
|
func initClient() {
|
||||||
bombadillo = MakeClient(" ((( Bombadillo ))) ")
|
bombadillo = MakeClient(" ((( Bombadillo ))) ")
|
||||||
loadConfig()
|
loadConfig()
|
||||||
if bombadillo.Options["tlscertificate"] != "" && bombadillo.Options["tlskey"] != "" {
|
|
||||||
bombadillo.Certs.LoadCertificate(bombadillo.Options["tlscertificate"], bombadillo.Options["tlskey"])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// In the event of specific signals, ensure the display is shown correctly.
|
// In the event of specific signals, ensure the display is shown correctly.
|
||||||
|
@ -204,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()
|
||||||
|
|
19
page.go
19
page.go
|
@ -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,11 +131,10 @@ 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++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_WrapContent_Wrapped_Line_Length(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
WrappedContent []string
|
||||||
|
RawContent string
|
||||||
|
Links []string
|
||||||
|
Location Url
|
||||||
|
ScrollPosition int
|
||||||
|
FoundLinkLines []int
|
||||||
|
SearchTerm string
|
||||||
|
SearchIndex int
|
||||||
|
FileType string
|
||||||
|
WrapWidth int
|
||||||
|
Color bool
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
width int
|
||||||
|
color bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a Url for use by the MakePage function
|
||||||
|
url, _ := MakeUrl("gemini://rawtext.club")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expects []string
|
||||||
|
args args
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"Short line that doesn't wrap",
|
||||||
|
"0123456789\n",
|
||||||
|
[]string{
|
||||||
|
"0123456789",
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
10,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Long line wrapped to 10 columns",
|
||||||
|
"0123456789 123456789 123456789 123456789 123456789\n",
|
||||||
|
[]string{
|
||||||
|
"0123456789",
|
||||||
|
" 123456789",
|
||||||
|
" 123456789",
|
||||||
|
" 123456789",
|
||||||
|
" 123456789",
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
10,
|
||||||
|
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) {
|
||||||
|
p := MakePage(url, tt.input, []string{""})
|
||||||
|
p.WrapContent(tt.args.width-1, tt.args.color)
|
||||||
|
if !reflect.DeepEqual(p.WrappedContent, tt.expects) {
|
||||||
|
t.Errorf("Test failed - %s\nexpects %s\nactual %s", tt.name, tt.expects, p.WrappedContent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
14
pages.go
14
pages.go
|
@ -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 + + + \\
|
||||||
//--------------------------------------------------\\
|
//--------------------------------------------------\\
|
||||||
|
|
Loading…
Reference in New Issue