Compare commits

..

1 Commits

Author SHA1 Message Date
sloum cb55293bdb Makes search insensitive 2020-03-17 05:19:26 +00:00
36 changed files with 286 additions and 1501 deletions

1
.gitignore vendored
View File

@ -1,3 +1,2 @@
bombadillo
*.asciinema
*.swp

View File

@ -25,10 +25,10 @@ Changes are implemented to the default branch when:
### Process for introducing a new change
Before you begin, 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.
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 current release branch (confirm this in the issue comments before proceeding).
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.

View File

@ -13,16 +13,24 @@ test : GOCMD := go1.11.13
# %:z - so settle for %z.
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}
ifndef VERSION
VERSION := ${shell cat ./VERSION}
endif
LDFLAGS := -ldflags "-s -X main.version=${VERSION} -X main.build=${BUILD_TIME}"
.PHONY: build
build:
${GOCMD} build -o ${BINARY}
${GOCMD} build ${LDFLAGS} -o ${BINARY}
.PHONY: install
install: install-bin install-man install-desktop clean
.PHONY: install-man
install-man: bombadillo.1
gzip -c ./bombadillo.1 > ./bombadillo.1.gz
gzip -k ./bombadillo.1
install -d ${DESTDIR}${MAN1DIR}
install -m 0644 ./bombadillo.1.gz ${DESTDIR}${MAN1DIR}
@ -49,7 +57,6 @@ install-bin: build
clean:
${GOCMD} clean
rm -f ./bombadillo.1.gz 2> /dev/null
rm -f ./${BINARY}_* 2> /dev/null
.PHONY: uninstall
uninstall: clean
@ -59,13 +66,6 @@ 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,7 +27,6 @@ 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.3.3
2.1.3

View File

@ -33,7 +33,7 @@ Gopher is the default protocol for \fBbombadillo\fP. Any textual item types will
.TP
.B
gemini
Gemini is supported, but as a new protocol with an incomplete specification, features may change over time. At present Bombadillo supports TLS with a trust on first use certificate pinning system (similar to SSH). Gemini maps and other text types are rendered in the browser and non-text types will be downloaded.
Gemini is supported, but as a new protocol with an incomplete specification, features may change over time. At present Bombadillo supports TLS with a trust on first use certificate pinning system (similar to SSH). Client certificates are also supported as a configurable option. Gemini maps and other text types are rendered in the browser and non-text types will be downloaded.
.TP
.B
finger
@ -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, h
b
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, l
f
Navigate forward one place in your document history.
.TP
.B
@ -95,7 +95,6 @@ n
Jump to next found text item.
.TP
.B
N
Jump to previous found text item.
.TP
.B
@ -107,14 +106,6 @@ 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
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
@ -185,14 +176,6 @@ home
Navigates to the document set by the \fIhomeurl\fP setting. \fIh\fP can be entered, rather than the full \fIhome\fP.
.TP
.B
jump
Navigates to the previous page in history from the current page. Useful for keeping the current page in your history while still browsing. \fIj\fP can be used instead of the full \fIjump\fP.
.TP
.B
jump [history location]
Navigates to the given history location. The history location should be an integer between 0 and 20. \fIj\fP can be used instead of the full \fIjump\fP.
.TP
.B
purge *
Deletes all pinned gemini server certificates. \fIp\fP can be used instead of the full \fIpurge\fP.
.TP
@ -221,10 +204,6 @@ set [setting name] [value]
Sets the value for a given configuration setting. \fIs\fP can be used instead of the full \fIset\fP.
.TP
.B
version
Shows the current Bombadillo version number.
.TP
.B
write .
Writes the current document to a file. The file is named by the last component of the url path. If the last component is blank or \fI/\fP a default name will be used. The file saves to the directory set by the \fIsavelocation\fP setting. \fIw\fP can be entered rather than the full \fIwrite\fP.
.TP
@ -249,24 +228,16 @@ 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
.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
.B
searchengine
The url to use for the LINE COMMAND \fIsearch\fP. Should be a valid search path that terms may be appended to.
The url to use for the LINE COMMANDs \fI?\fP and \fIsearch\fP. Should be a valid search path that terms may be appended to.
.TP
.B
telnetcommand
@ -277,8 +248,12 @@ theme
Can toggle between visual modes. Valid values are \fInormal\fP, \fIcolor\fP, and \fIinverse\fP. When set to inverse, the normal mode colors are inverted. Both normal and inverse modes filter out terminal escape sequences. When set to color, Bombadillo will render terminal escape sequences representing colors when it finds them in documents.
.TP
.B
timeout
The number of seconds after which connections to gopher or gemini servers should time out if the server has not responded.
tlscertificate
A path to a tls certificate file on a user's local filesystem. Defaults to NULL. Both \fItlscertificate\fP and \fItlskey\fP must be set for client certificates to work in gemini.
.TP
.B
tlskey
A path to a tls key that pairs with the tlscertificate setting, on a user's local filesystem. Defaults to NULL. Both \fItlskey\fP and \fItlscertificate\fP must be set for client certificates to work in gemini.
.TP
.B
webmode

View File

