diff --git a/DEVELOPING.md b/DEVELOPING.md new file mode 100644 index 0000000..4b05842 --- /dev/null +++ b/DEVELOPING.md @@ -0,0 +1,64 @@ +# Developing Bombadillo + +## Getting Started + +Following the standard install instructions should lead you to have nearly everything you need to commence development. The only additions to this are: + +- To be able to submit pull requests, you will need to fork this repository first. +- The build process must be tested with Go 1.11 to ensure backward compatibility. This version can be installed as per the [Go install documentation](https://golang.org/doc/install#extra_versions). Check that changes build with this version using `make test`. +- Linting must be performed on new changes using `gofmt` and [golangci-lint](https://github.com/golangci/golangci-lint) + + +## How changes are made + +A stable version of Bombadillo is kept in the default branch, so that people can easily clone the repo and get a good version of the software. + +New changes are implemented to the **develop** branch as **development releases**. + +Changes are implemented to the default branch when: + + - There are a set of changes in **develop** that are good enough to be considered stable. + - This may be a **minor** set of changes for a **minor release**, or + - a large **major** change for **major release**. + - An urgent issue is identified in the stable version that requires an immediate **patch release**. + + +### Process for introducing a new change + +Please refer to our [notes on contributing](README.md#contributing) to get an understanding of how new changes are initiated, the type of changes accepted and the review process. + +1. Create a new feature branch based on the **develop** branch. +1. Raise a pull request (PR) targeting the **develop** branch. +1. The PR is reviewed. +1. If the PR is approved, it is merged. +1. The version number is incremented, along with any other release activity. + + +### Process for incrementing the version number + +The version number is incremented during a **development release**, **patch release**, and **minor** and **major releases**. This is primarily managed through git tags in the following way: + +```shell +# switch to the branch the release is being performed for +git checkout branch + +# ensure everything is up to date +git pull + +# get the commit ID for the recent merge +git log + +# get the current version number (the highest number) +git tag + +# for a development release, add the incremented version number to the commit-id, for example: +git tag 2.0.2 abcdef + +# for releases to the default branch, this tag can also be added with annotations +git tag 2.1.0 abdef -a "This version adds several new features..." +``` + +Releases to the default branch also include the following tasks: + +1. The version number in the VERSION file is incremented and committed. +1. Release information should also be verified on the [tildegit releases page](https://tildegit.org/sloum/bombadillo/releases). diff --git a/README.md b/README.md index f965945..1b1af0f 100644 --- a/README.md +++ b/README.md @@ -127,11 +127,7 @@ The maintainers use the [tildegit](https://tildegit.org) issues system to discus ## Development -Following the standard install instructions should lead you to have nearly everything you need to commence development. The only additions to this are: - -- To be able to submit pull requests, you will need to fork this repository first. -- The build process must be tested with Go 1.11 to ensure backward compatibility. This version can be installed as per the [Go install documentation](https://golang.org/doc/install#extra_versions). Check that changes build with this version using `make test`. -- Linting must be performed on new changes using `gofmt` and [golangci-lint](https://github.com/golangci/golangci-lint) +See [DEVELOPING.md](DEVELOPING.md) for information on how changes to Bombadillo are made, along with other technical information for developers. ## License diff --git a/VERSION b/VERSION index 7ec1d6d..ccbccc3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.1.0 +2.2.0 diff --git a/client.go b/client.go index ed642a2..855f79d 100644 --- a/client.go +++ b/client.go @@ -929,7 +929,7 @@ func (c *client) Visit(url string) { // +++ Begin Protocol Handlers +++ func (c *client) handleGopher(u Url) { - if u.DownloadOnly { + if u.DownloadOnly || (c.Options["showimages"] == "false" && (u.Mime == "I" || u.Mime == "g")) { nameSplit := strings.Split(u.Resource, "/") filename := nameSplit[len(nameSplit)-1] filename = strings.Trim(filename, " \t\r\n\v\f\a") @@ -947,6 +947,11 @@ func (c *client) handleGopher(u Url) { return } pg := MakePage(u, content, links) + if u.Mime == "I" || u.Mime == "g" { + pg.FileType = "image" + } else { + pg.FileType = "text" + } pg.WrapContent(c.Width-1, (c.Options["theme"] == "color")) c.PageState.Add(pg) c.SetPercentRead() @@ -968,8 +973,9 @@ func (c *client) handleGemini(u Url) { case 1: c.search("", u.Full, capsule.Content) case 2: - if capsule.MimeMaj == "text" { + if capsule.MimeMaj == "text" || (c.Options["showimages"] == "true" && capsule.MimeMaj == "image") { pg := MakePage(u, capsule.Content, capsule.Links) + pg.FileType = capsule.MimeMaj pg.WrapContent(c.Width-1, (c.Options["theme"] == "color")) c.PageState.Add(pg) c.SetPercentRead() @@ -1018,6 +1024,10 @@ func (c *client) handleLocal(u Url) { return } pg := MakePage(u, content, links) + ext := strings.ToLower(filepath.Ext(u.Full)) + if ext == ".jpg" || ext == ".jpeg" || ext == ".gif" || ext == ".png" { + pg.FileType = "image" + } pg.WrapContent(c.Width-1, (c.Options["theme"] == "color")) c.PageState.Add(pg) c.SetPercentRead() diff --git a/defaults.go b/defaults.go index 1c0a230..c4ca227 100644 --- a/defaults.go +++ b/defaults.go @@ -45,12 +45,13 @@ var defaultOptions = map[string]string{ // the "configlocation" as follows: // "configlocation": xdgConfigPath() + "configlocation": xdgConfigPath(), + "defaultscheme": "gopher", // "gopher", "gemini", "http", "https" "homeurl": "gopher://bombadillo.colorfield.space:70/1/user-guide.map", "savelocation": homePath(), "searchengine": "gopher://gopher.floodgap.com:70/7/v2/vs", + "showimages": "true", "telnetcommand": "telnet", - "configlocation": xdgConfigPath(), - "defaultscheme": "gopher", // "gopher", "gemini", "http", "https" "theme": "normal", // "normal", "inverted", "color" "tlscertificate": "", "tlskey": "", diff --git a/gemini/gemini.go b/gemini/gemini.go index 884303a..e13cb59 100644 --- a/gemini/gemini.go +++ b/gemini/gemini.go @@ -339,9 +339,16 @@ func parseGemini(b, rootUrl, currentUrl string) (string, []string) { splitContent := strings.Split(b, "\n") links := make([]string, 0, 10) + outputIndex := 0 for i, ln := range splitContent { splitContent[i] = strings.Trim(ln, "\r\n") - if len([]rune(ln)) > 3 && ln[:2] == "=>" { + if ln == "```" { + // By continuing we create a variance between i and outputIndex + // the other branches here will write to the outputIndex rather + // than i, thus removing these lines while itterating without + // needing mroe allocations. + continue + } else if len([]rune(ln)) > 3 && ln[:2] == "=>" { var link, decorator string subLn := strings.Trim(ln[2:], "\r\n\t \a") splitPoint := strings.IndexAny(subLn, " \t") @@ -360,10 +367,14 @@ func parseGemini(b, rootUrl, currentUrl string) (string, []string) { links = append(links, link) linknum := fmt.Sprintf("[%d]", len(links)) - splitContent[i] = fmt.Sprintf("%-5s %s", linknum, decorator) + splitContent[outputIndex] = fmt.Sprintf("%-5s %s", linknum, decorator) + outputIndex++ + } else { + splitContent[outputIndex] = ln + outputIndex++ } } - return strings.Join(splitContent, "\n"), links + return strings.Join(splitContent[:outputIndex], "\n"), links } func handleRelativeUrl(u, root, current string) string { diff --git a/gopher/gopher.go b/gopher/gopher.go index fd5744f..20e76e9 100644 --- a/gopher/gopher.go +++ b/gopher/gopher.go @@ -131,13 +131,16 @@ func parseMap(text string) (string, []string) { if len(line[0]) > 1 { title = line[0][1:] + } else if len(line[0]) == 1 { + title = "" } else { title = "" + line[0] = "i" } - if len(line) > 1 && len(line[0]) > 0 && string(line[0][0]) == "i" { + if len(line) < 4 || strings.HasPrefix(line[0], "i") { splitContent[i] = " " + string(title) - } else if len(line) >= 4 { + } else { link := buildLink(line[2], line[3], string(line[0][0]), line[1]) links = append(links, link) linktext := fmt.Sprintf("(%s) %2d %s", getType(string(line[0][0])), len(links), title) diff --git a/main.go b/main.go index 1362e78..53918f7 100644 --- a/main.go +++ b/main.go @@ -65,6 +65,7 @@ func validateOpt(opt, val string) bool { "webmode": []string{"none", "gui", "lynx", "w3m", "elinks"}, "theme": []string{"normal", "inverse", "color"}, "defaultscheme": []string{"gopher", "gemini", "http", "https"}, + "showimages": []string{"true", "false"}, } opt = strings.ToLower(opt) @@ -83,7 +84,7 @@ func validateOpt(opt, val string) bool { func lowerCaseOpt(opt, val string) string { switch opt { - case "webmode", "theme", "defaultscheme": + case "webmode", "theme", "defaultscheme", "showimages": return strings.ToLower(val) default: return val diff --git a/page.go b/page.go index 08e75fb..4ca0842 100644 --- a/page.go +++ b/page.go @@ -3,6 +3,8 @@ package main import ( "fmt" "strings" + + "tildegit.org/sloum/bombadillo/tdiv" ) //------------------------------------------------\\ @@ -21,6 +23,9 @@ type Page struct { FoundLinkLines []int SearchTerm string SearchIndex int + FileType string + WrapWidth int + Color bool } //------------------------------------------------\\ @@ -47,11 +52,24 @@ func (p *Page) ScrollPositionRange(termHeight int) (int, int) { return p.ScrollPosition, end } +func (p *Page) RenderImage(width int) { + w := (width - 5) * 2 + if w > 300 { + w = 300 + } + p.WrappedContent = tdiv.Render([]byte(p.RawContent), w) + p.WrapWidth = width +} + // WrapContent performs a hard wrap to the requested // 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) { + if p.FileType == "image" { + p.RenderImage(width) + return + } counter := 0 var content strings.Builder var esc strings.Builder @@ -116,6 +134,8 @@ func (p *Page) WrapContent(width int, color bool) { } p.WrappedContent = strings.Split(content.String(), "\n") + p.WrapWidth = width + p.Color = color p.HighlightFoundText() } @@ -165,6 +185,6 @@ func (p *Page) FindText() { // MakePage returns a Page struct with default values func MakePage(url Url, content string, links []string) Page { - p := Page{make([]string, 0), content, links, url, 0, make([]int, 0), "", 0} + p := Page{make([]string, 0), content, links, url, 0, make([]int, 0), "", 0, "", 40, false} return p } diff --git a/pages.go b/pages.go index 46d962b..9fa6606 100644 --- a/pages.go +++ b/pages.go @@ -66,7 +66,11 @@ func (p *Pages) Render(termHeight, termWidth int, color bool) []string { } pos := p.History[p.Position].ScrollPosition prev := len(p.History[p.Position].WrappedContent) - p.History[p.Position].WrapContent(termWidth, color) + + if termWidth != p.History[p.Position].WrapWidth || p.History[p.Position].Color != color { + p.History[p.Position].WrapContent(termWidth, color) + } + now := len(p.History[p.Position].WrappedContent) if prev > now { diff := prev - now diff --git a/tdiv/tdiv.go b/tdiv/tdiv.go new file mode 100644 index 0000000..2ce3e2d --- /dev/null +++ b/tdiv/tdiv.go @@ -0,0 +1,290 @@ +package tdiv + +import ( + "bytes" + "fmt" + "image" + "image/gif" + "image/jpeg" + "image/png" + "io" + "strings" +) + +func getBraille(pattern string) (rune, error) { + switch pattern { + case "000000": + return ' ', nil + case "100000": + return '⠁', nil + case "001000": + return '⠂', nil + case "101000": + return '⠃', nil + case "000010": + return '⠄', nil + case "100010": + return '⠅', nil + case "001010": + return '⠆', nil + case "101010": + return '⠇', nil + case "010000": + return '⠈', nil + case "110000": + return '⠉', nil + case "011000": + return '⠊', nil + case "111000": + return '⠋', nil + case "010010": + return '⠌', nil + case "110010": + return '⠍', nil + case "011010": + return '⠎', nil + case "111010": + return '⠏', nil + case "000100": + return '⠐', nil + case "100100": + return '⠑', nil + case "001100": + return '⠒', nil + case "101100": + return '⠓', nil + case "000110": + return '⠔', nil + case "100110": + return '⠕', nil + case "001110": + return '⠖', nil + case "101110": + return '⠗', nil + case "010100": + return '⠘', nil + case "110100": + return '⠙', nil + case "011100": + return '⠚', nil + case "111100": + return '⠛', nil + case "010110": + return '⠜', nil + case "110110": + return '⠝', nil + case "011110": + return '⠞', nil + case "111110": + return '⠟', nil + case "000001": + return '⠠', nil + case "100001": + return '⠡', nil + case "001001": + return '⠢', nil + case "101001": + return '⠣', nil + case "000011": + return '⠤', nil + case "100011": + return '⠥', nil + case "001011": + return '⠦', nil + case "101011": + return '⠧', nil + case "010001": + return '⠨', nil + case "110001": + return '⠩', nil + case "011001": + return '⠪', nil + case "111001": + return '⠫', nil + case "010011": + return '⠬', nil + case "110011": + return '⠭', nil + case "011011": + return '⠮', nil + case "111011": + return '⠯', nil + case "000101": + return '⠰', nil + case "100101": + return '⠱', nil + case "001101": + return '⠲', nil + case "101101": + return '⠳', nil + case "000111": + return '⠴', nil + case "100111": + return '⠵', nil + case "001111": + return '⠶', nil + case "101111": + return '⠷', nil + case "010101": + return '⠸', nil + case "110101": + return '⠹', nil + case "011101": + return '⠺', nil + case "111101": + return '⠻', nil + case "010111": + return '⠼', nil + case "110111": + return '⠽', nil + case "011111": + return '⠾', nil + case "111111": + return '⠿', nil + default: + return '!', fmt.Errorf("Invalid character entry") + } +} + +// scaleImage loads and scales an image and returns a 2d pixel-int slice +// +// Adapted from: +// http://tech-algorithm.com/articles/nearest-neighbor-image-scaling/ +func scaleImage(file io.Reader, newWidth int) (int, int, [][]int, error) { + img, _, err := image.Decode(file) + if err != nil { + return 0, 0, nil, err + } + + bounds := img.Bounds() + width, height := bounds.Max.X, bounds.Max.Y + newHeight := int(float64(newWidth) * (float64(height) / float64(width))) + + out := make([][]int, newHeight) + for i := range out { + out[i] = make([]int, newWidth) + } + + xRatio := float64(width) / float64(newWidth) + yRatio := float64(height) / float64(newHeight) + var px, py int + for i := 0; i < newHeight; i++ { + for j := 0; j < newWidth; j++ { + px = int(float64(j) * xRatio) + py = int(float64(i) * yRatio) + out[i][j] = rgbaToGray(img.At(px, py).RGBA()) + } + } + return newWidth, newHeight, out, nil +} + +// Get the bi-dimensional pixel array +func getPixels(file io.Reader) (int, int, [][]int, error) { + img, _, err := image.Decode(file) + if err != nil { + return 0, 0, nil, err + } + + bounds := img.Bounds() + width, height := bounds.Max.X, bounds.Max.Y + + var pixels [][]int + for y := 0; y < height; y++ { + var row []int + for x := 0; x < width; x++ { + row = append(row, rgbaToGray(img.At(x, y).RGBA())) + } + pixels = append(pixels, row) + } + + return width, height, pixels, nil +} + +func errorDither(w, h int, p [][]int) [][]int { + mv := [4][2]int{ + [2]int{0, 1}, + [2]int{1, 1}, + [2]int{1, 0}, + [2]int{1, -1}, + } + per := [4]float64{0.4375, 0.0625, 0.3125, 0.1875} + var res, diff int + for y := 0; y < h; y++ { + for x := 0; x < w; x++ { + cur := p[y][x] + if cur > 128 { + res = 1 + diff = -(255 - cur) + } else { + res = 0 + diff = cur // TODO see why this was abs() in the py version + } + for i, v := range mv { + if y+v[0] >= h || x+v[1] >= w || x+v[1] <= 0 { + continue + } + px := p[y+v[0]][x+v[1]] + px = int(float64(diff)*per[i] + float64(px)) + if px < 0 { + px = 0 + } else if px > 255 { + px = 255 + } + p[y+v[0]][x+v[1]] = px + p[y][x] = res + } + } + } + return p +} + +func toBraille(p [][]int) []rune { + w := len(p[0]) // TODO this is unsafe + h := len(p) + rows := h / 3 + cols := w / 2 + out := make([]rune, rows*(cols+1)) + counter := 0 + for y := 0; y < h-3; y += 4 { + for x := 0; x < w-1; x += 2 { + str := fmt.Sprintf( + "%d%d%d%d%d%d", + p[y][x], p[y][x+1], + p[y+1][x], p[y+1][x+1], + p[y+2][x], p[y+2][x+1]) + b, err := getBraille(str) + if err != nil { + out[counter] = ' ' + } else { + out[counter] = b + } + counter++ + } + out[counter] = '\n' + counter++ + } + return out +} + +func rgbaToGray(r uint32, g uint32, b uint32, a uint32) int { + rf := float64(r/257) * 0.92126 + gf := float64(g/257) * 0.97152 + bf := float64(b/257) * 0.90722 + grey := int((rf + gf + bf) / 3) + return grey +} + +func Render(in []byte, width int) []string { + image.RegisterFormat("jpeg", "jpeg", jpeg.Decode, jpeg.DecodeConfig) + image.RegisterFormat("png", "png", png.Decode, png.DecodeConfig) + image.RegisterFormat("gif", "gif", gif.Decode, gif.DecodeConfig) + w, h, p, err := scaleImage(bytes.NewReader(in), width) + + if err != nil { + return []string{"Unable to render image.", "Please download using:", "", " :w ."} + } + px := errorDither(w, h, p) + b := toBraille(px) + out := strings.SplitN(string(b), "\n", -1) + return out +} diff --git a/url.go b/url.go index a6823fa..0831411 100644 --- a/url.go +++ b/url.go @@ -130,7 +130,7 @@ func MakeUrl(u string) (Url, error) { out.Mime = "1" } switch out.Mime { - case "1", "0", "h", "7": + case "1", "0", "h", "7", "I", "g": out.DownloadOnly = false default: out.DownloadOnly = true