Adds web support in lynx mode #60

Merged
sloum merged 7 commits from lynx-web-mode into develop 2019-10-27 14:15:57 +00:00
5 changed files with 333 additions and 168 deletions

View File

@ -1,4 +1,4 @@
.TH "bombadillo" 1 "12 OCT 2019" "" "General Opperation Manual"
.TH "bombadillo" 1 "12 OCT 2019" "" "General Operation Manual"
.SH NAME
\fBbombadillo \fP- a non-web client
.SH SYNOPSIS
@ -32,7 +32,7 @@ Gemini is supported, but as a new protocol with an incomplete specification, fea
.TP
.B
finger
Basic support is provided for the finger protocol. The format is: \fIfinger://[[username@]][hostname]\fP. Many servers still support finger and it can be fun to see if friends are online or read about the users who's phlogs you follow.
Basic support is provided for the finger protocol. The format is: \fIfinger://[[username@]][hostname]\fP. Many servers still support finger and it can be fun to see if friends are online or read about the users whose phlogs you follow.
.TP
.B
local
@ -44,7 +44,11 @@ Telnet is not supported directly, but addresses will be followed and opened as a
.TP
.B
http, https
Neither of the world wide web protocols are supported directly. However, bombadillo is capable of attempting to open web links in a user's default web browser. This feature is opt-in only and controlled in a user's settings. This feature does not function properly in a terminal only environment at present.
Neither of the world wide web protocols are supported directly. However, \fBbombadillo\fP can open web links in a user's default web browser, or display web content directly in the client via the lynx web browser. Opening http links is opt-in only, controlled by the \fIopenhttp\fP setting.
.IP
Opening links in a default web browser only works if a GUI environment is available.
.IP
Opening web content directly in the client requires the lynx web browser, and is enabled using the \fIlynxmode\fP setting. Web content is processed using lynx, and then displayed in the client.
.SH COMMANDS
.SS KEY COMMANDS
These commands work as a single keypress anytime \fBbombadillo\fP is not taking in a line based command. This is the default command mode of \fBbombadillo\fP.
@ -216,8 +220,13 @@ 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
lynxmode
Will use lynx as a rendering engine for http/https requests if lynx is installed and \fIopenhttp\fP is set to \fItrue\fP. Valid values are \fItrue\fP and \fIfalse\fP.
.TP
.B
openhttp
Tells the client whether or not to try to follow web (http/https) links. If set to \fItrue\fP, \fBbombadillo\fP will try to open a user's default web browser to the link in question. Any value other than \fItrue\fP is considered false.
Tells the client whether or not to try to follow web (http/https) links. If set to \fItrue\fP, \fBbombadillo\fP will try to open a user's default web browser to the link in question. Valid values are \fItrue\fP and \fIfalse\fP.
.TP
.B
savelocation
@ -233,11 +242,11 @@ Tells the client what command to use to start a telnet session. Should be a vali
.TP
.B
terminalonly
Sets whether or not to try to open non-text files served via gemini in gui programs or not. If set to \fItrue\fP, bombdaillo will only attempt to use terminal programs to open files. If set to anything else, \fBbombadillo\fP may choose from the appropriate programs installed on the system, if one is present.
Sets whether or not to try to open non-text files served via gemini in GUI programs or not. If set to \fItrue\fP \fBbombadillo\fP will only attempt to use terminal programs to open files. If set to \fIfalse\fP \fBbombadillo\fP may choose from the appropriate programs installed on the system, including graphical ones.
Review

This section will need review once the discussion around Mailcap is complete.

This section will need review once the discussion around Mailcap is complete.
.TP
.B
theme
Can toggle between visual modes. Valid values are \fInormal\fP and \fIinverse\fP. When set to ivnerse, the terminal color mode is inversed.
Can toggle between visual modes. Valid values are \fInormal\fP and \fIinverse\fP. When set to inverse, the terminal color mode is inverted.
.TP
.B
tlscertificate
@ -245,7 +254,7 @@ A path to a tls certificate file on a user's local filesystem. Defaults to NULL.
.TP
.B
tlskey
A path the 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.
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.
.SH BUGS
There are very likely bugs. Many known bugs can be found in the issues section of \fBbombadillo\fP's software repository (see \fIlinks\fP).
.SH LINKS

