Merge pull request 'Release 2.2.0' (#140) from develop into master

This commit is contained in:
Sloom Sloum Sluom IV 2020-03-23 01:29:16 -04:00
commit 61ae2859bf
12 changed files with 419 additions and 19 deletions

64
DEVELOPING.md Normal file
View File

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

View File

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

View File

@ -1 +1 @@
2.1.0
2.2.0

View File

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

View File

@ -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": "",

View File

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

View File

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

View File

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

22
page.go
View File

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

View File

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

290
tdiv/tdiv.go Normal file
View File

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

2
url.go
View File

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