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} diff --git a/bombadillo.1 b/bombadillo.1 index f1e9839..4ed7690 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 @@ -253,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 diff --git a/client.go b/client.go index b1795cb..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") @@ -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': @@ -258,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 ':', ' ': @@ -988,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() @@ -1015,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() @@ -1081,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() @@ -1097,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() @@ -1117,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() @@ -1258,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) 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"