From d7472490693666eaaaf90e0e9c7e177caad3f1c5 Mon Sep 17 00:00:00 2001 From: sloum Date: Sat, 24 Apr 2021 14:26:33 -0700 Subject: [PATCH 1/7] Adds UP hot key --- bombadillo.1 | 4 ++++ client.go | 28 +++++++++++++++++----------- url.go | 19 +++++++++++++++++++ 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/bombadillo.1 b/bombadillo.1 index f1e9839..bd20d92 100644 --- a/bombadillo.1 +++ b/bombadillo.1 @@ -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. .TP .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 Scroll up an amount corresponding to 75% of your terminal window height in the current document. .TP diff --git a/client.go b/client.go index b1795cb..61b69da 100644 --- a/client.go +++ b/client.go @@ -146,42 +146,47 @@ func (c *client) TakeControlInput() { switch input { case '1', '2', '3', '4', '5', '6', '7', '8', '9', '0': + // Quick link if input == '0' { c.goToLink("10") } else { c.goToLink(string(input)) } case 'j': - // scroll down one line + // Scroll down one line c.ClearMessage() c.Scroll(1) case 'k': - // scroll up one line + // Scroll up one line c.ClearMessage() c.Scroll(-1) case 'q': - // quit bombadillo + // Quit cui.Exit(0, "") case 'g': - // scroll to top + // Scroll to top c.ClearMessage() c.Scroll(-len(c.PageState.History[c.PageState.Position].WrappedContent)) case 'G': - // scroll to bottom + // Scroll to bottom c.ClearMessage() c.Scroll(len(c.PageState.History[c.PageState.Position].WrappedContent)) case 'd': - // scroll down 75% + // Scroll down 75% c.ClearMessage() distance := c.Height - c.Height/4 c.Scroll(distance) case 'u': - // scroll up 75% + // Scroll up 75% c.ClearMessage() distance := c.Height - c.Height/4 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': - // go back + // Go back c.ClearMessage() err := c.PageState.NavigateHistory(-1) if err != nil { @@ -193,6 +198,7 @@ func (c *client) TakeControlInput() { c.Draw() } case 'R': + // Refresh the current page c.ClearMessage() err := c.ReloadPage() if err != nil { @@ -202,11 +208,11 @@ func (c *client) TakeControlInput() { c.Draw() } case 'B': - // open the bookmarks browser + // Toggle the bookmark browser c.BookMarks.ToggleOpen() c.Draw() case 'f', 'l': - // go forward + // Go forward c.ClearMessage() err := c.PageState.NavigateHistory(1) if err != nil { @@ -218,7 +224,7 @@ func (c *client) TakeControlInput() { c.Draw() } case '\t': - // Toggle bookmark browser focus on/off + // Toggle bookmark browser focus c.BookMarks.ToggleFocused() c.Draw() case 'n': diff --git a/url.go b/url.go index 0831411..1ca45cc 100644 --- a/url.go +++ b/url.go @@ -3,6 +3,7 @@ package main import ( "fmt" "os/user" + "path" "path/filepath" "regexp" "strings" @@ -145,6 +146,24 @@ func MakeUrl(u string) (Url, error) { 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) { var out Url out.Scheme = "finger" From 80aabcd531da54a1aaf6f8290bf3231f1cffcac3 Mon Sep 17 00:00:00 2001 From: Jay Williams Date: Fri, 14 May 2021 12:18:04 -0500 Subject: [PATCH 2/7] Don't use GNU-specific gzip -k flag --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index cca7657..b3fd661 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ install: install-bin install-man install-desktop clean .PHONY: install-man install-man: bombadillo.1 - gzip -k ./bombadillo.1 + gzip -c ./bombadillo.1 > ./bombadillo.1.gz install -d ${DESTDIR}${MAN1DIR} install -m 0644 ./bombadillo.1.gz ${DESTDIR}${MAN1DIR} From 984fb4eb2faa15142cde5482e558c28c8f178ec6 Mon Sep 17 00:00:00 2001 From: "R. Aidan Campbell" Date: Sun, 6 Feb 2022 21:21:08 -0700 Subject: [PATCH 3/7] max width for softwraping now configured via maxwidth configuration item: default or invalid value falls back to previous 100 column --- client.go | 21 ++++++++++++++------- defaults.go | 1 + page.go | 7 +++++-- page_test.go | 10 +++++++--- pages.go | 4 ++-- 5 files changed, 29 insertions(+), 14 deletions(-) diff --git a/client.go b/client.go index b1795cb..1141030 100644 --- a/client.go +++ b/client.go @@ -73,7 +73,8 @@ func (c *client) Draw() { screen.WriteString("\033[0m") screen.WriteString(c.TopBar.Render(c.Width, c.Options["theme"])) screen.WriteString("\n") - pageContent := c.PageState.Render(c.Height, c.Width-1, (c.Options["theme"] == "color")) + maxWidth, _ := strconv.Atoi(c.Options["maxwidth"]) + pageContent := c.PageState.Render(c.Height, c.Width-1, maxWidth, (c.Options["theme"] == "color")) var re *regexp.Regexp if c.Options["theme"] == "inverse" { screen.WriteString("\033[7m") @@ -258,7 +259,8 @@ 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")) + maxWidth, _ := strconv.Atoi(c.Options["maxwidth"]) + c.PageState.History[c.PageState.Position].WrapContent(c.Width-1,maxWidth,(c.Options["theme"] == "color")) c.Draw() } case ':', ' ': @@ -988,7 +990,8 @@ func (c *client) handleGopher(u Url) { } else { pg.FileType = "text" } - pg.WrapContent(c.Width-1, (c.Options["theme"] == "color")) + maxWidth, _ := strconv.Atoi(c.Options["maxwidth"]) + pg.WrapContent(c.Width-1, maxWidth, (c.Options["theme"] == "color")) c.PageState.Add(pg) c.SetPercentRead() c.ClearMessage() @@ -1015,7 +1018,8 @@ func (c *client) handleGemini(u Url) { u.Mime = capsule.MimeMin pg := MakePage(u, capsule.Content, capsule.Links) pg.FileType = capsule.MimeMaj - pg.WrapContent(c.Width-1, (c.Options["theme"] == "color")) + maxWidth, _ := strconv.Atoi(c.Options["maxwidth"]) + pg.WrapContent(c.Width-1, maxWidth, (c.Options["theme"] == "color")) c.PageState.Add(pg) c.SetPercentRead() c.ClearMessage() @@ -1081,7 +1085,8 @@ func (c *client) handleLocal(u Url) { if ext == ".jpg" || ext == ".jpeg" || ext == ".gif" || ext == ".png" { pg.FileType = "image" } - pg.WrapContent(c.Width-1, (c.Options["theme"] == "color")) + maxWidth, _ := strconv.Atoi(c.Options["maxwidth"]) + pg.WrapContent(c.Width-1, maxWidth, (c.Options["theme"] == "color")) c.PageState.Add(pg) c.SetPercentRead() c.ClearMessage() @@ -1097,7 +1102,8 @@ func (c *client) handleFinger(u Url) { return } pg := MakePage(u, content, []string{}) - pg.WrapContent(c.Width-1, (c.Options["theme"] == "color")) + maxWidth, _ := strconv.Atoi(c.Options["maxwidth"]) + pg.WrapContent(c.Width-1, maxWidth, (c.Options["theme"] == "color")) c.PageState.Add(pg) c.SetPercentRead() c.ClearMessage() @@ -1117,7 +1123,8 @@ func (c *client) handleWeb(u Url) { return } pg := MakePage(u, page.Content, page.Links) - pg.WrapContent(c.Width-1, (c.Options["theme"] == "color")) + maxWidth, _ := strconv.Atoi(c.Options["maxwidth"]) + pg.WrapContent(c.Width-1, maxWidth, (c.Options["theme"] == "color")) c.PageState.Add(pg) c.SetPercentRead() c.ClearMessage() diff --git a/defaults.go b/defaults.go index 24429d7..92feaad 100644 --- a/defaults.go +++ b/defaults.go @@ -56,6 +56,7 @@ var defaultOptions = map[string]string{ "theme": "normal", // "normal", "inverted", "color" "timeout": "15", // connection timeout for gopher/gemini in seconds "webmode": "none", // "none", "gui", "lynx", "w3m", "elinks" + "maxwidth": "100", } // homePath will return the path to your home directory as a string diff --git a/page.go b/page.go index ac31168..99ebdce 100644 --- a/page.go +++ b/page.go @@ -65,12 +65,15 @@ func (p *Page) RenderImage(width int) { // width and updates the WrappedContent // of the Page struct width a string slice // 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" { p.RenderImage(width) return } - width = min(width, 100) + if maxWidth < 100 { + maxWidth = 100 + } + width = min(width, maxWidth) counter := 0 spacer := "" var content strings.Builder diff --git a/page_test.go b/page_test.go index b5e8f94..33de341 100644 --- a/page_test.go +++ b/page_test.go @@ -20,8 +20,9 @@ func Test_WrapContent_Wrapped_Line_Length(t *testing.T) { Color bool } type args struct { - width int - color bool + width int + maxWidth int + color bool } // create a Url for use by the MakePage function @@ -41,6 +42,7 @@ func Test_WrapContent_Wrapped_Line_Length(t *testing.T) { "", }, args{ + 10, 10, false, }, @@ -57,6 +59,7 @@ func Test_WrapContent_Wrapped_Line_Length(t *testing.T) { "", }, args{ + 10, 10, false, }, @@ -77,6 +80,7 @@ func Test_WrapContent_Wrapped_Line_Length(t *testing.T) { "", }, args{ + 10, 10, false, }, @@ -85,7 +89,7 @@ func Test_WrapContent_Wrapped_Line_Length(t *testing.T) { 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) + p.WrapContent(tt.args.width-1, tt.args.maxWidth, 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) } diff --git a/pages.go b/pages.go index e4ef964..647264e 100644 --- a/pages.go +++ b/pages.go @@ -60,7 +60,7 @@ func (p *Pages) Add(pg Page) { // Render wraps the content for the current page and returns // 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 { 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) 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) From dac13e1669071cea781226dffa0fd48c2710b015 Mon Sep 17 00:00:00 2001 From: "R. Aidan Campbell" Date: Sun, 6 Feb 2022 22:09:06 -0700 Subject: [PATCH 4/7] soft wrap between words --- page.go | 22 ++++++++++++++++++---- page_test.go | 17 +++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/page.go b/page.go index 99ebdce..d45184a 100644 --- a/page.go +++ b/page.go @@ -3,8 +3,8 @@ package main import ( "fmt" "strings" - "tildegit.org/sloum/bombadillo/tdiv" + "unicode" ) //------------------------------------------------\\ @@ -86,7 +86,10 @@ func (p *Page) WrapContent(width, maxWidth int, color bool) { } else if strings.HasSuffix(p.Location.Mime, "gemini") { //gemini document spacer = " " } - for _, ch := range []rune(p.RawContent) { + + runeArr := []rune(p.RawContent) + for i := 0; i < len(runeArr); i++ { + ch := runeArr[i] if escape { if color { esc.WriteRune(ch) @@ -112,7 +115,7 @@ func (p *Page) WrapContent(width, maxWidth int, color bool) { counter = 0 } } 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 } else if ch == 27 { if p.Location.Scheme == "local" { @@ -128,7 +131,18 @@ func (p *Page) WrapContent(width, maxWidth int, color bool) { } continue } 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) counter++ } else { diff --git a/page_test.go b/page_test.go index 33de341..1b6866d 100644 --- a/page_test.go +++ b/page_test.go @@ -47,6 +47,23 @@ func Test_WrapContent_Wrapped_Line_Length(t *testing.T) { false, }, }, + { + "multiple words should wrap at the right point", + "01 345 789 123456789 123456789 123456789 123456789\n", + []string{ + "01 345 789", + " 123456789", + " 123456789", + " 123456789", + " 123456789", + "", + }, + args{ + 10, + 10, + false, + }, + }, { "Long line wrapped to 10 columns", "0123456789 123456789 123456789 123456789 123456789\n", From 070f7eb6ba261866395706f709d12dff3ead2e74 Mon Sep 17 00:00:00 2001 From: "R. Aidan Campbell" Date: Mon, 7 Feb 2022 21:54:12 -0700 Subject: [PATCH 5/7] extract logic for reading maxwidth config into a function: now safely handles fallbacks for missing or malformed configuration --- client.go | 34 ++++++++++++++++++++-------------- page.go | 3 --- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/client.go b/client.go index 1141030..123ddf6 100644 --- a/client.go +++ b/client.go @@ -73,8 +73,7 @@ func (c *client) Draw() { screen.WriteString("\033[0m") screen.WriteString(c.TopBar.Render(c.Width, c.Options["theme"])) screen.WriteString("\n") - maxWidth, _ := strconv.Atoi(c.Options["maxwidth"]) - pageContent := c.PageState.Render(c.Height, c.Width-1, maxWidth, (c.Options["theme"] == "color")) + pageContent := c.PageState.Render(c.Height, c.Width-1, getMaxWidth(c.Options), (c.Options["theme"] == "color")) var re *regexp.Regexp if c.Options["theme"] == "inverse" { screen.WriteString("\033[7m") @@ -259,8 +258,7 @@ func (c *client) TakeControlInput() { } err = c.NextSearchItem(0) if err != nil { - maxWidth, _ := strconv.Atoi(c.Options["maxwidth"]) - c.PageState.History[c.PageState.Position].WrapContent(c.Width-1,maxWidth,(c.Options["theme"] == "color")) + c.PageState.History[c.PageState.Position].WrapContent(c.Width-1,getMaxWidth(c.Options),(c.Options["theme"] == "color")) c.Draw() } case ':', ' ': @@ -990,8 +988,7 @@ func (c *client) handleGopher(u Url) { } else { pg.FileType = "text" } - maxWidth, _ := strconv.Atoi(c.Options["maxwidth"]) - pg.WrapContent(c.Width-1, maxWidth, (c.Options["theme"] == "color")) + pg.WrapContent(c.Width-1, getMaxWidth(c.Options), (c.Options["theme"] == "color")) c.PageState.Add(pg) c.SetPercentRead() c.ClearMessage() @@ -1018,8 +1015,7 @@ func (c *client) handleGemini(u Url) { u.Mime = capsule.MimeMin pg := MakePage(u, capsule.Content, capsule.Links) pg.FileType = capsule.MimeMaj - maxWidth, _ := strconv.Atoi(c.Options["maxwidth"]) - pg.WrapContent(c.Width-1, maxWidth, (c.Options["theme"] == "color")) + pg.WrapContent(c.Width-1, getMaxWidth(c.Options), (c.Options["theme"] == "color")) c.PageState.Add(pg) c.SetPercentRead() c.ClearMessage() @@ -1085,8 +1081,7 @@ func (c *client) handleLocal(u Url) { if ext == ".jpg" || ext == ".jpeg" || ext == ".gif" || ext == ".png" { pg.FileType = "image" } - maxWidth, _ := strconv.Atoi(c.Options["maxwidth"]) - pg.WrapContent(c.Width-1, maxWidth, (c.Options["theme"] == "color")) + pg.WrapContent(c.Width-1, getMaxWidth(c.Options), (c.Options["theme"] == "color")) c.PageState.Add(pg) c.SetPercentRead() c.ClearMessage() @@ -1102,8 +1097,7 @@ func (c *client) handleFinger(u Url) { return } pg := MakePage(u, content, []string{}) - maxWidth, _ := strconv.Atoi(c.Options["maxwidth"]) - pg.WrapContent(c.Width-1, maxWidth, (c.Options["theme"] == "color")) + pg.WrapContent(c.Width-1, getMaxWidth(c.Options), (c.Options["theme"] == "color")) c.PageState.Add(pg) c.SetPercentRead() c.ClearMessage() @@ -1123,8 +1117,7 @@ func (c *client) handleWeb(u Url) { return } pg := MakePage(u, page.Content, page.Links) - maxWidth, _ := strconv.Atoi(c.Options["maxwidth"]) - pg.WrapContent(c.Width-1, maxWidth, (c.Options["theme"] == "color")) + pg.WrapContent(c.Width-1, getMaxWidth(c.Options), (c.Options["theme"] == "color")) c.PageState.Add(pg) c.SetPercentRead() c.ClearMessage() @@ -1265,3 +1258,16 @@ func updateTimeouts(timeoutString string) error { 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 +} \ No newline at end of file diff --git a/page.go b/page.go index d45184a..7fc01c1 100644 --- a/page.go +++ b/page.go @@ -70,9 +70,6 @@ func (p *Page) WrapContent(width, maxWidth int, color bool) { p.RenderImage(width) return } - if maxWidth < 100 { - maxWidth = 100 - } width = min(width, maxWidth) counter := 0 spacer := "" From 1bef2e51a1705bf43f014b1aa7f0a4cbc7398f55 Mon Sep 17 00:00:00 2001 From: "R. Aidan Campbell" Date: Tue, 8 Feb 2022 20:47:16 -0700 Subject: [PATCH 6/7] when wrapping, omit leading whitespace created if the wrap occurs at a space char --- page.go | 4 ++++ page_test.go | 33 ++++++++++++++++++++++++--------- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/page.go b/page.go index 7fc01c1..40a0285 100644 --- a/page.go +++ b/page.go @@ -142,6 +142,10 @@ func (p *Page) WrapContent(width, maxWidth int, color bool) { if counter+(j-i) <= width+1 && !(j == i && counter == width+1) { content.WriteRune(ch) 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 { content.WriteRune('\n') counter = 0 diff --git a/page_test.go b/page_test.go index 1b6866d..ef244c2 100644 --- a/page_test.go +++ b/page_test.go @@ -52,10 +52,10 @@ func Test_WrapContent_Wrapped_Line_Length(t *testing.T) { "01 345 789 123456789 123456789 123456789 123456789\n", []string{ "01 345 789", - " 123456789", - " 123456789", - " 123456789", - " 123456789", + "123456789 ", + "123456789 ", + "123456789 ", + "123456789", "", }, args{ @@ -65,14 +65,29 @@ func Test_WrapContent_Wrapped_Line_Length(t *testing.T) { }, }, { - "Long line wrapped to 10 columns", + "Long line wrapped to 10 columns, leading spaces omitted when wrapping", "0123456789 123456789 123456789 123456789 123456789\n", []string{ "0123456789", - " 123456789", - " 123456789", - " 123456789", - " 123456789", + "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{ From 93cf4321d48d880593a64d2ee755ffbd661b8d19 Mon Sep 17 00:00:00 2001 From: sloum Date: Tue, 15 Feb 2022 21:19:22 -0800 Subject: [PATCH 7/7] Last minute update to add information about the maxwidth setting to the manpage --- bombadillo.1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bombadillo.1 b/bombadillo.1 index bd20d92..4ed7690 100644 --- a/bombadillo.1 +++ b/bombadillo.1 @@ -257,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. .TP .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 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