From 984fb4eb2faa15142cde5482e558c28c8f178ec6 Mon Sep 17 00:00:00 2001 From: "R. Aidan Campbell" Date: Sun, 6 Feb 2022 21:21:08 -0700 Subject: [PATCH 1/4] 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) -- 2.34.1 From dac13e1669071cea781226dffa0fd48c2710b015 Mon Sep 17 00:00:00 2001 From: "R. Aidan Campbell" Date: Sun, 6 Feb 2022 22:09:06 -0700 Subject: [PATCH 2/4] 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", -- 2.34.1 From 070f7eb6ba261866395706f709d12dff3ead2e74 Mon Sep 17 00:00:00 2001 From: "R. Aidan Campbell" Date: Mon, 7 Feb 2022 21:54:12 -0700 Subject: [PATCH 3/4] 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 := "" -- 2.34.1 From 1bef2e51a1705bf43f014b1aa7f0a4cbc7398f55 Mon Sep 17 00:00:00 2001 From: "R. Aidan Campbell" Date: Tue, 8 Feb 2022 20:47:16 -0700 Subject: [PATCH 4/4] 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{ -- 2.34.1