Merge pull request 'Release to master 2.4.0' (#217) from develop into master

Reviewed-on: #217
This commit is contained in:
Sloom Sloum Sluom IV 2022-03-06 21:50:42 +00:00
commit 30e550c183
8 changed files with 138 additions and 37 deletions

View File

@ -22,7 +22,7 @@ install: install-bin install-man install-desktop clean
.PHONY: install-man .PHONY: install-man
install-man: bombadillo.1 install-man: bombadillo.1
gzip -k ./bombadillo.1 gzip -c ./bombadillo.1 > ./bombadillo.1.gz
install -d ${DESTDIR}${MAN1DIR} install -d ${DESTDIR}${MAN1DIR}
install -m 0644 ./bombadillo.1.gz ${DESTDIR}${MAN1DIR} install -m 0644 ./bombadillo.1.gz ${DESTDIR}${MAN1DIR}

View File

@ -111,6 +111,10 @@ Reload the current page (does not destroy forward history).
Quick navigation to the first 10 links on a page. The 0 key will navigate to the link numbered '10', all other numbers navigate to their matching link number. Quick navigation to the first 10 links on a page. The 0 key will navigate to the link numbered '10', all other numbers navigate to their matching link number.
.TP .TP
.B .B
U
Move up a level in the current url path. \fI/mydir/mysubdir/myfile.txt\fP would become \fI/mydir/mysubdir/\fP, and so on.
.TP
.B
u u
Scroll up an amount corresponding to 75% of your terminal window height in the current document. Scroll up an amount corresponding to 75% of your terminal window height in the current document.
.TP .TP
@ -253,6 +257,10 @@ 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. 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 .TP
.B .B
maxwidth
The number of characters at which lines should be wrapped. If this is bigger than the available terminal width, the full width of the terminal will be used. If a non-integer or an integer less than 10 is given, a default value will be used.
.TP
.B
savelocation savelocation
The path to the directory that \fBbombadillo\fP should write files to. This must be a valid filepath for the system, must be a directory, and must already exist. The path to the directory that \fBbombadillo\fP should write files to. This must be a valid filepath for the system, must be a directory, and must already exist.
.TP .TP

View File

@ -73,7 +73,7 @@ func (c *client) Draw() {
screen.WriteString("\033[0m") screen.WriteString("\033[0m")
screen.WriteString(c.TopBar.Render(c.Width, c.Options["theme"])) screen.WriteString(c.TopBar.Render(c.Width, c.Options["theme"]))
screen.WriteString("\n") screen.WriteString("\n")
pageContent := c.PageState.Render(c.Height, c.Width-1, (c.Options["theme"] == "color")) pageContent := c.PageState.Render(c.Height, c.Width-1, getMaxWidth(c.Options), (c.Options["theme"] == "color"))
var re *regexp.Regexp var re *regexp.Regexp
if c.Options["theme"] == "inverse" { if c.Options["theme"] == "inverse" {
screen.WriteString("\033[7m") screen.WriteString("\033[7m")
@ -146,42 +146,47 @@ func (c *client) TakeControlInput() {
switch input { switch input {
case '1', '2', '3', '4', '5', '6', '7', '8', '9', '0': case '1', '2', '3', '4', '5', '6', '7', '8', '9', '0':
// Quick link
if input == '0' { if input == '0' {
c.goToLink("10") c.goToLink("10")
} else { } else {
c.goToLink(string(input)) c.goToLink(string(input))
} }
case 'j': case 'j':
// scroll down one line // Scroll down one line
c.ClearMessage() c.ClearMessage()
c.Scroll(1) c.Scroll(1)
case 'k': case 'k':
// scroll up one line // Scroll up one line
c.ClearMessage() c.ClearMessage()
c.Scroll(-1) c.Scroll(-1)
case 'q': case 'q':
// quit bombadillo // Quit
cui.Exit(0, "") cui.Exit(0, "")
case 'g': case 'g':
// scroll to top // Scroll to top
c.ClearMessage() c.ClearMessage()
c.Scroll(-len(c.PageState.History[c.PageState.Position].WrappedContent)) c.Scroll(-len(c.PageState.History[c.PageState.Position].WrappedContent))
case 'G': case 'G':
// scroll to bottom // Scroll to bottom
c.ClearMessage() c.ClearMessage()
c.Scroll(len(c.PageState.History[c.PageState.Position].WrappedContent)) c.Scroll(len(c.PageState.History[c.PageState.Position].WrappedContent))
case 'd': case 'd':
// scroll down 75% // Scroll down 75%
c.ClearMessage() c.ClearMessage()
distance := c.Height - c.Height/4 distance := c.Height - c.Height/4
c.Scroll(distance) c.Scroll(distance)
case 'u': case 'u':
// scroll up 75% // Scroll up 75%
c.ClearMessage() c.ClearMessage()
distance := c.Height - c.Height/4 distance := c.Height - c.Height/4
c.Scroll(-distance) c.Scroll(-distance)
case 'U':
// Move up a directory for the current host
url := c.PageState.History[c.PageState.Position].Location.Full
c.Visit(UpOneDir(url))
case 'b', 'h': case 'b', 'h':
// go back // Go back
c.ClearMessage() c.ClearMessage()
err := c.PageState.NavigateHistory(-1) err := c.PageState.NavigateHistory(-1)
if err != nil { if err != nil {
@ -193,6 +198,7 @@ func (c *client) TakeControlInput() {
c.Draw() c.Draw()
} }
case 'R': case 'R':
// Refresh the current page
c.ClearMessage() c.ClearMessage()
err := c.ReloadPage() err := c.ReloadPage()
if err != nil { if err != nil {
@ -202,11 +208,11 @@ func (c *client) TakeControlInput() {
c.Draw() c.Draw()
} }
case 'B': case 'B':
// open the bookmarks browser // Toggle the bookmark browser
c.BookMarks.ToggleOpen() c.BookMarks.ToggleOpen()
c.Draw() c.Draw()
case 'f', 'l': case 'f', 'l':
// go forward // Go forward
c.ClearMessage() c.ClearMessage()
err := c.PageState.NavigateHistory(1) err := c.PageState.NavigateHistory(1)
if err != nil { if err != nil {
@ -218,7 +224,7 @@ func (c *client) TakeControlInput() {
c.Draw() c.Draw()
} }
case '\t': case '\t':
// Toggle bookmark browser focus on/off // Toggle bookmark browser focus
c.BookMarks.ToggleFocused() c.BookMarks.ToggleFocused()
c.Draw() c.Draw()
case 'n': case 'n':
@ -258,7 +264,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.PageState.History[c.PageState.Position].WrapContent(c.Width-1,getMaxWidth(c.Options),(c.Options["theme"] == "color"))
c.Draw() c.Draw()
} }
case ':', ' ': case ':', ' ':
@ -988,7 +994,7 @@ func (c *client) handleGopher(u Url) {
} else { } else {
pg.FileType = "text" pg.FileType = "text"
} }
pg.WrapContent(c.Width-1, (c.Options["theme"] == "color")) pg.WrapContent(c.Width-1, getMaxWidth(c.Options), (c.Options["theme"] == "color"))
c.PageState.Add(pg) c.PageState.Add(pg)
c.SetPercentRead() c.SetPercentRead()
c.ClearMessage() c.ClearMessage()
@ -1015,7 +1021,7 @@ func (c *client) handleGemini(u Url) {
u.Mime = capsule.MimeMin 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, getMaxWidth(c.Options), (c.Options["theme"] == "color"))
c.PageState.Add(pg) c.PageState.Add(pg)
c.SetPercentRead() c.SetPercentRead()
c.ClearMessage() c.ClearMessage()
@ -1081,7 +1087,7 @@ func (c *client) handleLocal(u Url) {
if ext == ".jpg" || ext == ".jpeg" || ext == ".gif" || ext == ".png" { if ext == ".jpg" || ext == ".jpeg" || ext == ".gif" || ext == ".png" {
pg.FileType = "image" pg.FileType = "image"
} }
pg.WrapContent(c.Width-1, (c.Options["theme"] == "color")) pg.WrapContent(c.Width-1, getMaxWidth(c.Options), (c.Options["theme"] == "color"))
c.PageState.Add(pg) c.PageState.Add(pg)
c.SetPercentRead() c.SetPercentRead()
c.ClearMessage() c.ClearMessage()
@ -1097,7 +1103,7 @@ func (c *client) handleFinger(u Url) {
return return
} }
pg := MakePage(u, content, []string{}) pg := MakePage(u, content, []string{})
pg.WrapContent(c.Width-1, (c.Options["theme"] == "color")) pg.WrapContent(c.Width-1, getMaxWidth(c.Options), (c.Options["theme"] == "color"))
c.PageState.Add(pg) c.PageState.Add(pg)
c.SetPercentRead() c.SetPercentRead()
c.ClearMessage() c.ClearMessage()
@ -1117,7 +1123,7 @@ func (c *client) handleWeb(u Url) {
return return
} }
pg := MakePage(u, page.Content, page.Links) pg := MakePage(u, page.Content, page.Links)
pg.WrapContent(c.Width-1, (c.Options["theme"] == "color")) pg.WrapContent(c.Width-1, getMaxWidth(c.Options), (c.Options["theme"] == "color"))
c.PageState.Add(pg) c.PageState.Add(pg)
c.SetPercentRead() c.SetPercentRead()
c.ClearMessage() c.ClearMessage()
@ -1258,3 +1264,16 @@ func updateTimeouts(timeoutString string) error {
return nil return nil
} }
// getMaxWidth looks through the given options map and will safely return a max width to render
// if the option is missing or malformed, it will default to 100. A sane minimum of 10 is enforced.
func getMaxWidth(options map[string]string) int {
out, err := strconv.Atoi(options["maxwidth"])
if err != nil {
out = 100
}
if out < 10 {
out = 10
}
return out
}

View File

@ -56,6 +56,7 @@ var defaultOptions = map[string]string{
"theme": "normal", // "normal", "inverted", "color" "theme": "normal", // "normal", "inverted", "color"
"timeout": "15", // connection timeout for gopher/gemini in seconds "timeout": "15", // connection timeout for gopher/gemini in seconds
"webmode": "none", // "none", "gui", "lynx", "w3m", "elinks" "webmode": "none", // "none", "gui", "lynx", "w3m", "elinks"
"maxwidth": "100",
} }
// homePath will return the path to your home directory as a string // homePath will return the path to your home directory as a string

30
page.go
View File

@ -3,8 +3,8 @@ package main
import ( import (
"fmt" "fmt"
"strings" "strings"
"tildegit.org/sloum/bombadillo/tdiv" "tildegit.org/sloum/bombadillo/tdiv"
"unicode"
) )
//------------------------------------------------\\ //------------------------------------------------\\
@ -65,12 +65,12 @@ func (p *Page) RenderImage(width int) {
// width and updates the WrappedContent // width and updates the WrappedContent
// of the Page struct width a string slice // of the Page struct width a string slice
// of the wrapped data // of the wrapped data
func (p *Page) WrapContent(width int, color bool) { func (p *Page) WrapContent(width, maxWidth int, color bool) {
if p.FileType == "image" { if p.FileType == "image" {
p.RenderImage(width) p.RenderImage(width)
return return
} }
width = min(width, 100) width = min(width, maxWidth)
counter := 0 counter := 0
spacer := "" spacer := ""
var content strings.Builder var content strings.Builder
@ -83,7 +83,10 @@ func (p *Page) WrapContent(width int, color bool) {
} else if strings.HasSuffix(p.Location.Mime, "gemini") { //gemini document } else if strings.HasSuffix(p.Location.Mime, "gemini") { //gemini document
spacer = " " spacer = " "
} }
for _, ch := range []rune(p.RawContent) {
runeArr := []rune(p.RawContent)
for i := 0; i < len(runeArr); i++ {
ch := runeArr[i]
if escape { if escape {
if color { if color {
esc.WriteRune(ch) esc.WriteRune(ch)
@ -109,7 +112,7 @@ func (p *Page) WrapContent(width int, color bool) {
counter = 0 counter = 0
} }
} else if ch == '\r' || ch == '\v' || ch == '\b' || ch == '\f' || ch == '\a' { } else if ch == '\r' || ch == '\v' || ch == '\b' || ch == '\f' || ch == '\a' {
// Get rid of control characters we dont want // Get rid of control characters we don't want
continue continue
} else if ch == 27 { } else if ch == 27 {
if p.Location.Scheme == "local" { if p.Location.Scheme == "local" {
@ -125,9 +128,24 @@ func (p *Page) WrapContent(width int, color bool) {
} }
continue continue
} else { } else {
if counter <= width { // peek forward to see if we can render the word without going over
j := i
for ; j < len(runeArr) && !unicode.IsSpace(runeArr[j]); j++ {
if counter+(j-i) > width+1 {
break
}
}
// if we can render the rest of the word, write the next letter. else, skip to the next line.
// TODO(raidancampbell): optimize this to write out the whole word, this will involve referencing the
// above special cases
if counter+(j-i) <= width+1 && !(j == i && counter == width+1) {
content.WriteRune(ch) content.WriteRune(ch)
counter++ counter++
} else if ch == ' ' || ch == '\t' {
// we want to wrap and write this char, but it's a space. eat it to prevent the next line from
// having a leading whitespace because of our wrapping
counter++
} else { } else {
content.WriteRune('\n') content.WriteRune('\n')
counter = 0 counter = 0

View File

@ -20,8 +20,9 @@ func Test_WrapContent_Wrapped_Line_Length(t *testing.T) {
Color bool Color bool
} }
type args struct { type args struct {
width int width int
color bool maxWidth int
color bool
} }
// create a Url for use by the MakePage function // create a Url for use by the MakePage function
@ -41,22 +42,56 @@ func Test_WrapContent_Wrapped_Line_Length(t *testing.T) {
"", "",
}, },
args{ args{
10,
10, 10,
false, false,
}, },
}, },
{ {
"Long line wrapped to 10 columns", "multiple words should wrap at the right point",
"0123456789 123456789 123456789 123456789 123456789\n", "01 345 789 123456789 123456789 123456789 123456789\n",
[]string{ []string{
"0123456789", "01 345 789",
" 123456789", "123456789 ",
" 123456789", "123456789 ",
" 123456789", "123456789 ",
" 123456789", "123456789",
"", "",
}, },
args{ args{
10,
10,
false,
},
},
{
"Long line wrapped to 10 columns, leading spaces omitted when wrapping",
"0123456789 123456789 123456789 123456789 123456789\n",
[]string{
"0123456789",
"123456789 ",
"123456789 ",
"123456789 ",
"123456789",
"",
},
args{
10,
10,
false,
},
},
{
"Intentional leading spaces aren't trimmed",
"01 345\n 789 123456789\n",
[]string{
"01 345",
" 789 ",
"123456789",
"",
},
args{
10,
10, 10,
false, false,
}, },
@ -77,6 +112,7 @@ func Test_WrapContent_Wrapped_Line_Length(t *testing.T) {
"", "",
}, },
args{ args{
10,
10, 10,
false, false,
}, },
@ -85,7 +121,7 @@ func Test_WrapContent_Wrapped_Line_Length(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
p := MakePage(url, tt.input, []string{""}) p := MakePage(url, tt.input, []string{""})
p.WrapContent(tt.args.width-1, tt.args.color) p.WrapContent(tt.args.width-1, tt.args.maxWidth, tt.args.color)
if !reflect.DeepEqual(p.WrappedContent, tt.expects) { if !reflect.DeepEqual(p.WrappedContent, tt.expects) {
t.Errorf("Test failed - %s\nexpects %s\nactual %s", tt.name, tt.expects, p.WrappedContent) t.Errorf("Test failed - %s\nexpects %s\nactual %s", tt.name, tt.expects, p.WrappedContent)
} }

View File

@ -60,7 +60,7 @@ func (p *Pages) Add(pg Page) {
// Render wraps the content for the current page and returns // Render wraps the content for the current page and returns
// the page content as a string slice // the page content as a string slice
func (p *Pages) Render(termHeight, termWidth int, color bool) []string { func (p *Pages) Render(termHeight, termWidth, maxWidth int, color bool) []string {
if p.Length < 1 { if p.Length < 1 {
return make([]string, 0) return make([]string, 0)
} }
@ -68,7 +68,7 @@ func (p *Pages) Render(termHeight, termWidth int, color bool) []string {
prev := len(p.History[p.Position].WrappedContent) prev := len(p.History[p.Position].WrappedContent)
if termWidth != p.History[p.Position].WrapWidth || p.History[p.Position].Color != color { if termWidth != p.History[p.Position].WrapWidth || p.History[p.Position].Color != color {
p.History[p.Position].WrapContent(termWidth, color) p.History[p.Position].WrapContent(termWidth, maxWidth, color)
} }
now := len(p.History[p.Position].WrappedContent) now := len(p.History[p.Position].WrappedContent)

19
url.go
View File

@ -3,6 +3,7 @@ package main
import ( import (
"fmt" "fmt"
"os/user" "os/user"
"path"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
@ -145,6 +146,24 @@ func MakeUrl(u string) (Url, error) {
return out, nil return out, nil
} }
func UpOneDir(u string) string {
url, err := MakeUrl(u)
if len(url.Resource) < 1 || err != nil {
return u
}
if strings.HasSuffix(url.Resource, "/") {
url.Resource = url.Resource[:len(url.Resource)-1]
}
url.Resource, _ = path.Split(url.Resource)
if url.Scheme == "gopher" {
url.Mime = "1"
}
url.Full = url.Scheme + "://" + url.Host + ":" + url.Port + "/" + url.Mime + url.Resource
return url.Full
}
func parseFinger(u string) (Url, error) { func parseFinger(u string) (Url, error) {
var out Url var out Url
out.Scheme = "finger" out.Scheme = "finger"