Compare commits

...

57 Commits

Author SHA1 Message Date
Sloom Sloum Sluom IV abc6a170dc Merge pull request 'Develop -> Master' (#172) from develop into master 2020-05-30 12:52:48 -04:00
Sloom Sloum Sluom IV bc38cb8fb5 Merge pull request 'Release 2.3.1 -> Develop' (#171) from release-2.3.1 into develop 2020-05-30 12:51:03 -04:00
Sloom Sloum Sluom IV 3af583da9b Merge branch 'release-2.3.1' of https://tildegit.org/sloum/bombadillo into release-2.3.1 2020-05-28 22:31:39 -07:00
Sloom Sloum Sluom IV 573eb0d230 Fixes issue in makefile that made it so VERSION file was never referenced 2020-05-28 22:30:48 -07:00
Sloom Sloum Sluom IV f683845136 Merge pull request 'Adds a release target to makefile' (#170) from make-release into release-2.3.1
Merging in since there is no code to the application here and none of the core makefile targets are altered.
2020-05-28 22:34:30 -04:00
Sloom Sloum Sluom IV d7a08268b0 Updates version info 2020-05-27 22:39:44 -07:00
Sloom Sloum Sluom IV 733893edc0 Adds prerequisit to readme 2020-05-27 22:31:23 -07:00
Sloom Sloum Sluom IV 15d67ef230 Adds a make target for release and updates clean to clean it up 2020-05-27 22:29:41 -07:00
Sloom Sloum Sluom IV b8721366a2 Merge pull request 'Alternative way of rendering when bookmarks are not open' (#167) from rendering-fix into release-2.3.1
Since this has been reviewed as functional I am merging into the release branch. There will be another opportunity to review, if need be, for the release branch when a PR is made into `develop`.
2020-05-28 00:50:47 -04:00
Sloom Sloum Sluom IV 7a0506853a Merge pull request 'Fixes gemini relative links to work for all relative link types' (#168) from fix-relative-links into release-2.3.1
Since this has been reviewed as functional I am merging into the release branch. There will be another opportunity to review, if need be, for the release branch when a PR is made into `develop`.
2020-05-28 00:50:13 -04:00
Sloom Sloum Sluom IV 38144a0e2a Merge pull request 'Adds in the correct variable when checking for existing querystring value' (#166) from fix-repeated-query into release-2.3.1
Since this has been reviewed as functional I am merging into the release branch. There will be another opportunity to review, if need be, for the release branch when a PR is made into `develop`.
2020-05-28 00:49:28 -04:00
Sloom Sloum Sluom IV c175b45cb9 Switches relative URLs to RelativeReference lib method 2020-05-26 21:46:17 -07:00
sloum cba3682c27 Adds up one dir relative linking fix 2020-05-26 15:10:12 -07:00
sloum 0227523487 Adds query escaping 2020-05-25 19:43:19 -07:00
sloum f47f6597f4 Adds in the correct variable when checking for existing querystring value 2020-05-25 19:12:52 -07:00
sloum c9d634499b Alternative way of rendering when bookmarks are not open 2020-05-25 09:49:34 -07:00
Sloom Sloum Sluom IV e06de9bac1 Merge pull request 'Release of v.2.3.0 to master' (#160) from develop into master 2020-05-24 12:47:34 -04:00
Sloom Sloum Sluom IV f793bdd806 Merge pull request 'Release candidate for 2.3.0' (#147) from release-2.3.0 into develop
Alright. This is a big one for Gemini. I am going to merge in. :)
2020-05-24 12:42:27 -04:00
Sloom Sloum Sluom IV 47863519a2 Merge pull request 'Fixes querystring chaining issue' (#159) from querystring into release-2.3.0 2020-05-24 12:40:30 -04:00
sloum 90cf1d6681 Resolves merge conflict 2020-05-24 09:39:29 -07:00
sloum 0d8b3e015b Fixes querystring chaining issue 2020-05-24 07:45:34 -07:00
Sloom Sloum Sluom IV cf340edc36 Merge pull request 'Replace calls to `stty` with syscalls' (#158) from dancek/bombadillo:replace-stty into release-2.3.0 2020-05-24 10:23:17 -04:00
Hannu Hartikainen e3e2afc4fc Replace calls to `stty` with syscalls
- Move termios-related functionality to its own package
- Reimplement stty calls using ioctl calls
- Add per-platform constants for linux and non-linux because the ioctl
  enums are different. The enums in Darwin seem to come from BSD and so
  I'm assuming they might also work for other operating systems. If not,
  we'll need to add other build-constrained const files.

This code has been tested on Darwin and will be tested on Linux before
merging.
2020-05-24 09:25:38 +03:00
Sloom Sloum Sluom IV ba38b78ca6 Merge pull request 'Adds option to handle preformatted code blocks in different ways for gemini' (#148) from gemini-alt-text into release-2.3.0 2020-05-22 17:49:58 -04:00
sloum 0257ca92b1 Adds clarification to man page and removes old comment 2020-05-19 14:40:56 -07:00
Sloom Sloum Sluom IV c34226e4c1 Merge pull request 'Reworks how relative URLs are handled for gemini' (#153) from pathing-fixes into release-2.3.0 2020-05-19 10:36:48 -04:00
sloum 64590f53e7 Resolved merge conflict 2020-05-18 20:14:32 -07:00
sloum b9057508d9 Reworks how relative URLs are handled for gemini 2020-05-18 20:10:02 -07:00
sloum 9f1eb632bc Adds geminiblocks to the man page 2020-05-17 22:01:31 -07:00
sloum b23b9b3121 Fixes the workflow for allowing alt text and handling preformatted blocks 2020-05-17 21:36:49 -07:00
sloum 909131cda8 Lets images be full width 2020-05-15 22:25:56 -07:00
sloum 2bb8272cf9 Combines a few features into a release branch 2020-05-15 22:22:32 -07:00
sloum c508498b42 Merge branch 'gemini-cert-expiry' of https://tildegit.org/sloum/Bombadillo into release-2.3.0 2020-05-15 22:21:38 -07:00
sloum 322002ba66 Adds a max width of 100, anything more gets weird to read. Also adds a slight optimization. 2020-05-15 22:19:09 -07:00
sloum a23e0026fa Adds command aliasing to vim keys for forward and back 2020-05-15 17:32:08 -07:00
sloum c58b40def2 Removes goroutines in search that were causing input issues on gemini cgi scripts 2020-05-14 08:28:39 -07:00
sloum 00313442d4 Hopefully an improvement to the initial way of dealing with expired certs 2020-05-09 16:18:38 -07:00
sloum cb151f75aa Handles cert expirations silently 2020-05-09 11:04:06 -07:00
Sloom Sloum Sluom IV 36ae4a228f Merge pull request 'Release setup for 2.2.1' (#141) from release-setup-2.2.1 into develop 2020-04-23 13:44:46 -04:00
sloum 961bdfc92f Minor release with improved term handling and quick link navigation 2020-04-23 10:41:32 -07:00
sloum 26b3223379 Merge branch 'short-links' of https://tildegit.org/sloum/Bombadillo into release-setup-2.2.1 2020-04-23 10:40:26 -07:00
sloum cdfec887fd Solves the issue where line wrapping is not turned back on 2020-04-14 18:26:03 +00:00
sloum bfb6b85844 Updates man page with quick link information 2020-04-12 22:57:50 -07:00
sloum 9af1a4d642 Adds quick-link navigation by number keys 2020-04-12 22:53:08 -07:00
Sloom Sloum Sluom IV 61ae2859bf Merge pull request 'Release 2.2.0' (#140) from develop into master 2020-03-23 01:29:16 -04:00
Sloom Sloum Sluom IV 23bc3d75a5 Merge pull request 'Release 2.1.4' (#139) from release214 into develop 2020-03-23 01:27:58 -04:00
sloum 8b7441ea17 Updated version 2020-03-22 22:27:38 -07:00
Sloom Sloum Sluom IV b2d6e2ff1e Merge pull request 'Adds support for gemini tripple backticks' (#138) from gemini-spec-update into release214 2020-03-05 00:55:09 -05:00
sloum 021f1290d4 Adds support for gemini tripple backticks 2020-03-04 21:51:54 -08:00
Sloom Sloum Sluom IV ca552bf383 Merge pull request 'Adds support for image rendering in the terminal' (#137) from render-images into release214 2020-03-05 00:33:35 -05:00
sloum cd1be92d34 Changing default to true 2020-03-04 21:27:21 -08:00
sloum 7edf01eb99 Adds a color property to the page struct to track the color mode 2020-02-15 10:51:39 -08:00
sloum 4f9c8877b5 Removes unneeded comment 2020-02-15 10:44:41 -08:00
sloum cda502fbb8 Adds a WrapWidth param to the page struct so that pages.go does not have to rewrap on every render. 2020-02-15 08:43:54 -08:00
sloum 996e209c50 Leaves a note re: improving render speeds 2020-02-13 22:46:39 -08:00
sloum c9765bf0fa Adds lo-fi image rendering to Bombadillo 2020-02-13 21:26:23 -08:00
sloum 53cbb9dd02 Improves upon previoius fixes to gophermap parsing 2020-02-01 09:55:55 -08:00
17 changed files with 607 additions and 116 deletions

View File

@ -14,7 +14,7 @@ test : GOCMD := go1.11.13
BUILD_TIME := ${shell date "+%Y-%m-%dT%H:%M%z"}
# If VERSION is empty or not defined use the contents of the VERSION file
VERSION := ${shell git describe --tags 2> /dev/null}
VERSION := ${shell git describe --exact-match 2> /dev/null}
ifndef VERSION
VERSION := ${shell cat ./VERSION}
endif
@ -57,6 +57,7 @@ install-bin: build
clean:
${GOCMD} clean
rm -f ./bombadillo.1.gz 2> /dev/null
rm -f ./${BINARY}_* 2> /dev/null
.PHONY: uninstall
uninstall: clean
@ -66,6 +67,13 @@ uninstall: clean
rm -f ${DESTDIR}${DATAROOTDIR}/pixmaps/bombadillo-icon.png
-update-desktop-database 2> /dev/null
.PHONY: release
release:
GOOS=linux GOARCH=amd64 ${GOCMD} build ${LDFLAGS} -o ${BINARY}_linux_64
GOOS=linux GOARCH=arm ${GOCMD} build ${LDFLAGS} -o ${BINARY}_linux_arm
GOOS=linux GOARCH=386 ${GOCMD} build ${LDFLAGS} -o ${BINARY}_linux_32
GOOS=darwin GOARCH=amd64 ${GOCMD} build ${LDFLAGS} -o ${BINARY}_darwin_64
.PHONY: test
test: clean build

View File

@ -27,6 +27,7 @@ These instructions will get a copy of the project up and running on your local m
### Prerequisites
You will need to have [Go](https://golang.org/) version >= 1.11.
To use the Makefile you will need a make that is GNU Make compatible (sorry BSD folks)
### Building, Installing, Uninstalling

View File

@ -1 +1 @@
2.1.3
2.3.1

View File

@ -59,7 +59,7 @@ Displaying web content directly in \fBbombadillo\fP requires lynx, w3m or elinks
These commands work as a single keypress anytime \fBbombadillo\fP is not taking in a line based command or when the user is being prompted for action. This is the default command mode of \fBbombadillo\fP.
.TP
.B
b
b, h
Navigate back one place in your document history.
.TP
.B
@ -71,7 +71,7 @@ d
Scroll down an amount corresponding to 75% of your terminal window height in the current document.
.TP
.B
f
f, l
Navigate forward one place in your document history.
.TP
.B
@ -95,6 +95,7 @@ n
Jump to next found text item.
.TP
.B
N
Jump to previous found text item.
.TP
.B
@ -106,6 +107,10 @@ R
Reload the current page (does not destroy forward history).
.TP
.B
1, 2, 3, 4, 5, 6, 7, 8, 9, 0
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
Scroll up an amount corresponding to 75% of your terminal window height in the current document.
.TP
@ -228,6 +233,10 @@ defaultscheme
The scheme that should be used when no scheme is present in a given URL. \fIgopher\fP, \fIgemini\fP, \fIhttp\fP, and \fIhttps\fP are valid values.
.TP
.B
geminiblocks
Determines how to treat preformatted text blocks in text/gemini documents. \fIblock\fP will show the contents of the block, \fIalt\fP will show any available alt text for the block, \fIboth\fP will show both the content and the alt text, and \fIneither\fP will show neither. Unlike other settings, a change to this value will require a fresh page load to see the change.
.TP
.B
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

110
client.go
View File

@ -3,8 +3,8 @@ package main
import (
"fmt"
"io/ioutil"
"net/url"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
@ -19,6 +19,7 @@ import (
"tildegit.org/sloum/bombadillo/http"
"tildegit.org/sloum/bombadillo/local"
"tildegit.org/sloum/bombadillo/telnet"
"tildegit.org/sloum/bombadillo/termios"
)
//------------------------------------------------\\
@ -43,14 +44,7 @@ type client struct {
//--------------------------------------------------\\
func (c *client) GetSizeOnce() {
cmd := exec.Command("stty", "size")
cmd.Stdin = os.Stdin
out, err := cmd.Output()
if err != nil {
cui.Exit(5, "Fatal error: Unable to retrieve terminal size")
}
var h, w int
_, _ = fmt.Sscan(string(out), &h, &w)
var w, h = termios.GetWindowSize()
c.Height = h
c.Width = w
}
@ -61,14 +55,7 @@ func (c *client) GetSize() {
c.Draw()
for {
cmd := exec.Command("stty", "size")
cmd.Stdin = os.Stdin
out, err := cmd.Output()
if err != nil {
cui.Exit(5, "Fatal error: Unable to retrieve terminal size")
}
var h, w int
_, _ = fmt.Sscan(string(out), &h, &w)
var w, h = termios.GetWindowSize()
if h != c.Height || w != c.Width {
c.Height = h
c.Width = w
@ -135,15 +122,11 @@ func (c *client) Draw() {
} else {
for i := 0; i < c.Height-3; i++ {
if i < len(pageContent) {
extra := 0
escapes := re.FindAllString(pageContent[i], -1)
for _, esc := range escapes {
extra += len(esc)
}
screen.WriteString(fmt.Sprintf("%-*.*s", c.Width+extra, c.Width+extra, pageContent[i]))
screen.WriteString(pageContent[i])
screen.WriteString("\033[0K")
screen.WriteString("\n")
} else {
screen.WriteString(fmt.Sprintf("%-*.*s", c.Width, c.Width, " "))
screen.WriteString("\033[0K")
screen.WriteString("\n")
}
}
@ -162,15 +145,21 @@ func (c *client) TakeControlInput() {
input := cui.Getch()
switch input {
case 'j', 'J':
case '1', '2', '3', '4', '5', '6', '7', '8', '9', '0':
if input == '0' {
c.goToLink("10")
} else {
c.goToLink(string(input))
}
case 'j':
// scroll down one line
c.ClearMessage()
c.Scroll(1)
case 'k', 'K':
case 'k':
// scroll up one line
c.ClearMessage()
c.Scroll(-1)
case 'q', 'Q':
case 'q':
// quit bombadillo
cui.Exit(0, "")
case 'g':
@ -191,7 +180,7 @@ func (c *client) TakeControlInput() {
c.ClearMessage()
distance := c.Height - c.Height/4
c.Scroll(-distance)
case 'b':
case 'b', 'h':
// go back
c.ClearMessage()
err := c.PageState.NavigateHistory(-1)
@ -216,7 +205,7 @@ func (c *client) TakeControlInput() {
// open the bookmarks browser
c.BookMarks.ToggleOpen()
c.Draw()
case 'f', 'F':
case 'f', 'l':
// go forward
c.ClearMessage()
err := c.PageState.NavigateHistory(1)
@ -464,6 +453,8 @@ func (c *client) doCommandAs(action string, values []string) {
c.Options[values[0]] = lowerCaseOpt(values[0], val)
if values[0] == "tlskey" || values[0] == "tlscertificate" {
c.Certs.LoadCertificate(c.Options["tlscertificate"], c.Options["tlskey"])
} else if values[0] == "geminiblocks" {
gemini.BlockBehavior = c.Options[values[0]]
} else if values[0] == "configlocation" {
c.SetMessage("Cannot set READ ONLY setting 'configlocation'", true)
c.DrawMessage()
@ -670,7 +661,7 @@ func (c *client) doLinkCommand(action, target string) {
}
func (c *client) search(query, url, question string) {
func (c *client) search(query, uri, question string) {
var entry string
var err error
if query == "" {
@ -694,22 +685,32 @@ func (c *client) search(query, url, question string) {
} else {
entry = query
}
if url == "" {
url = c.Options["searchengine"]
if uri == "" {
uri = c.Options["searchengine"]
}
u, err := MakeUrl(url)
u, err := MakeUrl(uri)
if err != nil {
c.SetMessage("The search url is not a valid url", true)
c.SetMessage("The search url is not valid", true)
c.DrawMessage()
return
}
var rootUrl string
switch u.Scheme {
case "gopher":
go c.Visit(fmt.Sprintf("%s\t%s", u.Full, entry))
if ind := strings.Index(u.Full, "\t"); ind >= 0 {
rootUrl = u.Full[:ind]
} else {
rootUrl = u.Full
}
c.Visit(fmt.Sprintf("%s\t%s", rootUrl, entry))
case "gemini":
// TODO url escape the entry variable
escapedEntry := entry
go c.Visit(fmt.Sprintf("%s?%s", u.Full, escapedEntry))
if ind := strings.Index(u.Full, "?"); ind >= 0 {
rootUrl = u.Full[:ind]
} else {
rootUrl = u.Full
}
escapedEntry := url.PathEscape(entry)
c.Visit(fmt.Sprintf("%s?%s", rootUrl, escapedEntry))
case "http", "https":
c.Visit(u.Full)
default:
@ -929,7 +930,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 +948,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()
@ -966,10 +972,13 @@ func (c *client) handleGemini(u Url) {
go saveConfig()
switch capsule.Status {
case 1:
// Query
c.search("", u.Full, capsule.Content)
case 2:
if capsule.MimeMaj == "text" {
// Success
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()
@ -984,14 +993,21 @@ func (c *client) handleGemini(u Url) {
c.saveFileFromData(capsule.Content, filename)
}
case 3:
c.SetMessage(fmt.Sprintf("Follow redirect (y/n): %s?", capsule.Content), false)
c.DrawMessage()
ch := cui.Getch()
if ch == 'y' || ch == 'Y' {
// Redirect
lowerRedirect := strings.ToLower(capsule.Content)
lowerOriginal := strings.ToLower(u.Full)
if strings.Replace(lowerRedirect, lowerOriginal, "", 1) == "/" {
c.Visit(capsule.Content)
} else {
c.SetMessage("Redirect aborted", false)
c.SetMessage(fmt.Sprintf("Follow redirect (y/n): %s?", capsule.Content), false)
c.DrawMessage()
ch := cui.Getch()
if ch == 'y' || ch == 'Y' {
c.Visit(capsule.Content)
} else {
c.SetMessage("Redirect aborted", false)
c.DrawMessage()
}
}
}
}
@ -1018,6 +1034,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

@ -5,6 +5,8 @@ import (
"fmt"
"os"
"os/exec"
"tildegit.org/sloum/bombadillo/termios"
)
var Shapes = map[string]string{
@ -55,16 +57,17 @@ func Exit(exitCode int, msg string) {
// InitTerm sets the terminal modes appropriate for Bombadillo
func InitTerm() {
SetCharMode()
Tput("rmam") // turn off line wrapping
Tput("smcup") // use alternate screen
termios.SetCharMode()
Tput("smcup") // use alternate screen
Tput("rmam") // turn off line wrapping
fmt.Print("\033[?25l") // hide cursor
}
// CleanupTerm reverts changs to terminal mode made by InitTerm
func CleanupTerm() {
moveCursorToward("down", 500)
moveCursorToward("right", 500)
SetLineMode()
termios.SetLineMode()
fmt.Print("\n")
fmt.Print("\033[?25h") // reenables cursor blinking
@ -98,7 +101,7 @@ func Getch() rune {
}
func GetLine(prefix string) (string, error) {
SetLineMode()
termios.SetLineMode()
reader := bufio.NewReader(os.Stdin)
fmt.Print(prefix)
@ -107,32 +110,10 @@ func GetLine(prefix string) (string, error) {
return "", err
}
SetCharMode()
termios.SetCharMode()
return text[:len(text)-1], nil
}
func SetCharMode() {
cmd := exec.Command("stty", "cbreak", "-echo")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
err := cmd.Run()
if err != nil {
panic(err)
}
fmt.Print("\033[?25l")
}
func SetLineMode() {
cmd := exec.Command("stty", "-cbreak", "echo")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
err := cmd.Run()
if err != nil {
panic(err)
}
}
func Tput(opt string) {
cmd := exec.Command("tput", opt)
cmd.Stdin = os.Stdin

View File

@ -45,12 +45,14 @@ var defaultOptions = map[string]string{
// the "configlocation" as follows:
// "configlocation": xdgConfigPath()
"configlocation": xdgConfigPath(),
"defaultscheme": "gopher", // "gopher", "gemini", "http", "https"
"geminiblocks": "block", // "block", "alt", "neither", "both"
"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

@ -6,6 +6,7 @@ import (
"crypto/tls"
"fmt"
"io/ioutil"
"net/url"
"strconv"
"strings"
"time"
@ -24,6 +25,8 @@ type TofuDigest struct {
ClientCert tls.Certificate
}
var BlockBehavior = "block"
//------------------------------------------------\\
// + + + R E C E I V E R S + + + \\
//--------------------------------------------------\\
@ -49,8 +52,8 @@ func (t *TofuDigest) Purge(host string) error {
return fmt.Errorf("Invalid host %q", host)
}
func (t *TofuDigest) Add(host, hash string) {
t.certs[strings.ToLower(host)] = hash
func (t *TofuDigest) Add(host, hash string, time int64) {
t.certs[strings.ToLower(host)] = fmt.Sprintf("%s|%d", hash, time)
}
func (t *TofuDigest) Exists(host string) bool {
@ -67,12 +70,11 @@ func (t *TofuDigest) Find(host string) (string, error) {
return "", fmt.Errorf("Invalid hostname, no key saved")
}
func (t *TofuDigest) Match(host string, cState *tls.ConnectionState) error {
host = strings.ToLower(host)
func (t *TofuDigest) Match(host, localCert string, cState *tls.ConnectionState) error {
now := time.Now()
for _, cert := range cState.PeerCertificates {
if t.certs[host] != hashCert(cert.Raw) {
if localCert != hashCert(cert.Raw) {
continue
}
@ -118,13 +120,40 @@ func (t *TofuDigest) newCert(host string, cState *tls.ConnectionState) error {
continue
}
t.Add(host, hashCert(cert.Raw))
t.Add(host, hashCert(cert.Raw), cert.NotAfter.Unix())
return nil
}
return fmt.Errorf(reasons.String())
}
func (t *TofuDigest) GetCertAndTimestamp(host string) (string, int64, error) {
certTs, err := t.Find(host)
if err != nil {
return "", -1, err
}
certTsSplit := strings.SplitN(certTs, "|", -1)
if len(certTsSplit) < 2 {
_ = t.Purge(host)
return certTsSplit[0], -1, fmt.Errorf("Invalid certstring, no delimiter")
}
ts, err := strconv.ParseInt(certTsSplit[1], 10, 64)
if err != nil {
_ = t.Purge(host)
return certTsSplit[0], -1, err
}
now := time.Now()
if ts < now.Unix() {
// Ignore error return here since an error would indicate
// the host does not exist and we have already checked for
// that and the desired outcome of the action is that the
// host will no longer exist, so we are good either way
_ = t.Purge(host)
return "", -1, fmt.Errorf("Expired cert")
}
return certTsSplit[0], ts, nil
}
func (t *TofuDigest) IniDump() string {
if len(t.certs) < 1 {
return ""
@ -176,9 +205,11 @@ func Retrieve(host, port, resource string, td *TofuDigest) (string, error) {
return "", fmt.Errorf("Insecure, no certificates offered by server")
}
if td.Exists(host) {
localCert, localTs, err := td.GetCertAndTimestamp(host)
if localTs > 0 {
// See if we have a matching cert
err := td.Match(host, &connState)
err := td.Match(host, localCert, &connState)
if err != nil && err.Error() != "EXP" {
// If there is no match and it isnt because of an expiration
// just return the error
@ -313,8 +344,7 @@ func Visit(host, port, resource string, td *TofuDigest) (Capsule, error) {
resource = "/"
}
currentUrl := fmt.Sprintf("gemini://%s:%s%s", host, port, resource)
rootUrl := fmt.Sprintf("gemini://%s:%s", host, port)
capsule.Content, capsule.Links = parseGemini(body, rootUrl, currentUrl)
capsule.Content, capsule.Links = parseGemini(body, currentUrl)
} else {
capsule.Content = body
}
@ -335,13 +365,27 @@ func Visit(host, port, resource string, td *TofuDigest) (Capsule, error) {
}
}
func parseGemini(b, rootUrl, currentUrl string) (string, []string) {
func parseGemini(b, currentUrl string) (string, []string) {
splitContent := strings.Split(b, "\n")
links := make([]string, 0, 10)
inPreBlock := false
outputIndex := 0
for i, ln := range splitContent {
splitContent[i] = strings.Trim(ln, "\r\n")
if len([]rune(ln)) > 3 && ln[:2] == "=>" {
isPreBlockDeclaration := strings.HasPrefix(ln, "```")
if isPreBlockDeclaration && !inPreBlock && (BlockBehavior == "both" || BlockBehavior == "alt") {
inPreBlock = !inPreBlock
alt := strings.TrimSpace(ln)
if len(alt) > 3 {
alt = strings.TrimSpace(alt[3:])
splitContent[outputIndex] = fmt.Sprintf("[ %s ]", alt)
outputIndex++
}
} else if isPreBlockDeclaration {
inPreBlock = !inPreBlock
} else if len([]rune(ln)) > 3 && ln[:2] == "=>" && !inPreBlock {
var link, decorator string
subLn := strings.Trim(ln[2:], "\r\n\t \a")
splitPoint := strings.IndexAny(subLn, " \t")
@ -355,33 +399,35 @@ func parseGemini(b, rootUrl, currentUrl string) (string, []string) {
}
if strings.Index(link, "://") < 0 {
link = handleRelativeUrl(link, rootUrl, currentUrl)
link, _ = handleRelativeUrl(link, currentUrl)
}
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 {
if inPreBlock && (BlockBehavior == "alt" || BlockBehavior == "neither") {
continue
}
splitContent[outputIndex] = ln
outputIndex++
}
}
return strings.Join(splitContent, "\n"), links
return strings.Join(splitContent[:outputIndex], "\n"), links
}
func handleRelativeUrl(u, root, current string) string {
if len(u) < 1 {
return u
// handleRelativeUrl provides link completion
func handleRelativeUrl(relLink, current string) (string, error) {
base, err := url.Parse(current)
if err != nil {
return relLink, err
}
if u[0] == '/' {
return fmt.Sprintf("%s%s", root, u)
rel, err := url.Parse(relLink)
if err != nil {
return relLink, err
}
ind := strings.LastIndex(current, "/")
if ind < 10 {
return fmt.Sprintf("%s/%s", root, u)
}
current = current[:ind+1]
return fmt.Sprintf("%s%s", current, u)
return base.ResolveReference(rel).String(), nil
}
func hashCert(cert []byte) string {

View File

@ -131,9 +131,11 @@ 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 "
line[0] = "i"
}
if len(line) < 4 || strings.HasPrefix(line[0], "i") {

26
main.go
View File

@ -25,12 +25,14 @@ import (
"os"
"os/signal"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
"tildegit.org/sloum/bombadillo/config"
"tildegit.org/sloum/bombadillo/cui"
_ "tildegit.org/sloum/bombadillo/gemini"
"tildegit.org/sloum/bombadillo/gemini"
)
var version string
@ -65,6 +67,8 @@ 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"},
"geminiblocks": []string{"block", "neither", "alt", "both"},
}
opt = strings.ToLower(opt)
@ -83,7 +87,7 @@ func validateOpt(opt, val string) bool {
func lowerCaseOpt(opt, val string) string {
switch opt {
case "webmode", "theme", "defaultscheme":
case "webmode", "theme", "defaultscheme", "showimages", "geminiblocks":
return strings.ToLower(val)
default:
return val
@ -120,6 +124,9 @@ func loadConfig() {
if _, ok := bombadillo.Options[lowerkey]; ok {
if validateOpt(lowerkey, v.Value) {
bombadillo.Options[lowerkey] = v.Value
if lowerkey == "geminiblocks" {
gemini.BlockBehavior = v.Value
}
} else {
bombadillo.Options[lowerkey] = defaultOptions[lowerkey]
}
@ -131,7 +138,20 @@ func loadConfig() {
}
for _, v := range settings.Certs {
bombadillo.Certs.Add(v.Key, v.Value)
// Remove expired certs
vals := strings.SplitN(v.Value, "|", -1)
if len(vals) < 2 {
continue
}
ts, err := strconv.ParseInt(vals[1], 10, 64)
now := time.Now()
if err != nil || now.Unix() > ts {
continue
}
// Satisfied that the cert is not expired
// or malformed: add to the current client
// instance
bombadillo.Certs.Add(v.Key, vals[0], ts)
}
}

32
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,12 +52,27 @@ 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
}
width = min(width, 100)
counter := 0
spacer := " "
var content strings.Builder
var esc strings.Builder
escape := false
@ -106,7 +126,6 @@ func (p *Page) WrapContent(width int, color bool) {
content.WriteRune('\n')
counter = 0
if p.Location.Mime == "1" {
spacer := " "
content.WriteString(spacer)
counter += len(spacer)
}
@ -116,6 +135,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 +186,13 @@ 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
}
func min(a, b int) int {
if a < b {
return a
}
return b
}

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
}

10
termios/consts_linux.go Normal file
View File

@ -0,0 +1,10 @@
// +build linux
package termios
import "syscall"
const (
getTermiosIoctl = syscall.TCGETS
setTermiosIoctl = syscall.TCSETS
)

View File

@ -0,0 +1,10 @@
// +build !linux
package termios
import "syscall"
const (
getTermiosIoctl = syscall.TIOCGETA
setTermiosIoctl = syscall.TIOCSETAF
)

60
termios/termios.go Normal file
View File

@ -0,0 +1,60 @@
package termios
import (
"os"
"runtime"
"syscall"
"unsafe"
)
type winsize struct {
Row uint16
Col uint16
Xpixel uint16
Ypixel uint16
}
var fd = os.Stdin.Fd()
func ioctl(fd, request, argp uintptr) error {
if _, _, e := syscall.Syscall(syscall.SYS_IOCTL, fd, request, argp); e != 0 {
return e
}
return nil
}
func GetWindowSize() (int, int) {
var value winsize
ioctl(fd, syscall.TIOCGWINSZ, uintptr(unsafe.Pointer(&value)))
return int(value.Col), int(value.Row)
}
func getTermios() syscall.Termios {
var value syscall.Termios
err := ioctl(fd, getTermiosIoctl, uintptr(unsafe.Pointer(&value)))
if err != nil {
panic(err)
}
return value
}
func setTermios(termios syscall.Termios) {
err := ioctl(fd, setTermiosIoctl, uintptr(unsafe.Pointer(&termios)))
if err != nil {
panic(err)
}
runtime.KeepAlive(termios)
}
func SetCharMode() {
t := getTermios()
t.Lflag = t.Lflag ^ syscall.ICANON
t.Lflag = t.Lflag ^ syscall.ECHO
setTermios(t)
}
func SetLineMode() {
var t = getTermios()
t.Lflag = t.Lflag | (syscall.ICANON | syscall.ECHO)
setTermios(t)
}

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