bombadillo/client.go

542 lines
12 KiB
Go

package main
import (
"fmt"
"io/ioutil"
"net"
"os"
"os/exec"
"os/user"
"regexp"
"strconv"
"strings"
"time"
"tildegit.org/sloum/bombadillo/cmdparse"
"tildegit.org/sloum/bombadillo/cui"
"tildegit.org/sloum/bombadillo/gopher"
)
//------------------------------------------------\\
// + + + T Y P E S + + + \\
//--------------------------------------------------\\
type client struct {
Height int
Width int
Options map[string]string
Message string
PageState Pages
BookMarks Bookmarks
TopBar Headbar
FootBar Footbar
}
//------------------------------------------------\\
// + + + R E C E I V E R S + + + \\
//--------------------------------------------------\\
func (c *client) GetSize() {
for {
redraw := false
cmd := exec.Command("stty", "size")
cmd.Stdin = os.Stdin
out, err := cmd.Output()
if err != nil {
fmt.Println("Fatal error: Unable to retrieve terminal size")
os.Exit(5)
}
var h, w int
fmt.Sscan(string(out), &h, &w)
if h != c.Height || w != c.Width {
redraw = true
}
c.Height = h
c.Width = w
if redraw {
c.Draw()
}
time.Sleep(1 * time.Second)
}
}
func (c *client) Draw() {
// TODO build this out.
// It should call all of the renders
// and add them to the a string buffer
// It should then print the buffer
}
func (c *client) TakeControlInput() {
input := cui.Getch()
switch input {
case 'j', 'J':
// scroll down one line
c.Scroll(1)
case 'k', 'K':
// scroll up one line
c.Scroll(-1)
case 'q', 'Q':
// quite bombadillo
cui.Exit()
case 'g':
// scroll to top
c.Scroll(-len(c.PageState.History[c.PageState.Position].WrappedContent))
case 'G':
// scroll to bottom
c.Scroll(len(c.PageState.History[c.PageState.Position].WrappedContent))
case 'd':
// scroll down 75%
distance := c.Height - c.Height / 4
c.Scroll(distance)
case 'u':
// scroll up 75%
distance := c.Height - c.Height / 4
c.Scroll(-distance)
case 'b':
// go back
err := c.PageState.NavigateHistory(-1)
if err != nil {
c.SetMessage(err.Error(), false)
c.DrawMessage()
} else {
c.Draw()
}
case 'B':
// open the bookmarks browser
c.BookMarks.ToggleOpen()
c.Draw()
case 'f', 'F':
// go forward
err := c.PageState.NavigateHistory(1)
if err != nil {
c.SetMessage(err.Error(), false)
c.DrawMessage()
} else {
c.Draw()
}
case '\t':
// Toggle bookmark browser focus on/off
c.BookMarks.ToggleFocused()
c.Draw()
case ':', ' ':
// Process a command
c.ClearMessage()
c.ClearMessageLine()
entry, err := cui.GetLine()
c.ClearMessageLine()
if err != nil {
c.SetMessage(err.Error(), true)
c.DrawMessage()
break
} else if strings.TrimSpace(entry) == "" {
break
}
parser := cmdparse.NewParser(strings.NewReader(entry))
p, err := parser.Parse()
if err != nil {
c.SetMessage(err.Error(), true)
c.DrawMessage()
} else {
err := c.routeCommandInput(p)
if err != nil {
c.SetMessage(err.Error(), true)
c.DrawMessage()
}
}
}
}
func (c *client) routeCommandInput(com *cmdparse.Command) error {
var err error
switch com.Type {
case cmdparse.SIMPLE:
c.simpleCommand(com.Action)
case cmdparse.GOURL:
c.goToURL(com.Target)
case cmdparse.GOLINK:
c.goToLink(com.Target)
case cmdparse.DO:
c.doCommand(com.Action, com.Value)
case cmdparse.DOLINK:
// err = doLinkCommand(com.Action, com.Target)
case cmdparse.DOAS:
c.doCommandAs(com.Action, com.Value)
case cmdparse.DOLINKAS:
// err = doLinkCommandAs(com.Action, com.Target, com.Value)
default:
return fmt.Errorf("Unknown command entry!")
}
return err
}
func (c *client) simpleCommand(action string) {
action = strings.ToUpper(action)
switch action {
case "Q", "QUIT":
cui.Exit()
case "H", "HOME":
if c.Options["homeurl"] != "unset" {
go c.Visit(c.Options["homeurl"])
} else {
c.SetMessage(fmt.Sprintf("No home address has been set"), false)
c.DrawMessage()
}
case "B", "BOOKMARKS":
c.BookMarks.ToggleOpen()
case "SEARCH":
c.search()
case "HELP", "?":
go c.Visit(helplocation)
default:
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 "CHECK", "C":
c.displayConfigValue(values[0])
default:
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 1 argument, 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":
err := c.BookMarks.Add(values)
if err != nil {
c.SetMessage(err.Error(), true)
c.DrawMessage()
return
}
err = saveConfig()
if err != nil {
c.SetMessage("Error saving bookmark to file", true)
c.DrawMessage()
}
if c.BookMarks.IsOpen {
c.Draw()
}
case "WRITE", "W":
// TODO figure out how best to handle file
// writing... it will depend on request model
// using fetch would be best
// - - - - - - - - - - - - - - - - - - - - -
// var data []byte
// if values[0] == "." {
// d, err := c.getCurrentPageRawData()
// if err != nil {
// c.SetMessage(err.Error(), true)
// c.DrawMessage()
// return
// }
// data = []byte(d)
// }
// fp, err := c.saveFile(data, strings.Join(values[1:], " "))
// if err != nil {
// c.SetMessage(err.Error(), true)
// c.DrawMessage()
// return
// }
// c.SetMessage(fmt.Sprintf("File saved to: %s", fp), false)
// c.DrawMessage()
case "SET", "S":
if _, ok := c.Options[values[0]]; ok {
c.Options[values[0]] = strings.Join(values[1:], " ")
err := saveConfig()
if err != nil {
c.SetMessage("Value set, but error saving config to file", true)
c.DrawMessage()
} else {
c.SetMessage(fmt.Sprintf("%s is now set to %q", values[0], c.Options[values[0]]), true)
c.DrawMessage()
}
return
}
c.SetMessage(fmt.Sprintf("Unable to set %s, it does not exist", values[0]), true)
c.DrawMessage()
return
}
c.SetMessage(fmt.Sprintf("Unknown command structure"), true)
}
func (c *client) getCurrentPageUrl() (string, error) {
if c.PageState.Length < 1 {
return "", fmt.Errorf("There are no pages in history")
}
return c.PageState.History[c.PageState.Position].Location.Full, nil
}
func (c *client) getCurrentPageRawData() (string, error) {
if c.PageState.Length < 1 {
return "", fmt.Errorf("There are no pages in history")
}
return c.PageState.History[c.PageState.Position].RawContent, nil
}
func (c *client) saveFile(data []byte, name string) (string, error) {
savePath := c.Options["savelocation"] + name
err := ioutil.WriteFile(savePath, data, 0644)
if err != nil {
return "", err
}
return savePath, nil
}
func (c *client) search() {
c.ClearMessage()
c.ClearMessageLine()
fmt.Print("?")
entry, err := cui.GetLine()
c.ClearMessageLine()
if err != nil {
c.SetMessage(err.Error(), true)
c.DrawMessage()
return
} else if strings.TrimSpace(entry) == "" {
return
}
u, err := MakeUrl(c.Options["searchurl"])
if err != nil {
c.SetMessage("'searchurl' is not set to a valid url", true)
c.DrawMessage()
return
}
switch u.Scheme {
case "gopher":
go c.Visit(fmt.Sprintf("%s\t%s",u.Full,entry))
case "gemini":
// TODO url escape the entry variable
escapedEntry := entry
go c.Visit(fmt.Sprintf("%s?%s",u.Full,escapedEntry))
case "http", "https":
c.SetMessage("Attempting to open in web browser", false)
c.DrawMessage()
err := gopher.OpenBrowser(u.Full)
if err != nil {
c.SetMessage(err.Error(), true)
} else {
c.SetMessage("Opened in web browser", false)
}
c.DrawMessage()
default:
c.SetMessage(fmt.Sprintf("%q is not a supported protocol", u.Scheme), true)
c.DrawMessage()
}
}
func (c *client) Scroll(amount int) {
page := c.PageState.History[c.PageState.Position]
bottom := len(page.WrappedContent) - c.Height
if amount < 0 && page.ScrollPosition == 0 {
c.SetMessage("You are already at the top", false)
c.DrawMessage()
fmt.Print("\a")
return
} else if amount > 0 && page.ScrollPosition == bottom || bottom < 0 {
c.SetMessage("You are already at the bottom", false)
c.DrawMessage()
fmt.Print("\a")
return
}
newScrollPosition := page.ScrollPosition + amount
if newScrollPosition < 0 {
newScrollPosition = 0
} else if newScrollPosition > bottom {
newScrollPosition = bottom
}
page.ScrollPosition = newScrollPosition
c.Draw()
}
func (c *client) displayConfigValue(setting string) {
if val, ok := c.Options[setting]; ok {
c.SetMessage(fmt.Sprintf("%s is set to: %q", setting, val), false)
c.DrawMessage()
} else {
c.SetMessage(fmt.Sprintf("Invalid: %q does not exist", setting), true)
c.DrawMessage()
}
}
func (c *client) SetMessage(msg string, isError bool) {
leadIn, leadOut := "", ""
if isError {
leadIn = "\033[31m"
leadOut = "\033[0m"
}
c.Message = fmt.Sprintf("%s%s%s", leadIn, msg, leadOut)
}
func (c *client) DrawMessage() {
c.ClearMessageLine()
cui.MoveCursorTo(c.Height-1, 0)
fmt.Print(c.Message)
}
func (c *client) ClearMessage() {
c.Message = ""
}
func (c *client) ClearMessageLine() {
cui.MoveCursorTo(c.Height-1, 0)
cui.Clear("line")
}
func (c *client) goToURL(u string) {
if num, _ := regexp.MatchString(`^-?\d+.?\d*$`, u); num {
c.goToLink(u)
return
}
go c.Visit(u)
}
func (c *client) goToLink(l string) {
if num, _ := regexp.MatchString(`^-?\d+$`, l); num && c.PageState.Length > 0 {
linkcount := len(c.PageState.History[c.PageState.Position].Links)
item, err := strconv.Atoi(l)
if err != nil {
c.SetMessage(fmt.Sprintf("Invalid link id: %s", l), true)
c.DrawMessage()
return
}
if item <= linkcount && item > 0 {
linkurl := c.PageState.History[c.PageState.Position].Links[item-1]
c.Visit(linkurl)
} else {
c.SetMessage(fmt.Sprintf("Invalid link id: %s", l), true)
c.DrawMessage()
return
}
}
c.SetMessage(fmt.Sprintf("Invalid link id: %s", l), true)
c.DrawMessage()
}
func (c *client) Visit(url string) {
u, err := MakeUrl(url)
if err != nil {
c.SetMessage(err.Error(), true)
c.DrawMessage()
return
}
switch u.Scheme {
case "gopher":
// TODO send over to gopher request
case "gemini":
// TODO send over to gemini request
case "http", "https":
c.SetMessage("Attempting to open in web browser", false)
c.DrawMessage()
if strings.ToUpper(c.Options["openhttp"]) == "TRUE" {
err := gopher.OpenBrowser(u.Full)
if err != nil {
c.SetMessage(err.Error(), true)
} else {
c.SetMessage("Opened in web browser", false)
}
c.DrawMessage()
} else {
c.SetMessage("'openhttp' is not set to true, aborting opening web link", false)
c.DrawMessage()
}
default:
c.SetMessage(fmt.Sprintf("%q is not a supported protocol", u.Scheme), true)
c.DrawMessage()
}
}
//------------------------------------------------\\
// + + + F U N C T I O N S + + + \\
//--------------------------------------------------\\
func MakeClient(name string) *client {
var userinfo, _ = user.Current()
var options = map[string]string{
"homeurl": "gopher://colorfield.space:70/1/bombadillo-info",
"savelocation": userinfo.HomeDir,
"searchengine": "gopher://gopher.floodgap.com:70/7/v2/vs",
"openhttp": "false",
"httpbrowser": "lynx",
"configlocation": userinfo.HomeDir,
}
c := client{0, 0, options, "", MakePages(), MakeBookmarks(), MakeHeadbar(name), MakeFootbar()}
c.GetSize()
return &c
}
// Retrieve a byte slice of raw response dataa
// from a url string
func Fetch(url string) ([]byte, error) {
u, err := MakeUrl(url)
if err != nil {
return []byte(""), err
}
timeOut := time.Duration(5) * time.Second
if u.Host == "" || u.Port == "" {
return []byte(""), fmt.Errorf("Incomplete request url")
}
addr := u.Host + ":" + u.Port
conn, err := net.DialTimeout("tcp", addr, timeOut)
if err != nil {
return []byte(""), err
}
send := u.Resource + "\n"
_, err = conn.Write([]byte(send))
if err != nil {
return []byte(""), err
}
result, err := ioutil.ReadAll(conn)
if err != nil {
return []byte(""), err
}
return result, err
}