diff --git a/client.go b/client.go index 61b69da..082bc3a 100644 --- a/client.go +++ b/client.go @@ -73,7 +73,7 @@ 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")) + 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") @@ -264,7 +264,7 @@ 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")) + c.PageState.History[c.PageState.Position].WrapContent(c.Width-1,getMaxWidth(c.Options),(c.Options["theme"] == "color")) c.Draw() } case ':', ' ': @@ -994,7 +994,7 @@ func (c *client) handleGopher(u Url) { } else { 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.SetPercentRead() c.ClearMessage() @@ -1021,7 +1021,7 @@ 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")) + pg.WrapContent(c.Width-1, getMaxWidth(c.Options), (c.Options["theme"] == "color")) c.PageState.Add(pg) c.SetPercentRead() c.ClearMessage() @@ -1087,7 +1087,7 @@ 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")) + pg.WrapContent(c.Width-1, getMaxWidth(c.Options), (c.Options["theme"] == "color")) c.PageState.Add(pg) c.SetPercentRead() c.ClearMessage() @@ -1103,7 +1103,7 @@ func (c *client) handleFinger(u Url) { return } 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.SetPercentRead() c.ClearMessage() @@ -1123,7 +1123,7 @@ func (c *client) handleWeb(u Url) { return } 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.SetPercentRead() c.ClearMessage() @@ -1264,3 +1264,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/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..40a0285 100644 --- a/page.go +++ b/page.go @@ -3,8 +3,8 @@ package main import ( "fmt" "strings" - "tildegit.org/sloum/bombadillo/tdiv" + "unicode" ) //------------------------------------------------\\ @@ -65,12 +65,12 @@ 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) + width = min(width, maxWidth) counter := 0 spacer := "" 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 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) @@ -109,7 +112,7 @@ func (p *Page) WrapContent(width 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" { @@ -125,9 +128,24 @@ func (p *Page) WrapContent(width 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 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 b5e8f94..ef244c2 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,22 +42,56 @@ func Test_WrapContent_Wrapped_Line_Length(t *testing.T) { "", }, args{ + 10, 10, false, }, }, { - "Long line wrapped to 10 columns", - "0123456789 123456789 123456789 123456789 123456789\n", + "multiple words should wrap at the right point", + "01 345 789 123456789 123456789 123456789 123456789\n", []string{ - "0123456789", - " 123456789", - " 123456789", - " 123456789", - " 123456789", + "01 345 789", + "123456789 ", + "123456789 ", + "123456789 ", + "123456789", "", }, 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, false, }, @@ -77,6 +112,7 @@ func Test_WrapContent_Wrapped_Line_Length(t *testing.T) { "", }, args{ + 10, 10, false, }, @@ -85,7 +121,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)