Merge pull request 'Better word wrapping' (#214) from raidancampbell/bombadillo:master into release-2.4.0

Reviewed-on: #214
This commit is contained in:
Sloom Sloum Sluom IV 2022-02-16 04:03:48 +00:00
commit 5a91f4ac94
5 changed files with 93 additions and 25 deletions

View File

@ -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
}

View File

@ -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

30
page.go
View File

@ -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

View File

@ -21,6 +21,7 @@ func Test_WrapContent_Wrapped_Line_Length(t *testing.T) {
}
type args struct {
width int
maxWidth int
color bool
}
@ -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)
}

View File

@ -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)