@ -1,18 +1,3 @@
/*
* Copyright (C) 2022 Brian Evans
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package main
import (

293
client.go
View File

@ -1,25 +1,10 @@
/*
* Copyright (C) 2022 Brian Evans
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package main
import (
"fmt"
"io/ioutil"
"net/url"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
@ -34,7 +19,6 @@ import (
"tildegit.org/sloum/bombadillo/http"
"tildegit.org/sloum/bombadillo/local"
"tildegit.org/sloum/bombadillo/telnet"
"tildegit.org/sloum/bombadillo/termios"
)
//------------------------------------------------\\
@ -59,7 +43,14 @@ type client struct {
//--------------------------------------------------\\
func (c *client) GetSizeOnce() {
var w, h = termios.GetWindowSize()
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)
c.Height = h
c.Width = w
}
@ -70,7 +61,14 @@ func (c *client) GetSize() {
c.Draw()
for {
var w, h = termios.GetWindowSize()
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)
if h != c.Height || w != c.Width {
c.Height = h
c.Width = w
@ -88,7 +86,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, getMaxWidth(c.Options), (c.Options["theme"] == "color"))
pageContent := c.PageState.Render(c.Height, c.Width-1, (c.Options["theme"] == "color"))
var re *regexp.Regexp
if c.Options["theme"] == "inverse" {
screen.WriteString("\033[7m")
@ -137,11 +135,15 @@ func (c *client) Draw() {
} else {
for i := 0; i < c.Height-3; i++ {
if i < len(pageContent) {
screen.WriteString("\033[0K")
screen.WriteString(pageContent[i])
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("\n")
} else {
screen.WriteString("\033[0K")
screen.WriteString(fmt.Sprintf("%-*.*s", c.Width, c.Width, " "))
screen.WriteString("\n")
}
}
@ -160,48 +162,37 @@ func (c *client) TakeControlInput() {
input := cui.Getch()
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
case 'j', 'J':
// scroll down one line
c.ClearMessage()
c.Scroll(1)
case 'k':
// Scroll up one line
case 'k', 'K':
// scroll up one line
c.ClearMessage()
c.Scroll(-1)
case 'q':
// Quit
case 'q', 'Q':
// quit bombadillo
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
case 'b':
// go back
c.ClearMessage()
err := c.PageState.NavigateHistory(-1)
if err != nil {
@ -213,7 +204,6 @@ func (c *client) TakeControlInput() {
c.Draw()
}
case 'R':
// Refresh the current page
c.ClearMessage()
err := c.ReloadPage()
if err != nil {
@ -223,11 +213,11 @@ func (c *client) TakeControlInput() {
c.Draw()
}
case 'B':
// Toggle the bookmark browser
// open the bookmarks browser
c.BookMarks.ToggleOpen()
c.Draw()
case 'f', 'l':
// Go forward
case 'f', 'F':
// go forward
c.ClearMessage()
err := c.PageState.NavigateHistory(1)
if err != nil {
@ -239,7 +229,7 @@ func (c *client) TakeControlInput() {
c.Draw()
}
case '\t':
// Toggle bookmark browser focus
// Toggle bookmark browser focus on/off
c.BookMarks.ToggleFocused()
c.Draw()
case 'n':
@ -279,7 +269,6 @@ func (c *client) TakeControlInput() {
}
err = c.NextSearchItem(0)
if err != nil {
c.PageState.History[c.PageState.Position].WrapContent(c.Width-1,getMaxWidth(c.Options),(c.Options["theme"] == "color"))
c.Draw()
}
case ':', ' ':
@ -366,41 +355,23 @@ func (c *client) simpleCommand(action string) {
case "SEARCH":
c.search("", "", "?")
case "HELP", "?":
c.Visit(helplocation)
case "JUMP", "J":
err := c.PageState.CopyHistory(-1)
if err != nil {
c.SetMessage(err.Error(), false)
c.DrawMessage()
} else {
c.Draw()
}
case "VERSION":
ver := version
if ver == "" {
ver = "Improperly compiled, no version information"
}
c.SetMessage("Bombadillo version: " + ver, false)
c.DrawMessage()
go c.Visit(helplocation)
default:
c.SetMessage(syntaxErrorMessage(action), true)
c.SetMessage(fmt.Sprintf("Unknown action %q", action), true)
c.DrawMessage()
}
}
func (c *client) doCommand(action string, values []string) {
if length := len(values); length != 1 {
c.SetMessage(fmt.Sprintf("Expected 1 argument, received %d", len(values)), true)
c.DrawMessage()
return
}
switch action {
case "C", "CHECK":
case "CHECK", "C":
c.displayConfigValue(values[0])
c.DrawMessage()
case "HELP", "?":
if val, ok := ERRS[values[0]]; ok {
c.SetMessage("Usage: " + val, false)
} else {
msg := fmt.Sprintf("%q is not a valid command; help syntax: %s", values[0], ERRS[action])
c.SetMessage(msg, false)
}
c.DrawMessage()
case "PURGE", "P":
err := c.Certs.Purge(values[0])
if err != nil {
@ -443,23 +414,26 @@ func (c *client) doCommand(action string, values []string) {
fn = "index"
}
c.saveFile(u, fn)
default:
c.SetMessage(syntaxErrorMessage(action), true)
c.SetMessage(fmt.Sprintf("Unknown action %q", action), true)
c.DrawMessage()
}
}
func (c *client) doCommandAs(action string, values []string) {
if len(values) < 2 {
c.SetMessage(fmt.Sprintf("Expected 2+ arguments, received %d", len(values)), true)
c.DrawMessage()
return
}
if values[0] == "." {
values[0] = c.PageState.History[c.PageState.Position].Location.Full
}
switch action {
case "ADD", "A":
if len(values) < 2 {
c.SetMessage(syntaxErrorMessage(action), true)
c.DrawMessage()
return
}
if values[0] == "." {
values[0] = c.PageState.History[c.PageState.Position].Location.Full
}
msg, err := c.BookMarks.Add(values)
if err != nil {
c.SetMessage(err.Error(), true)
@ -478,18 +452,8 @@ func (c *client) doCommandAs(action string, values []string) {
c.Draw()
}
case "SEARCH":
if len(values) < 2 {
c.SetMessage(syntaxErrorMessage(action), true)
c.DrawMessage()
return
}
c.search(strings.Join(values, " "), "", "")
case "SET", "S":
if len(values) < 2 {
c.SetMessage(syntaxErrorMessage(action), true)
c.DrawMessage()
return
}
if _, ok := c.Options[values[0]]; ok {
val := strings.Join(values[1:], " ")
if !validateOpt(values[0], val) {
@ -498,10 +462,8 @@ func (c *client) doCommandAs(action string, values []string) {
return
}
c.Options[values[0]] = lowerCaseOpt(values[0], val)
if values[0] == "geminiblocks" {
gemini.BlockBehavior = c.Options[values[0]]
} else if values[0] == "timeout" {
updateTimeouts(c.Options[values[0]])
if values[0] == "tlskey" || values[0] == "tlscertificate" {
c.Certs.LoadCertificate(c.Options["tlscertificate"], c.Options["tlskey"])
} else if values[0] == "configlocation" {
c.SetMessage("Cannot set READ ONLY setting 'configlocation'", true)
c.DrawMessage()
@ -520,7 +482,7 @@ func (c *client) doCommandAs(action string, values []string) {
c.SetMessage(fmt.Sprintf("Unable to set %s, it does not exist", values[0]), true)
c.DrawMessage()
default:
c.SetMessage(syntaxErrorMessage(action), true)
c.SetMessage(fmt.Sprintf("Unknown command structure"), true)
c.DrawMessage()
}
}
@ -570,7 +532,7 @@ func (c *client) doLinkCommandAs(action, target string, values []string) {
out = append(out, values...)
c.doCommandAs(action, out)
default:
c.SetMessage(syntaxErrorMessage(action), true)
c.SetMessage(fmt.Sprintf("Unknown command structure"), true)
c.DrawMessage()
}
}
@ -677,15 +639,6 @@ func (c *client) doLinkCommand(action, target string) {
link := links[num]
c.SetMessage(fmt.Sprintf("[%d] %s", num+1, link), false)
c.DrawMessage()
case "JUMP", "J":
num--
err = c.PageState.CopyHistory(num)
if err != nil {
c.SetMessage(err.Error(), false)
c.DrawMessage()
} else {
c.Draw()
}
case "WRITE", "W":
links := c.PageState.History[c.PageState.Position].Links
if len(links) < num || num < 1 {
@ -711,13 +664,13 @@ func (c *client) doLinkCommand(action, target string) {
}
c.saveFile(u, fn)
default:
c.SetMessage(syntaxErrorMessage(action), true)
c.SetMessage("Unknown command structure", true)
c.DrawMessage()
}
}
func (c *client) search(query, uri, question string) {
func (c *client) search(query, url, question string) {
var entry string
var err error
if query == "" {
@ -741,32 +694,22 @@ func (c *client) search(query, uri, question string) {
} else {
entry = query
}
if uri == "" {
uri = c.Options["searchengine"]
if url == "" {
url = c.Options["searchengine"]
}
u, err := MakeUrl(uri)
u, err := MakeUrl(url)
if err != nil {
c.SetMessage("The search url is not valid", true)
c.SetMessage("The search url is not a valid url", true)
c.DrawMessage()
return
}
var rootUrl string
switch u.Scheme {
case "gopher":
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))
go c.Visit(fmt.Sprintf("%s\t%s", u.Full, entry))
case "gemini":
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))
// TODO url escape the entry variable
escapedEntry := entry
go c.Visit(fmt.Sprintf("%s?%s", u.Full, escapedEntry))
case "http", "https":
c.Visit(u.Full)
default:
@ -986,7 +929,7 @@ func (c *client) Visit(url string) {
// +++ Begin Protocol Handlers +++
func (c *client) handleGopher(u Url) {
if u.DownloadOnly || (c.Options["showimages"] == "false" && (u.Mime == "I" || u.Mime == "g")) {
if u.DownloadOnly {
nameSplit := strings.Split(u.Resource, "/")
filename := nameSplit[len(nameSplit)-1]
filename = strings.Trim(filename, " \t\r\n\v\f\a")
@ -1004,12 +947,7 @@ 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, getMaxWidth(c.Options), (c.Options["theme"] == "color"))
pg.WrapContent(c.Width-1, (c.Options["theme"] == "color"))
c.PageState.Add(pg)
c.SetPercentRead()
c.ClearMessage()
@ -1028,15 +966,11 @@ func (c *client) handleGemini(u Url) {
go saveConfig()
switch capsule.Status {
case 1:
// Query
c.search("", u.Full, capsule.Content)
case 2:
// Success
if capsule.MimeMaj == "text" || (c.Options["showimages"] == "true" && capsule.MimeMaj == "image") {
u.Mime = capsule.MimeMin
if capsule.MimeMaj == "text" {
pg := MakePage(u, capsule.Content, capsule.Links)
pg.FileType = capsule.MimeMaj
pg.WrapContent(c.Width-1, getMaxWidth(c.Options), (c.Options["theme"] == "color"))
pg.WrapContent(c.Width-1, (c.Options["theme"] == "color"))
c.PageState.Add(pg)
c.SetPercentRead()
c.ClearMessage()
@ -1050,28 +984,14 @@ func (c *client) handleGemini(u Url) {
c.saveFileFromData(capsule.Content, filename)
}
case 3:
// Redirect
lowerRedirect := strings.ToLower(capsule.Content)
lowerOriginal := strings.ToLower(u.Full)
if strings.Replace(lowerRedirect, lowerOriginal, "", 1) == "/" {
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 {
if !strings.Contains(capsule.Content, "://") {
lnk, lnkErr := gemini.HandleRelativeUrl(capsule.Content, u.Full)
if lnkErr == nil {
capsule.Content = lnk
}
}
c.SetMessage(fmt.Sprintf("Follow redirect (y/n): %s?", capsule.Content), false)
c.SetMessage("Redirect aborted", false)
c.DrawMessage()
ch := cui.Getch()
if ch == 'y' || ch == 'Y' {
c.Visit(capsule.Content)
} else {
c.SetMessage("Redirect aborted", false)
c.DrawMessage()
}
}
}
}
@ -1098,11 +1018,7 @@ 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, getMaxWidth(c.Options), (c.Options["theme"] == "color"))
pg.WrapContent(c.Width-1, (c.Options["theme"] == "color"))
c.PageState.Add(pg)
c.SetPercentRead()
c.ClearMessage()
@ -1118,7 +1034,7 @@ func (c *client) handleFinger(u Url) {
return
}
pg := MakePage(u, content, []string{})
pg.WrapContent(c.Width-1, getMaxWidth(c.Options), (c.Options["theme"] == "color"))
pg.WrapContent(c.Width-1, (c.Options["theme"] == "color"))
c.PageState.Add(pg)
c.SetPercentRead()
c.ClearMessage()
@ -1138,7 +1054,7 @@ func (c *client) handleWeb(u Url) {
return
}
pg := MakePage(u, page.Content, page.Links)
pg.WrapContent(c.Width-1, getMaxWidth(c.Options), (c.Options["theme"] == "color"))
pg.WrapContent(c.Width-1, (c.Options["theme"] == "color"))
c.PageState.Add(pg)
c.SetPercentRead()
c.ClearMessage()
@ -1259,36 +1175,3 @@ func findAvailableFileName(fpath, fname string) (string, error) {
return savePath, nil
}
func syntaxErrorMessage(action string) string {
if val, ok := ERRS[action]; ok {
return fmt.Sprintf("Incorrect syntax. Try: %s", val)
}
return fmt.Sprintf("Unknown command %q", action)
}
func updateTimeouts(timeoutString string) error {
sec, err := strconv.Atoi(timeoutString)
if err != nil {
return err
}
timeout := time.Duration(sec) * time.Second
gopher.Timeout = timeout
gemini.TlsTimeout = timeout
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
}

View File

@ -1,18 +1,3 @@
/*
* Copyright (C) 2022 Brian Evans
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package cmdparse
import (
@ -87,7 +72,7 @@ func (s *scanner) scanText() Token {
"S", "SET", "R", "RELOAD", "SEARCH",
"Q", "QUIT", "B", "BOOKMARKS", "H",
"HOME", "?", "HELP", "C", "CHECK",
"P", "PURGE", "JUMP", "J", "VERSION":
"P", "PURGE":
return Token{Action, capInput}
}

View File

@ -1,19 +1,3 @@
/*
* Copyright (C) 2022 Brian Evans
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package cmdparse
import (
@ -110,10 +94,10 @@ func (p *Parser) parseAction() (*Command, error) {
case Value:
cm.Target = t.val
cm.Type = DOLINK
case Word, Action:
case Word:
cm.Value = append(cm.Value, t.val)
cm.Type = DO
case Whitespace:
case Action, Whitespace:
return nil, fmt.Errorf("Found %q (%d), expected value", t.val, t.kind)
}
t = p.scan()

View File

@ -1,18 +1,3 @@
/*
* Copyright (C) 2022 Brian Evans
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package config
import (

View File

@ -1,18 +1,3 @@
/*
* Copyright (C) 2022 Brian Evans
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package config
import (

View File

@ -1,18 +1,3 @@
/*
* Copyright (C) 2022 Brian Evans
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package cui
import (
@ -20,8 +5,6 @@ import (
"fmt"
"os"
"os/exec"
"tildegit.org/sloum/bombadillo/termios"
)
var Shapes = map[string]string{
@ -72,17 +55,16 @@ func Exit(exitCode int, msg string) {
// InitTerm sets the terminal modes appropriate for Bombadillo
func InitTerm() {
termios.SetCharMode()
Tput("smcup") // use alternate screen
Tput("rmam") // turn off line wrapping
fmt.Print("\033[?25l") // hide cursor
SetCharMode()
Tput("rmam") // turn off line wrapping
Tput("smcup") // use alternate screen
}
// CleanupTerm reverts changs to terminal mode made by InitTerm
func CleanupTerm() {
moveCursorToward("down", 500)
moveCursorToward("right", 500)
termios.SetLineMode()
SetLineMode()
fmt.Print("\n")
fmt.Print("\033[?25h") // reenables cursor blinking
@ -116,8 +98,7 @@ func Getch() rune {
}
func GetLine(prefix string) (string, error) {
termios.SetLineMode()
defer termios.SetCharMode()
SetLineMode()
reader := bufio.NewReader(os.Stdin)
fmt.Print(prefix)
@ -126,9 +107,32 @@ func GetLine(prefix string) (string, error) {
return "", err
}
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

@ -1,18 +1,3 @@
/*
* Copyright (C) 2022 Brian Evans
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package main
import (
@ -60,18 +45,16 @@ 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"
"timeout": "15", // connection timeout for gopher/gemini in seconds
"webmode": "none", // "none", "gui", "lynx", "w3m", "elinks"
"maxwidth": "100",
"tlscertificate": "",
"tlskey": "",
"webmode": "none", // "none", "gui", "lynx", "w3m", "elinks"
}
// homePath will return the path to your home directory as a string

View File

@ -1,18 +1,3 @@
/*
* Copyright (C) 2022 Brian Evans
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package finger
import (

View File

@ -1,18 +1,3 @@
/*
* Copyright (C) 2022 Brian Evans
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package main
import (

View File

@ -1,18 +1,3 @@
/*
* Copyright (C) 2022 Brian Evans
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package gemini
import (
@ -21,8 +6,6 @@ import (
"crypto/tls"
"fmt"
"io/ioutil"
"net"
"net/url"
"strconv"
"strings"
"time"
@ -38,15 +21,22 @@ type Capsule struct {
type TofuDigest struct {
certs map[string]string
ClientCert tls.Certificate
}
var BlockBehavior string = "block"
var TlsTimeout time.Duration = time.Duration(15) * time.Second
//------------------------------------------------\\
// + + + R E C E I V E R S + + + \\
//--------------------------------------------------\\
func (t *TofuDigest) LoadCertificate(cert, key string) {
certificate, err := tls.LoadX509KeyPair(cert, key)
if err != nil {
t.ClientCert = tls.Certificate{}
return
}
t.ClientCert = certificate
}
func (t *TofuDigest) Purge(host string) error {
host = strings.ToLower(host)
if host == "*" {
@ -59,8 +49,8 @@ func (t *TofuDigest) Purge(host string) error {
return fmt.Errorf("Invalid host %q", host)
}
func (t *TofuDigest) Add(host, hash string, time int64) {
t.certs[strings.ToLower(host)] = fmt.Sprintf("%s|%d", hash, time)
func (t *TofuDigest) Add(host, hash string) {
t.certs[strings.ToLower(host)] = hash
}
func (t *TofuDigest) Exists(host string) bool {
@ -77,11 +67,12 @@ func (t *TofuDigest) Find(host string) (string, error) {
return "", fmt.Errorf("Invalid hostname, no key saved")
}
func (t *TofuDigest) Match(host, localCert string, cState *tls.ConnectionState) error {
func (t *TofuDigest) Match(host string, cState *tls.ConnectionState) error {
host = strings.ToLower(host)
now := time.Now()
for _, cert := range cState.PeerCertificates {
if localCert != hashCert(cert.Raw) {
if t.certs[host] != hashCert(cert.Raw) {
continue
}
@ -93,7 +84,7 @@ func (t *TofuDigest) Match(host, localCert string, cState *tls.ConnectionState)
return fmt.Errorf("EXP")
}
if err := cert.VerifyHostname(host); err != nil && cert.Subject.CommonName != host {
if err := cert.VerifyHostname(host); err != nil {
return fmt.Errorf("Certificate error: %s", err)
}
@ -122,45 +113,18 @@ func (t *TofuDigest) newCert(host string, cState *tls.ConnectionState) error {
continue
}
if err := cert.VerifyHostname(host); err != nil && cert.Subject.CommonName != host {
if err := cert.VerifyHostname(host); err != nil {
reasons.WriteString(fmt.Sprintf("Cert [%d] hostname does not match", index+1))
continue
}
t.Add(host, hashCert(cert.Raw), cert.NotAfter.Unix())
t.Add(host, hashCert(cert.Raw))
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 ""
@ -192,7 +156,11 @@ func Retrieve(host, port, resource string, td *TofuDigest) (string, error) {
InsecureSkipVerify: true,
}
conn, err := tls.DialWithDialer(&net.Dialer{Timeout: TlsTimeout}, "tcp", addr, conf)
conf.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
return &td.ClientCert, nil
}
conn, err := tls.Dial("tcp", addr, conf)
if err != nil {
return "", fmt.Errorf("TLS Dial Error: %s", err.Error())
}
@ -208,11 +176,9 @@ func Retrieve(host, port, resource string, td *TofuDigest) (string, error) {
return "", fmt.Errorf("Insecure, no certificates offered by server")
}
localCert, localTs, err := td.GetCertAndTimestamp(host)
if localTs > 0 {
if td.Exists(host) {
// See if we have a matching cert
err := td.Match(host, localCert, &connState)
err := td.Match(host, &connState)
if err != nil && err.Error() != "EXP" {
// If there is no match and it isnt because of an expiration
// just return the error
@ -285,7 +251,7 @@ func Fetch(host, port, resource string, td *TofuDigest) ([]byte, error) {
case 5:
return make([]byte, 0), fmt.Errorf("[5] Permanent Failure.")
case 6:
return make([]byte, 0), fmt.Errorf("[6] Client Certificate Required (Unsupported)")
return make([]byte, 0), fmt.Errorf("[6] Client Certificate Required")
default:
return make([]byte, 0), fmt.Errorf("Invalid response status from server")
}
@ -334,9 +300,6 @@ func Visit(host, port, resource string, td *TofuDigest) (Capsule, error) {
case 2:
mimeAndCharset := strings.Split(header[1], ";")
meta = mimeAndCharset[0]
if meta == "" {
meta = "text/gemini"
}
minMajMime := strings.Split(meta, "/")
if len(minMajMime) < 2 {
return capsule, fmt.Errorf("Improperly formatted mimetype received from server")
@ -350,7 +313,8 @@ func Visit(host, port, resource string, td *TofuDigest) (Capsule, error) {
resource = "/"
}
currentUrl := fmt.Sprintf("gemini://%s:%s%s", host, port, resource)
capsule.Content, capsule.Links = parseGemini(body, currentUrl)
rootUrl := fmt.Sprintf("gemini://%s:%s", host, port)
capsule.Content, capsule.Links = parseGemini(body, rootUrl, currentUrl)
} else {
capsule.Content = body
}
@ -365,34 +329,19 @@ func Visit(host, port, resource string, td *TofuDigest) (Capsule, error) {
case 5:
return capsule, fmt.Errorf("[5] Permanent Failure. %s", header[1])
case 6:
return capsule, fmt.Errorf("[6] Client Certificate Required (Unsupported)")
return capsule, fmt.Errorf("[6] Client Certificate Required")
default:
return capsule, fmt.Errorf("Invalid response status from server")
}
}
func parseGemini(b, currentUrl string) (string, []string) {
func parseGemini(b, rootUrl, currentUrl string) (string, []string) {
splitContent := strings.Split(b, "\n")
links := make([]string, 0, 10)
inPreBlock := false
spacer := " "
outputIndex := 0
for i, ln := range splitContent {
splitContent[i] = strings.Trim(ln, "\r\n")
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][ %s ]", spacer, alt)
outputIndex++
}
} else if isPreBlockDeclaration {
inPreBlock = !inPreBlock
} else if len([]rune(ln)) > 3 && ln[:2] == "=>" && !inPreBlock {
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")
@ -406,40 +355,33 @@ func parseGemini(b, currentUrl string) (string, []string) {
}
if strings.Index(link, "://") < 0 {
link, _ = HandleRelativeUrl(link, currentUrl)
link = handleRelativeUrl(link, rootUrl, currentUrl)
}
links = append(links, link)
linknum := fmt.Sprintf("[%d]", len(links))
splitContent[outputIndex] = fmt.Sprintf("%-5s %s", linknum, decorator)
outputIndex++
} else {
if inPreBlock && (BlockBehavior == "alt" || BlockBehavior == "neither") {
continue
}
var leader, tail string = "", ""
if len(ln) > 0 && ln[0] == '#' {
leader = "\033[1m"
tail = "\033[0m"
}
splitContent[outputIndex] = fmt.Sprintf("%s%s%s%s", spacer, leader, ln, tail)
outputIndex++
splitContent[i] = fmt.Sprintf("%-5s %s", linknum, decorator)
}
}
return strings.Join(splitContent[:outputIndex], "\n"), links
return strings.Join(splitContent, "\n"), links
}
// handleRelativeUrl provides link completion
func HandleRelativeUrl(relLink, current string) (string, error) {
base, err := url.Parse(current)
if err != nil {
return relLink, err
func handleRelativeUrl(u, root, current string) string {
if len(u) < 1 {
return u
}
rel, err := url.Parse(relLink)
if err != nil {
return relLink, err
if u[0] == '/' {
return fmt.Sprintf("%s%s", root, u)
}
return base.ResolveReference(rel).String(), nil
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)
}
func hashCert(cert []byte) string {
@ -456,5 +398,5 @@ func MakeCapsule() Capsule {
}
func MakeTofuDigest() TofuDigest {
return TofuDigest{make(map[string]string)}
return TofuDigest{make(map[string]string), tls.Certificate{}}
}

View File

@ -1,19 +1,3 @@
/*
* Copyright (C) 2022 Brian Evans
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// Contains the building blocks of a gopher client: history, url, and view.
// History handles the browsing session and view represents individual
// text based resources, the url represents a parsed url.
@ -54,8 +38,6 @@ var types = map[string]string{
"T": "TEL",
}
var Timeout time.Duration = time.Duration(15) * time.Second
//------------------------------------------------\\
// + + + F U N C T I O N S + + + \\
//--------------------------------------------------\\
@ -67,6 +49,7 @@ var Timeout time.Duration = time.Duration(15) * time.Second
// be better.
func Retrieve(host, port, resource string) ([]byte, error) {
nullRes := make([]byte, 0)
timeOut := time.Duration(5) * time.Second
if host == "" || port == "" {
return nullRes, errors.New("Incomplete request url")
@ -74,7 +57,7 @@ func Retrieve(host, port, resource string) ([]byte, error) {
addr := host + ":" + port
conn, err := net.DialTimeout("tcp", addr, Timeout)
conn, err := net.DialTimeout("tcp", addr, timeOut)
if err != nil {
return nullRes, err
}
@ -148,11 +131,9 @@ 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") {
@ -160,8 +141,7 @@ func parseMap(text string) (string, []string) {
} else {
link := buildLink(line[2], line[3], string(line[0][0]), line[1])
links = append(links, link)
linkNum := fmt.Sprintf("[%d]",len(links))
linktext := fmt.Sprintf("%s %5s %s", getType(string(line[0][0])), linkNum, title)
linktext := fmt.Sprintf("(%s) %2d %s", getType(string(line[0][0])), len(links), title)
splitContent[i] = linktext
}
}

View File

@ -1,18 +1,3 @@
/*
* Copyright (C) 2022 Brian Evans
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package main
import (

46
help.go
View File

@ -1,46 +0,0 @@
/*
* Copyright (C) 2022 Brian Evans
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package main
// ERRS maps commands to their syntax error message
var ERRS = map[string]string{
"A": "`a [target] [name...]`",
"ADD": "`add [target] [name...]`",
"D": "`d [bookmark-id]`",
"DELETE": "`delete [bookmark-id]`",
"B": "`b [[bookmark-id]]`",
"BOOKMARKS": "`bookmarks [[bookmark-id]]`",
"C": "`c [link_id]` or `c [setting]`",
"CHECK": "`check [link_id]` or `check [setting]`",
"H": "`h`",
"HOME": "`home`",
"J": "`j [[history_position]]`",
"JUMP": "`jump [[history_position]]`",
"P": "`p [host]`",
"PURGE": "`purge [host]`",
"Q": "`q`",
"QUIT": "`quit`",
"R": "`r`",
"RELOAD": "`reload`",
"SEARCH": "`search [[keyword(s)...]]`",
"S": "`s [setting] [value]`",
"SET": "`set [setting] [value]`",
"W": "`w [target]`",
"WRITE": "`write [target]`",
"VERSION": "`version`",
"?": "`? [[command]]`",
"HELP": "`help [[command]]`",
}

View File

@ -1,18 +1,3 @@
/*
* Copyright (C) 2022 Brian Evans
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package http
import (
@ -48,7 +33,7 @@ func Visit(webmode, url string, width int) (Page, error) {
return Page{}, fmt.Errorf("Invalid webmode setting")
}
c, err := exec.Command(webmode, "-dump", w, fmt.Sprintf("%d", width), url).Output()
if err != nil && c == nil {
if err != nil {
return Page{}, err
}
return parseLinks(string(c)), nil

View File

@ -1,20 +1,4 @@
/*
* Copyright (C) 2022 Brian Evans
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// This will build for osx without a build tag based on the filename
// +build darwin
package http

View File

@ -0,0 +1,30 @@
// +build linux
package http
import (
"fmt"
"os"
"os/exec"
)
// OpenInBrowser checks for the presence of a display server
// and environment variables indicating a gui is present. If found
// then xdg-open is called on a url to open said url in the default
// gui web browser for the system
func OpenInBrowser(url string) (string, error) {
disp := os.Getenv("DISPLAY")
wayland := os.Getenv("WAYLAND_DISPLAY")
_, err := exec.LookPath("Xorg")
if disp == "" && wayland == "" && err != nil {
return "", fmt.Errorf("No gui is available, check 'webmode' setting")
}
// Use start rather than run or output in order
// to release the process and not block
err = exec.Command("xdg-open", url).Start()
if err != nil {
return "", err
}
return "Opened in system default web browser", nil
}

View File

@ -1,46 +1,11 @@
// +build !darwin,!windows
/*
* Copyright (C) 2022 Brian Evans
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// +build !linux
// +build !darwin
// +build !windows
package http
import (
"fmt"
"os"
"os/exec"
)
import "fmt"
// OpenInBrowser checks for the presence of a display server
// and environment variables indicating a gui is present. If found
// then xdg-open is called on a url to open said url in the default
// gui web browser for the system
func OpenInBrowser(url string) (string, error) {
disp := os.Getenv("DISPLAY")
wayland := os.Getenv("WAYLAND_DISPLAY")
_, err := exec.LookPath("Xorg")
if disp == "" && wayland == "" && err != nil {
return "", fmt.Errorf("No gui is available, check 'webmode' setting")
}
// Use start rather than run or output in order
// to release the process and not block
err = exec.Command("xdg-open", url).Start()
if err != nil {
return "", err
}
return "Opened in system default web browser", nil
return "", fmt.Errorf("Unsupported os for 'webmode' 'gui' setting")
}

View File

@ -1,21 +1,5 @@
/*
* Copyright (C) 2022 Brian Evans
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// +build windows
// This will only build for windows based on the filename
// no build tag required
package http
import "os/exec"

View File

@ -1,18 +1,3 @@
/*
* Copyright (C) 2022 Brian Evans
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package local
import (

74
main.go
View File

@ -3,21 +3,21 @@ package main
// Bombadillo is an internet client for the terminal of unix or
// unix-like systems.
//
/*
* Copyright (C) 2022 Brian Evans
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2019 Brian Evans
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import (
"flag"
"fmt"
@ -25,17 +25,16 @@ 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 = "2.3.3"
var version string
var build string
var bombadillo *client
var helplocation string = "gopher://bombadillo.colorfield.space:70/1/user-guide.map"
@ -66,8 +65,6 @@ 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)
@ -81,20 +78,12 @@ func validateOpt(opt, val string) bool {
}
return false
}
if opt == "timeout" {
_, err := strconv.Atoi(val)
if err != nil {
return false
}
}
return true
}
func lowerCaseOpt(opt, val string) string {
switch opt {
case "webmode", "theme", "defaultscheme", "showimages", "geminiblocks":
case "webmode", "theme", "defaultscheme":
return strings.ToLower(val)
default:
return val
@ -131,11 +120,6 @@ 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 if lowerkey == "timeout" {
updateTimeouts(v.Value)
}
} else {
bombadillo.Options[lowerkey] = defaultOptions[lowerkey]
}
@ -147,26 +131,16 @@ func loadConfig() {
}
for _, v := range settings.Certs {
// Remove expired certs
vals := strings.SplitN(v.Value, "|", -1)
if len(vals) < 2 {
continue
}
now := time.Now()
ts, err := strconv.ParseInt(vals[1], 10, 64)
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)
bombadillo.Certs.Add(v.Key, v.Value)
}
}
func initClient() {
bombadillo = MakeClient(" ((( Bombadillo ))) ")
loadConfig()
if bombadillo.Options["tlscertificate"] != "" && bombadillo.Options["tlskey"] != "" {
bombadillo.Certs.LoadCertificate(bombadillo.Options["tlscertificate"], bombadillo.Options["tlskey"])
}
}
// In the event of specific signals, ensure the display is shown correctly.
@ -210,7 +184,7 @@ func main() {
flag.Usage = printHelp
flag.Parse()
if *getVersion {
fmt.Printf("Bombadillo %s\n", version)
fmt.Printf("Bombadillo %s - build %s\n", version, build)
os.Exit(0)
}
args := flag.Args()

102
page.go
View File

@ -1,25 +1,8 @@
/*
* Copyright (C) 2022 Brian Evans
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package main
import (
"fmt"
"strings"
"tildegit.org/sloum/bombadillo/tdiv"
"unicode"
)
//------------------------------------------------\\
@ -38,9 +21,6 @@ type Page struct {
FoundLinkLines []int
SearchTerm string
SearchIndex int
FileType string
WrapWidth int
Color bool
}
//------------------------------------------------\\
@ -67,41 +47,17 @@ 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, maxWidth int, color bool) {
if p.FileType == "image" {
p.RenderImage(width)
return
}
width = min(width, maxWidth)
func (p *Page) WrapContent(width int, color bool) {
counter := 0
spacer := ""
var content strings.Builder
var esc strings.Builder
escape := false
content.Grow(len(p.RawContent))
if p.Location.Mime == "1" { // gopher document
spacer = " "
} else if strings.HasSuffix(p.Location.Mime, "gemini") { //gemini document
spacer = " "
}
runeArr := []rune(p.RawContent)
for i := 0; i < len(runeArr); i++ {
ch := runeArr[i]
for _, ch := range []rune(p.RawContent) {
if escape {
if color {
esc.WriteRune(ch)
@ -115,8 +71,8 @@ func (p *Page) WrapContent(width, maxWidth int, color bool) {
}
continue
}
if ch == '\n' || ch == '\u0085' || ch == '\u2028' || ch == '\u2029' {
content.WriteRune('\n')
if ch == '\n' {
content.WriteRune(ch)
counter = 0
} else if ch == '\t' {
if counter+4 < width {
@ -127,7 +83,7 @@ func (p *Page) WrapContent(width, maxWidth int, color bool) {
counter = 0
}
} else if ch == '\r' || ch == '\v' || ch == '\b' || ch == '\f' || ch == '\a' {
// Get rid of control characters we don't want
// Get rid of control characters we dont want
continue
} else if ch == 27 {
if p.Location.Scheme == "local" {
@ -143,38 +99,23 @@ func (p *Page) WrapContent(width, maxWidth int, color bool) {
}
continue
} else {
// 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) {
if counter <= width {
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
content.WriteString(spacer)
counter += len(spacer)
if p.Location.Mime == "1" {
spacer := " "
content.WriteString(spacer)
counter += len(spacer)
}
content.WriteRune(ch)
counter++
}
}
}
p.WrappedContent = strings.Split(content.String(), "\n")
p.WrapWidth = width
p.Color = color
p.HighlightFoundText()
}
@ -182,8 +123,9 @@ func (p *Page) HighlightFoundText() {
if p.SearchTerm == "" {
return
}
lowS := strings.ToLower(p.SearchTerm)
for i, ln := range p.WrappedContent {
found := strings.Index(ln, p.SearchTerm)
found := strings.Index(strings.ToLower(ln), lowS)
if found < 0 {
continue
}
@ -191,7 +133,8 @@ func (p *Page) HighlightFoundText() {
if bombadillo.Options["theme"] == "inverse" {
format = "\033[27m%s\033[7m"
}
ln = strings.Replace(ln, p.SearchTerm, fmt.Sprintf(format, p.SearchTerm), -1)
foundS := ln[found:found+len(lowS)]
ln = strings.Replace(ln, foundS, fmt.Sprintf(format, foundS), -1)
p.WrappedContent[i] = ln
}
}
@ -207,12 +150,14 @@ func (p *Page) FindText() {
if bombadillo.Options["theme"] == "inverse" {
format = "\033[27m%s\033[7m"
}
lowS := strings.ToLower(s)
for i, ln := range p.WrappedContent {
found := strings.Index(ln, s)
found := strings.Index(strings.ToLower(ln), lowS)
if found < 0 {
continue
}
ln = strings.Replace(ln, s, fmt.Sprintf(format, s), -1)
foundS := ln[found:found+len(lowS)]
ln = strings.Replace(ln, foundS, fmt.Sprintf(format, foundS), -1)
p.WrappedContent[i] = ln
p.FoundLinkLines = append(p.FoundLinkLines, i)
}
@ -224,13 +169,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, "", 40, false}
p := Page{make([]string, 0), content, links, url, 0, make([]int, 0), "", 0}
return p
}
func min(a, b int) int {
if a < b {
return a
}
return b
}

View File

@ -1,145 +0,0 @@
/*
* Copyright (C) 2022 Brian Evans
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package main
import (
"reflect"
"testing"
)
func Test_WrapContent_Wrapped_Line_Length(t *testing.T) {
type fields struct {
WrappedContent []string
RawContent string
Links []string
Location Url
ScrollPosition int
FoundLinkLines []int
SearchTerm string
SearchIndex int
FileType string
WrapWidth int
Color bool
}
type args struct {
width int
maxWidth int
color bool
}
// create a Url for use by the MakePage function
url, _ := MakeUrl("gemini://rawtext.club")
tests := []struct {
name string
input string
expects []string
args args
}{
{
"Short line that doesn't wrap",
"0123456789\n",
[]string{
"0123456789",
"",
},
args{
10,
10,
false,
},
},
{
"multiple words should wrap at the right point",
"01 345 789 123456789 123456789 123456789 123456789\n",
[]string{
"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,
},
},
{
"Unicode line endings that should not wrap",
"LF\u000A" +
"CR+LF\u000D\u000A" +
"NEL\u0085" +
"LS\u2028" +
"PS\u2029",
[]string{
"LF",
"CR+LF",
"NEL",
"LS",
"PS",
"",
},
args{
10,
10,
false,
},
},
}
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.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)
}
})
}
}

View File

@ -1,18 +1,3 @@
/*
* Copyright (C) 2022 Brian Evans
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package main
import (
@ -53,7 +38,7 @@ func (p *Pages) NavigateHistory(qty int) error {
}
// Add gets passed a Page, which gets added to the history
// array. Add also updates the current length and position
// arrayr. Add also updates the current length and position
// of the Pages struct to which it belongs. Add also shifts
// off array items if necessary.
func (p *Pages) Add(pg Page) {
@ -75,17 +60,13 @@ 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, maxWidth int, color bool) []string {
func (p *Pages) Render(termHeight, termWidth int, color bool) []string {
if p.Length < 1 {
return make([]string, 0)
}
pos := p.History[p.Position].ScrollPosition
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, maxWidth, color)
}
p.History[p.Position].WrapContent(termWidth, color)
now := len(p.History[p.Position].WrappedContent)
if prev > now {
diff := prev - now
@ -107,18 +88,6 @@ func (p *Pages) Render(termHeight, termWidth, maxWidth int, color bool) []string
return p.History[p.Position].WrappedContent[pos:]
}
func (p *Pages) CopyHistory(pos int) error {
if p.Length < 2 || pos > p.Position {
return fmt.Errorf("There are not enough history locations available")
}
if pos < 0 {
pos = p.Position-1
}
p.Add(p.History[pos])
return nil
}
//------------------------------------------------\\
// + + + F U N C T I O N S + + + \\
//--------------------------------------------------\\

View File

@ -1,305 +0,0 @@
/*
* Copyright (C) 2022 Brian Evans
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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
}

View File

@ -1,19 +1,3 @@
/*
* Copyright (C) 2022 Brian Evans
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// Package telnet provides a function that starts a telnet session in a subprocess.
package telnet

View File

@ -1,26 +0,0 @@
// +build linux
/*
* Copyright (C) 2022 Brian Evans
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package termios
import "syscall"
const (
getTermiosIoctl = syscall.TCGETS
setTermiosIoctl = syscall.TCSETS
)

View File

@ -1,26 +0,0 @@
// +build !linux
/*
* Copyright (C) 2022 Brian Evans
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package termios
import "syscall"
const (
getTermiosIoctl = syscall.TIOCGETA
setTermiosIoctl = syscall.TIOCSETAF
)

View File

@ -1,75 +0,0 @@
/*
* Copyright (C) 2022 Brian Evans
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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)
}

36
url.go
View File

@ -1,24 +1,8 @@
/*
* Copyright (C) 2022 Brian Evans
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package main
import (
"fmt"
"os/user"
"path"
"path/filepath"
"regexp"
"strings"
@ -146,7 +130,7 @@ func MakeUrl(u string) (Url, error) {
out.Mime = "1"
}
switch out.Mime {
case "1", "0", "h", "7", "I", "g":
case "1", "0", "h", "7":
out.DownloadOnly = false
default:
out.DownloadOnly = true
@ -161,24 +145,6 @@ 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"