Better word wrapping #214
27
client.go
27
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")
|
||||
|
@ -258,7 +258,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 ':', ' ':
|
||||
|
@ -988,7 +988,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()
|
||||
|
@ -1015,7 +1015,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()
|
||||
|
@ -1081,7 +1081,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()
|
||||
|
@ -1097,7 +1097,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()
|
||||
|
@ -1117,7 +1117,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()
|
||||
|
@ -1258,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
|
||||
}
|
|
@ -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
30
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
|
||||
sloum
commented
I'm not sure I follow why this is here. Since we are trying to wrap at maxwidth and remove hardcoded values in favor of the passed value that comes from the config, why are we hardcoding 100 here? I'm not sure I follow why this is here. Since we are trying to wrap at maxwidth and remove hardcoded values in favor of the passed value that comes from the config, why are we hardcoding 100 here?
raidancampbell
commented
The intent here was to catch malformed / missing configuration entries: your suggestion of wrapping the logic into a Much better, thanks! The intent here was to catch malformed / missing configuration entries: your suggestion of wrapping the logic into a `getMaxWidth` function will solve this problem and remove the need here.
Much better, thanks!
|
||||
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
|
||||
|
|
56
page_test.go
56
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)
|
||||
}
|
||||
|
|
4
pages.go
4
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)
|
||||
|
|
Loading…
Reference in New Issue
In general I think ignoring error should be avoided where possible. I think we can likely encapsule this in a function that can be called wherever we need it:
It makes sense that we could really just store the value in a module var:
Then in the code for setting config values have a case for changing maxwidth and reset the var. Then we can just reference the var everywhere instead of recasting to int in a few places (that said, I have not gotten to do a deep dive into your changes, just a cursory look.... so I may be overthinking this).
070f7eb6ba261866395706f709d12dff3ead2e74
contains a first pass on this. Sincec.Options
is both a field on the struct and altered by reading in the configuration after instantiating the struct, it's a bit less clean.Other potential options:
sync.Once
(existed in 1.12) ingetMaxWidth
to avoid the checks and castsmaxWidth
can be a field on the client alongsideWidth