387
client.go
View File

@ -514,6 +514,8 @@ func (c *client) saveFile(u Url, name string) {
file, err = gopher.Retrieve(u.Host, u.Port, u.Resource)
case "gemini":
file, err = gemini.Fetch(u.Host, u.Port, u.Resource, &c.Certs)
case "http", "https":
file, err = http.Fetch(u.Full)
default:
c.SetMessage(fmt.Sprintf("Saving files over %s is not supported", u.Scheme), true)
c.DrawMessage()
@ -737,6 +739,21 @@ func (c *client) Scroll(amount int) {
c.Draw()
}
func (c *client) ReloadPage() error {
if c.PageState.Length < 1 {
return fmt.Errorf("There is no page to reload")
}
url := c.PageState.History[c.PageState.Position].Location.Full
err := c.PageState.NavigateHistory(-1)
if err != nil {
return err
}
length := c.PageState.Length
c.Visit(url)
c.PageState.Length = length
return nil
}
func (c *client) SetPercentRead() {
page := c.PageState.History[c.PageState.Position]
var percentRead int
@ -834,6 +851,8 @@ func (c *client) SetHeaderUrl() {
}
}
// Visit functions as a controller/router to the
// appropriate protocol handler
func (c *client) Visit(url string) {
c.SetMessage("Loading...", false)
c.DrawMessage()
@ -848,133 +867,232 @@ func (c *client) Visit(url string) {
switch u.Scheme {
case "gopher":
if u.DownloadOnly {
nameSplit := strings.Split(u.Resource, "/")
filename := nameSplit[len(nameSplit)-1]
filename = strings.Trim(filename, " \t\r\n\v\f\a")
if filename == "" {
filename = "gopherfile"
}
c.saveFile(u, filename)
} else if u.Mime == "7" {
c.search("", u.Full, "?")
} else {
content, links, err := gopher.Visit(u.Mime, u.Host, u.Port, u.Resource)
if err != nil {
c.SetMessage(err.Error(), true)
c.DrawMessage()
return
}
pg := MakePage(u, content, links)
c.handleGopher(u)
case "gemini":
c.handleGemini(u)
case "telnet":
c.handleTelnet(u)
case "http", "https":
c.handleWeb(u)
case "local":
c.handleLocal(u)
case "finger":
c.handleFinger(u)
default:
c.SetMessage(fmt.Sprintf("%q is not a supported protocol", u.Scheme), true)
c.DrawMessage()
}
}
// +++ Begin Protocol Handlers +++
func (c *client) handleGopher(u Url) {
if u.DownloadOnly {
nameSplit := strings.Split(u.Resource, "/")
filename := nameSplit[len(nameSplit)-1]
filename = strings.Trim(filename, " \t\r\n\v\f\a")
if filename == "" {
filename = "gopherfile"
}
c.saveFile(u, filename)
} else if u.Mime == "7" {
c.search("", u.Full, "?")
} else {
content, links, err := gopher.Visit(u.Mime, u.Host, u.Port, u.Resource)
if err != nil {
c.SetMessage(err.Error(), true)
c.DrawMessage()
return
}
pg := MakePage(u, content, links)
pg.WrapContent(c.Width - 1)
c.PageState.Add(pg)
c.SetPercentRead()
c.ClearMessage()
c.SetHeaderUrl()
c.Draw()
}
}
func (c *client) handleGemini(u Url) {
capsule, err := gemini.Visit(u.Host, u.Port, u.Resource, &c.Certs)
if err != nil {
c.SetMessage(err.Error(), true)
c.DrawMessage()
return
}
go saveConfig()
switch capsule.Status {
case 1:
c.search("", u.Full, capsule.Content)
case 2:
if capsule.MimeMaj == "text" {
pg := MakePage(u, capsule.Content, capsule.Links)
pg.WrapContent(c.Width - 1)
c.PageState.Add(pg)
c.SetPercentRead()
c.ClearMessage()
c.SetHeaderUrl()
c.Draw()
}
case "gemini":
capsule, err := gemini.Visit(u.Host, u.Port, u.Resource, &c.Certs)
if err != nil {
c.SetMessage(err.Error(), true)
} else {
c.SetMessage("The file is non-text: (o)pen or (w)rite to disk", false)
c.DrawMessage()
return
}
go saveConfig()
switch capsule.Status {
case 1:
c.search("", u.Full, capsule.Content)
case 2:
if capsule.MimeMaj == "text" {
pg := MakePage(u, capsule.Content, capsule.Links)
pg.WrapContent(c.Width - 1)
c.PageState.Add(pg)
c.SetPercentRead()
c.ClearMessage()
c.SetHeaderUrl()
c.Draw()
} else {
c.SetMessage("The file is non-text: (o)pen or (w)rite to disk", false)
c.DrawMessage()
var ch rune
for {
ch = cui.Getch()
if ch == 'o' || ch == 'w' {
break
}
var ch rune
for {
ch = cui.Getch()
if ch == 'o' || ch == 'w' {
break
}
switch ch {
case 'o':
mime := fmt.Sprintf("%s/%s", capsule.MimeMaj, capsule.MimeMin)
var term bool
if c.Options["terminalonly"] == "true" {
term = true
} else {
term = false
}
mcEntry, err := mc.FindMatch(mime, "view", term)
if err != nil {
c.SetMessage(err.Error(), true)
c.DrawMessage()
return
}
file, err := ioutil.TempFile("/tmp/", "bombadillo-*.tmp")
if err != nil {
c.SetMessage("Unable to create temporary file for opening, aborting file open", true)
c.DrawMessage()
return
}
// defer os.Remove(file.Name())
file.Write([]byte(capsule.Content))
com, e := mcEntry.Command(file.Name())
if e != nil {
c.SetMessage(e.Error(), true)
c.DrawMessage()
return
}
com.Stdin = os.Stdin
com.Stdout = os.Stdout
com.Stderr = os.Stderr
if c.Options["terminalonly"] == "true" {
cui.Clear("screen")
}
com.Run()
c.SetMessage("File opened by an appropriate program", true)
}
switch ch {
case 'o':
mime := fmt.Sprintf("%s/%s", capsule.MimeMaj, capsule.MimeMin)
var term bool
if c.Options["terminalonly"] == "true" {
term = true
} else {
term = false
}
mcEntry, err := mc.FindMatch(mime, "view", term)
if err != nil {
c.SetMessage(err.Error(), true)
c.DrawMessage()
c.Draw()
case 'w':
nameSplit := strings.Split(u.Resource, "/")
filename := nameSplit[len(nameSplit)-1]
c.saveFileFromData(capsule.Content, filename)
return
}
}
case 3:
c.SetMessage("[3] Redirect. Follow redirect? y or any other key for no", false)
c.DrawMessage()
ch := cui.Getch()
if ch == 'y' || ch == 'Y' {
c.Visit(capsule.Content)
} else {
c.SetMessage("Redirect aborted", false)
file, err := ioutil.TempFile("/tmp/", "bombadillo-*.tmp")
if err != nil {
c.SetMessage("Unable to create temporary file for opening, aborting file open", true)
c.DrawMessage()
return
}
// defer os.Remove(file.Name())
file.Write([]byte(capsule.Content))
com, e := mcEntry.Command(file.Name())
if e != nil {
c.SetMessage(e.Error(), true)
c.DrawMessage()
return
}
com.Stdin = os.Stdin
com.Stdout = os.Stdout
com.Stderr = os.Stderr
if c.Options["terminalonly"] == "true" {
cui.Clear("screen")
}
com.Run()
c.SetMessage("File opened by an appropriate program", true)
c.DrawMessage()
c.Draw()
case 'w':
nameSplit := strings.Split(u.Resource, "/")
filename := nameSplit[len(nameSplit)-1]
c.saveFileFromData(capsule.Content, filename)
}
}
case "telnet":
c.SetMessage("Attempting to start telnet session", false)
case 3:
c.SetMessage("[3] Redirect. Follow redirect? y or any other key for no", false)
c.DrawMessage()
msg, err := telnet.StartSession(u.Host, u.Port)
if err != nil {
c.SetMessage(err.Error(), true)
ch := cui.Getch()
if ch == 'y' || ch == 'Y' {
c.Visit(capsule.Content)
} else {
c.SetMessage("Redirect aborted", false)
c.DrawMessage()
}
}
}
func (c *client) handleTelnet(u Url) {
c.SetMessage("Attempting to start telnet session", false)
c.DrawMessage()
msg, err := telnet.StartSession(u.Host, u.Port)
if err != nil {
c.SetMessage(err.Error(), true)
c.DrawMessage()
} else {
c.SetMessage(msg, true)
c.DrawMessage()
}
c.Draw()
}
func (c *client) handleLocal(u Url) {
content, err := local.Open(u.Resource)
if err != nil {
c.SetMessage(err.Error(), true)
c.DrawMessage()
return
}
pg := MakePage(u, content, []string{})
pg.WrapContent(c.Width - 1)
c.PageState.Add(pg)
c.SetPercentRead()
c.ClearMessage()
c.SetHeaderUrl()
c.Draw()
}
func (c *client) handleFinger(u Url) {
content, err := finger.Finger(u.Host, u.Port, u.Resource)
if err != nil {
c.SetMessage(err.Error(), true)
c.DrawMessage()
return
}
pg := MakePage(u, content, []string{})
pg.WrapContent(c.Width - 1)
c.PageState.Add(pg)
c.SetPercentRead()
c.ClearMessage()
c.SetHeaderUrl()
c.Draw()
}
func (c *client) handleWeb(u Url) {
// Following http is disabled
if strings.ToUpper(c.Options["openhttp"]) != "TRUE" {
c.SetMessage("'openhttp' is not set to true, cannot open web link", false)
c.DrawMessage()
return
}
// Use lynxmode
if strings.ToUpper(c.Options["lynxmode"]) == "TRUE" {
if http.IsTextFile(u.Full) {
page, err := http.Visit(u.Full, c.Width-1)
if err != nil {
c.SetMessage(fmt.Sprintf("Lynx error: %s", err.Error()), true)
c.DrawMessage()
return
}
pg := MakePage(u, page.Content, page.Links)
pg.WrapContent(c.Width - 1)
c.PageState.Add(pg)
c.SetPercentRead()
c.ClearMessage()
c.SetHeaderUrl()
c.Draw()
} else {
c.SetMessage("The file is non-text: writing to disk...", false)
c.DrawMessage()
var fn string
if i := strings.LastIndex(u.Full, "/"); i > 0 && i + 1 < len(u.Full) {
fn = u.Full[i + 1:]
} else {
fn = "bombadillo.download"
}
c.saveFile(u, fn)
}
// Open in default web browser if available
} else {
if strings.ToUpper(c.Options["terminalonly"]) == "TRUE" {
c.SetMessage("'terminalonly' is set to true and 'lynxmode' is not enabled, cannot open web link", false)
c.DrawMessage()
} else {
c.SetMessage(msg, true)
c.SetMessage("Attempting to open in web browser", false)
c.DrawMessage()
}
c.Draw()
case "http", "https":
c.SetMessage("Attempting to open in web browser", false)
c.DrawMessage()
if strings.ToUpper(c.Options["openhttp"]) == "TRUE" {
msg, err := http.OpenInBrowser(u.Full)
if err != nil {
c.SetMessage(err.Error(), true)
@ -982,59 +1100,10 @@ func (c *client) Visit(url string) {
c.SetMessage(msg, false)
}
c.DrawMessage()
} else {
c.SetMessage("'openhttp' is not set to true, cannot open web link", false)
c.DrawMessage()
}
case "local":
content, err := local.Open(u.Resource)
if err != nil {
c.SetMessage(err.Error(), true)
c.DrawMessage()
return
}
pg := MakePage(u, content, []string{})
pg.WrapContent(c.Width - 1)
c.PageState.Add(pg)
c.SetPercentRead()
c.ClearMessage()
c.SetHeaderUrl()
c.Draw()
case "finger":
content, err := finger.Finger(u.Host, u.Port, u.Resource)
if err != nil {
c.SetMessage(err.Error(), true)
c.DrawMessage()
return
}
pg := MakePage(u, content, []string{})
pg.WrapContent(c.Width - 1)
c.PageState.Add(pg)
c.SetPercentRead()
c.ClearMessage()
c.SetHeaderUrl()
c.Draw()
default:
c.SetMessage(fmt.Sprintf("%q is not a supported protocol", u.Scheme), true)
c.DrawMessage()
}
}
func (c *client) ReloadPage() error {
if c.PageState.Length < 1 {
return fmt.Errorf("There is no page to reload")
}
url := c.PageState.History[c.PageState.Position].Location.Full
err := c.PageState.NavigateHistory(-1)
if err != nil {
return err
}
length := c.PageState.Length
c.Visit(url)
c.PageState.Length = length
return nil
}
//------------------------------------------------\\
// + + + F U N C T I O N S + + + \\

View File

@ -21,12 +21,12 @@ var defaultOptions = map[string]string{
"savelocation": userinfo.HomeDir,
"searchengine": "gopher://gopher.floodgap.com:70/7/v2/vs",
"openhttp": "false",
"httpbrowser": "",
"telnetcommand": "telnet",
"configlocation": userinfo.HomeDir,
"theme": "normal", // "normal", "inverted"
"terminalonly": "true",
"tlscertificate": "",
"tlskey": "",
"lynxmode": "false",
}

85
http/lynx_mode.go Normal file
View File

@ -0,0 +1,85 @@
package http
import (
"fmt"
"io/ioutil"
"net/http"
"os/exec"
"strings"
)
type page struct {
Content string
Links []string
}
func Visit(url string, width int) (page, error) {
if width > 80 {
width = 80
}
w := fmt.Sprintf("-width=%d", width)
c, err := exec.Command("lynx", "-dump", w, url).Output()
if err != nil {
return page{}, err
}
return parseLinks(string(c)), nil
}
// Returns false on err or non-text type
// Else returns true
func IsTextFile(url string) bool {
c, err := exec.Command("lynx", "-dump", "-head", url).Output()
if err != nil {
return false
}
content := string(c)
content = strings.ToLower(content)
headers := strings.Split(content, "\n")
for _, header := range headers {
if strings.Contains(header, "content-type:") && strings.Contains(header, "text") {
return true
}
}
return false
}
func parseLinks(c string) page {
var out page
contentUntil := strings.LastIndex(c, "References")
if contentUntil >= 1 {
out.Content = c[:contentUntil]
} else {
out.Content = c
out.Links = make([]string, 0)
return out
}
links := c[contentUntil+11:]
links = strings.TrimSpace(links)
linkSlice := strings.Split(links, "\n")
out.Links = make([]string, 0, len(linkSlice))
for _, link := range linkSlice {
ls := strings.SplitN(link, ".", 2)
if len(ls) < 2 {
continue
}
out.Links = append(out.Links, strings.TrimSpace(ls[1]))
}
return out
}
func Fetch(url string) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
return []byte{}, err
}
defer resp.Body.Close()
bodyBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return []byte{}, err
}
return bodyBytes, nil
}

View File

@ -1,6 +1,7 @@
package main
// Bombadillo is a gopher and gemini client for the terminal of unix or unix-like systems.
// Bombadillo is an internet client for the terminal of unix or
// unix-like systems.
//
// Copyright (C) 2019 Brian Evans
//
@ -69,6 +70,7 @@ func validateOpt(opt, val string) bool {
"openhttp": []string{"true", "false"},
"theme": []string{"normal", "inverse"},
"terminalonly": []string{"true", "false"},
"lynxmode": []string{"true", "false"},
}
opt = strings.ToLower(opt)