diff --git a/README.md b/README.md index 688b199..ce879ef 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Support for the following protocols is also available via integration with 3rd p * http/https * Web support is opt-in (turned off by default). * Links can be opened in a user's default web browser when in a graphical environment. - * Web pages can be rendered directly in Bombadillo if [Lynx](https://lynx.invisible-island.net/) is installed on the system to handle the document parsing. + * Web pages can be rendered directly in Bombadillo if [Lynx](https://lynx.invisible-island.net/), [w3m](http://w3m.sourceforge.net/), or [elinks](http://elinks.or.cz/) are installed on the system to handle the document parsing. ## Getting Started diff --git a/bombadillo.1 b/bombadillo.1 index 3e58ca5..7402a1d 100644 --- a/bombadillo.1 +++ b/bombadillo.1 @@ -46,11 +46,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, \fBbombadillo\fP can open web links in a user's default web browser, or display web content directly in the browser via the lynx web browser. Opening http/https links is opt-in only, controlled by the \fIopenhttp\fP setting. +Neither of the world wide web protocols are supported directly. \fBbombadillo\fP can be configured to open web links in a user's default graphical web browser. It is also possible to display web content directly in \fBbombadillo\fP using lynx, w3m, or elinks terminal web browsers to render pages. Opening http/https links is opt-in only, controlled by the \fIwebmode\fP setting. .IP -Opening links in a default web browser only works if a GUI environment is available. +Opening links in a default graphical web browser will only work in a GUI environment. .IP -Opening web content directly in the browser requires the lynx web browser, and is enabled using the \fIlynxmode\fP setting. Web content is processed using lynx, and then displayed in \fBbombadillo\fP. +Displaying web content directly in \fBbombadillo\fP requires lynx, w3m or elinks terminal web browsers are installed on the system. .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. @@ -210,14 +210,6 @@ 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 -Sets whether or not to use lynx as a rendering engine for http/https requests, and display results in \fBbombadillo\fP. Lynx must be installed, and \fIopenhttp\fP must be set to \fItrue\fP. Valid values are \fItrue\fP and \fIfalse\fP. -.TP -.B -openhttp -Tells the browser whether or not to try to follow web (http/https) links. If set to \fItrue\fP, \fBbombadillo\fP will try to open these links in a user's default web browser, or render them via lynx, depending on the status of \fIlynxmode\fP and \fIterminalonly\fP settings. Valid values are \fItrue\fP and \fIfalse\fP. -.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 @@ -230,10 +222,6 @@ telnetcommand Tells the browser what command to use to start a telnet session. Should be a valid command, including any flags. The address being navigated to will be added to the end of the command. .TP .B -terminalonly -Sets whether web links should handled within \fBbombadillo\fP only, or if they can be opened in a user's external web browser. If \fIopenhttp\fP is true, and this setting is true, web links will be handled within \fBbombadillo\fP only. If \fIlynxmode\fP is also true, web links will be rendered in bombadillo via lynx. If \fIopenhttp\fP is true, \fIlynxmode\fP is disabled, and this setting is disabled, \fBbombadillo\fP will open web links in a user's default web browser. Valid values are \fItrue\fP and \fIfalse\fP. -.TP -.B theme Can toggle between visual modes. Valid values are \fInormal\fP and \fIinverse\fP. When set to inverse, the terminal color mode is inverted. .TP @@ -244,6 +232,11 @@ A path to a tls certificate file on a user's local filesystem. Defaults to NULL. .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 +Controls behavior when following web links. The following values are valid: \fInone\fP will disable following web links, \fIgui\fP will have the browser attempt to open web links in a user's default graphical web browser; \fIlynx\fP, \fIw3m\fP, and \fIelinks\fP will have the browser attempt to use the selected terminal web browser to handle the rendering of web pages and will display the pages directly in Bombadillo. + .SH BUGS There are very likely bugs. Many known bugs can be found in the issues section of \fBbombadillo\fP's source code repository (see \fIlinks\fP). .SH LINKS diff --git a/client.go b/client.go index b590755..23f4925 100644 --- a/client.go +++ b/client.go @@ -643,7 +643,7 @@ func (c *client) search(query, url, question string) { c.DrawMessage() return } else if strings.TrimSpace(entry) == "" { - c.ClearMessage() + c.ClearMessage() c.DrawMessage() return } @@ -995,19 +995,13 @@ func (c *client) handleFinger(u Url) { } 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" { + wm := strings.ToLower(c.Options["webmode"]) + switch wm { + case "lynx", "w3m", "elinks": if http.IsTextFile(u.Full) { - page, err := http.Visit(u.Full, c.Width-1) + page, err := http.Visit(wm, u.Full, c.Width-1) if err != nil { - c.SetMessage(fmt.Sprintf("Lynx error: %s", err.Error()), true) + c.SetMessage(fmt.Sprintf("%s error: %s", wm, err.Error()), true) c.DrawMessage() return } @@ -1029,23 +1023,19 @@ func (c *client) handleWeb(u Url) { } 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() + case "gui": + c.SetMessage("Attempting to open in gui web browser", false) + c.DrawMessage() + msg, err := http.OpenInBrowser(u.Full) + if err != nil { + c.SetMessage(err.Error(), true) } else { - c.SetMessage("Attempting to open in web browser", false) - c.DrawMessage() - msg, err := http.OpenInBrowser(u.Full) - if err != nil { - c.SetMessage(err.Error(), true) - } else { - c.SetMessage(msg, false) - } - c.DrawMessage() + c.SetMessage(msg, false) } + c.DrawMessage() + default: + c.SetMessage("Current 'webmode' setting does not allow http/https", false) + c.DrawMessage() } } diff --git a/defaults.go b/defaults.go index 9da05a5..a89e7f5 100644 --- a/defaults.go +++ b/defaults.go @@ -48,14 +48,12 @@ var defaultOptions = map[string]string{ "homeurl": "gopher://bombadillo.colorfield.space:70/1/user-guide.map", "savelocation": homePath(), "searchengine": "gopher://gopher.floodgap.com:70/7/v2/vs", - "openhttp": "false", "telnetcommand": "telnet", "configlocation": xdgConfigPath(), "theme": "normal", // "normal", "inverted" - "terminalonly": "true", "tlscertificate": "", "tlskey": "", - "lynxmode": "false", + "webmode": "none", // "none", "gui", "lynx", "w3m", "elinks" } // homePath will return the path to your home directory as a string diff --git a/http/http_render.go b/http/http_render.go new file mode 100644 index 0000000..f68ee2d --- /dev/null +++ b/http/http_render.go @@ -0,0 +1,98 @@ +package http + +import ( + "fmt" + "io/ioutil" + "net/http" + "os/exec" + "strings" +) + +// Page represents the contents and links or an http/https document +type Page struct { + Content string + Links []string +} + +// Visit is the main entry to viewing a web document in bombadillo. +// It takes a url, a terminal width, and which web backend the user +// currently has set. Visit returns a Page and an error +func Visit(webmode, url string, width int) (Page, error) { + if width > 80 { + width = 80 + } + var w string + switch webmode { + case "lynx": + w = "-width" + case "w3m": + w = "-cols" + case "elinks": + w = "-dump-width" + default: + return Page{}, fmt.Errorf("Invalid webmode setting") + } + c, err := exec.Command(webmode, "-dump", w, fmt.Sprintf("%d", width), url).Output() + if err != nil { + return Page{}, err + } + return parseLinks(string(c)), nil +} + +// IsTextFile makes an http(s) head request to a given URL +// and determines if the content-type is text based. It then +// returns a bool +func IsTextFile(url string) bool { + resp, err := http.Head(url) + if err != nil { + return false + } + ctype := resp.Header.Get("content-type") + if strings.Contains(ctype, "text") || ctype == "" { + 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 +} + +// Fetch makes an http(s) request and returns the []bytes +// for the response and an error. Fetch is used for saving +// the source file of an http(s) document +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 +} diff --git a/http/lynx_mode.go b/http/lynx_mode.go deleted file mode 100644 index 782aee3..0000000 --- a/http/lynx_mode.go +++ /dev/null @@ -1,91 +0,0 @@ -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 - } else if strings.Contains(header, "content-type:") { - return false - } - } - - // If we made it here, there is no content-type header. - // So in the event of the unknown, lets render to the - // screen. This will allow redirects to get rendered - // as well. - return true -} - -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 -} diff --git a/http/open_browser_linux.go b/http/open_browser_linux.go index dc99845..3b7dfee 100644 --- a/http/open_browser_linux.go +++ b/http/open_browser_linux.go @@ -2,10 +2,27 @@ package http -import "os/exec" +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) { - err := exec.Command("xdg-open", url).Start() + 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 } diff --git a/http/open_browser_other.go b/http/open_browser_other.go index c6e5342..1388c6b 100644 --- a/http/open_browser_other.go +++ b/http/open_browser_other.go @@ -7,5 +7,5 @@ package http import "fmt" func OpenInBrowser(url string) (string, error) { - return "", fmt.Errorf("Unsupported os for browser detection") + return "", fmt.Errorf("Unsupported os for 'webmode' 'gui' setting") } diff --git a/main.go b/main.go index 055ee1f..ef01c34 100644 --- a/main.go +++ b/main.go @@ -66,10 +66,8 @@ func saveConfig() error { func validateOpt(opt, val string) bool { var validOpts = map[string][]string{ - "openhttp": []string{"true", "false"}, - "theme": []string{"normal", "inverse"}, - "terminalonly": []string{"true", "false"}, - "lynxmode": []string{"true", "false"}, + "webmode": []string{"none", "gui", "lynx", "w3m", "elinks"}, + "theme": []string{"normal", "inverse"}, } opt = strings.ToLower(opt) @@ -88,7 +86,7 @@ func validateOpt(opt, val string) bool { func lowerCaseOpt(opt, val string) string { switch opt { - case "openhttp", "theme", "terminalonly": + case "webmode", "theme": return strings.ToLower(val) default: return val