Browse Source

Initial v2 commit, deep in restructuring... maybe not for the better?

pull/40/head
sloumdrone 2 years ago
parent
commit
da45f627e0
  1. 68
      bookmarks.go
  2. 541
      client.go
  3. 54
      footbar.go
  4. 2
      gopher/gopher.go
  5. 2
      gopher/open_browser_darwin.go
  6. 2
      gopher/open_browser_linux.go
  7. 2
      gopher/open_browser_other.go
  8. 2
      gopher/open_browser_windows.go
  9. 47
      headbar.go
  10. 642
      main.go
  11. 30
      page.go
  12. 54
      pages.go
  13. 107
      url.go

68
bookmarks.go

@ -0,0 +1,68 @@
package main
import (
"fmt"
)
//------------------------------------------------\\
// + + + T Y P E S + + + \\
//--------------------------------------------------\\
type Bookmarks struct {
IsOpen bool
IsFocused bool
Position int
Length int
Titles []string
Links []string
}
//------------------------------------------------\\
// + + + R E C E I V E R S + + + \\
//--------------------------------------------------\\
func (b *Bookmarks) Add([]string) error {
// TODO add a bookmark
return fmt.Errorf("")
}
func (b *Bookmarks) Delete(int) error {
// TODO delete a bookmark
return fmt.Errorf("")
}
func (b *Bookmarks) ToggleOpen() {
b.IsOpen = !b.IsOpen
if b.IsOpen {
b.IsFocused = true
} else {
b.IsFocused = false
}
}
func (b *Bookmarks) ToggleFocused() {
if b.IsOpen {
b.IsFocused = !b.IsFocused
}
}
func (b *Bookmarks) IniDump() string {
// TODO create dump of values for INI file
return ""
}
func (b *Bookmarks) Render() ([]string, error) {
// TODO grab all of the bookmarks as a fixed
// width string including border and spacing
return []string{}, fmt.Errorf("")
}
//------------------------------------------------\\
// + + + F U N C T I O N S + + + \\
//--------------------------------------------------\\
func MakeBookmarks() Bookmarks {
return Bookmarks{false, false, 0, 0, make([]string, 0), make([]string, 0)}
}

541
client.go

@ -0,0 +1,541 @@
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
}

54
footbar.go

@ -0,0 +1,54 @@
package main
import (
"fmt"
)
//------------------------------------------------\\
// + + + T Y P E S + + + \\
//--------------------------------------------------\\
type Footbar struct {
PercentRead string
PageType string
Content string
}
//------------------------------------------------\\
// + + + R E C E I V E R S + + + \\
//--------------------------------------------------\\
func (f *Footbar) SetPercentRead(p int) {
f.PercentRead = fmt.Sprintf("%d%%", p)
}
func (f *Footbar) SetPageType(t string) {
f.PageType = t
}
func (f *Footbar) Draw() {
// TODO this will actually draw the bar
// without having to redraw everything else
}
func (f *Footbar) Build(width string) string {
// TODO Build out header to specified width
f.Content = "" // This is a temp value to show intention
return ""
}
func (f *Footbar) Render() string {
// TODO returns a full line
return ""
}
//------------------------------------------------\\
// + + + F U N C T I O N S + + + \\
//--------------------------------------------------\\
func MakeFootbar() Footbar {
return Footbar{"", "N/A", ""}
}

2
gopher/gopher.go

@ -88,7 +88,7 @@ func Visit(addr, openhttp string) (View, error) {
if u.Gophertype == "h" {
if res, tf := isWebLink(u.Resource); tf && strings.ToUpper(openhttp) == "TRUE" {
err := openBrowser(res)
err := OpenBrowser(res)
if err != nil {
return View{}, err
}

2
gopher/open_browser_darwin.go

@ -4,6 +4,6 @@ package gopher
import "os/exec"
func openBrowser(url string) error {
func OpenBrowser(url string) error {
return exec.Command("open", url).Start()
}

2
gopher/open_browser_linux.go

@ -4,6 +4,6 @@ package gopher
import "os/exec"
func openBrowser(url string) error {
func OpenBrowser(url string) error {
return exec.Command("xdg-open", url).Start()
}

2
gopher/open_browser_other.go

@ -6,6 +6,6 @@ package gopher
import "fmt"
func openBrowser(url string) error {
func OpenBrowser(url string) error {
return fmt.Errorf("Unsupported os for browser detection")
}

2
gopher/open_browser_windows.go

@ -4,6 +4,6 @@ package gopher
import "os/exec"
func openBrowser(url string) error {
func OpenBrowser(url string) error {
return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
}

47
headbar.go

@ -0,0 +1,47 @@
package main
//------------------------------------------------\\
// + + + T Y P E S + + + \\
//--------------------------------------------------\\
type Headbar struct {
title string
url string
content string
}
//------------------------------------------------\\
// + + + R E C E I V E R S + + + \\
//--------------------------------------------------\\
func (h *Headbar) SetUrl(u string) {
h.url = u
}
func (h *Headbar) Build(width string) string {
// TODO Build out header to specified width
h.content = "" // This is a temp value to show intention
return ""
}
func (h *Headbar) Draw() {
// TODO this will actually draw the bar
// without having to redraw everything else
}
func (h *Headbar) Render() string {
// TODO returns the content value
return ""
}
//------------------------------------------------\\
// + + + F U N C T I O N S + + + \\
//--------------------------------------------------\\
func MakeHeadbar(title string) Headbar {
return Headbar{title, "", title}
}

642
main.go

@ -1,402 +1,143 @@
package main
import (
"fmt"
"io/ioutil"
"os"
"os/user"
"regexp"
"strconv"
// "strconv"
"strings"
"tildegit.org/sloum/bombadillo/cmdparse"
"tildegit.org/sloum/bombadillo/config"
"tildegit.org/sloum/bombadillo/cui"
"tildegit.org/sloum/bombadillo/gopher"
// "tildegit.org/sloum/bombadillo/gopher"
)
var bombadillo *client
var helplocation string = "gopher://colorfield.space:70/1/bombadillo-info"
var history gopher.History = gopher.MakeHistory()
var screen *cui.Screen
var userinfo, _ = user.Current()
var settings config.Config
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",
}
func saveFile(address, name string) error {
quickMessage("Saving file...", false)
url, err := gopher.MakeUrl(address)
if err != nil {
quickMessage("Saving file...", true)
return err
}
data, err := gopher.Retrieve(url)
if err != nil {
quickMessage("Saving file...", true)
return err
}
err = ioutil.WriteFile(options["savelocation"]+name, data, 0644)
if err != nil {
quickMessage("Saving file...", true)
return err
}
quickMessage(fmt.Sprintf("Saved file to %s%s", options["savelocation"], name), false)
return nil
}
func saveFileFromData(v gopher.View) error {
quickMessage("Saving file...", false)
urlsplit := strings.Split(v.Address.Full, "/")
filename := urlsplit[len(urlsplit)-1]
saveMsg := fmt.Sprintf("Saved file as %q", options["savelocation"]+filename)
err := ioutil.WriteFile(options["savelocation"]+filename, []byte(strings.Join(v.Content, "")), 0644)
if err != nil {
quickMessage("Saving file...", true)
return err
}
quickMessage(saveMsg, false)
return nil
}
func search(u string) error {
cui.MoveCursorTo(screen.Height-1, 0)
cui.Clear("line")
fmt.Print("Enter form input: ")
cui.MoveCursorTo(screen.Height-1, 17)
entry, err := cui.GetLine()
if err != nil {
return err
}
quickMessage("Searching...", false)
searchurl := fmt.Sprintf("%s\t%s", u, entry)
sv, err := gopher.Visit(searchurl, options["openhttp"])
if err != nil {
quickMessage("Searching...", true)
return err
}
history.Add(sv)
quickMessage("Searching...", true)
updateMainContent()
screen.Windows[0].Scrollposition = 0
screen.ReflashScreen(true)
return nil
}
func routeInput(com *cmdparse.Command) error {
var err error
switch com.Type {
case cmdparse.SIMPLE:
err = simpleCommand(com.Action)
case cmdparse.GOURL:
err = goToURL(com.Target)
case cmdparse.GOLINK:
err = goToLink(com.Target)
case cmdparse.DO:
err = doCommand(com.Action, com.Value)
case cmdparse.DOLINK:
err = doLinkCommand(com.Action, com.Target)
case cmdparse.DOAS:
err = 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 toggleBookmarks() {
bookmarks := screen.Windows[1]
main := screen.Windows[0]
if bookmarks.Show {
bookmarks.Show = false
screen.Activewindow = 0
main.Active = true
bookmarks.Active = false
} else {
bookmarks.Show = true
screen.Activewindow = 1
main.Active = false
bookmarks.Active = true
}
screen.ReflashScreen(false)
}
func simpleCommand(a string) error {
a = strings.ToUpper(a)
switch a {
case "Q", "QUIT":
cui.Exit()
case "H", "HOME":
return goHome()
case "B", "BOOKMARKS":
toggleBookmarks()
case "SEARCH":
return search(options["searchengine"])
case "HELP", "?":
return goToURL(helplocation)
default:
return fmt.Errorf("Unknown action %q", a)
}
return nil
}
func goToURL(u string) error {
if num, _ := regexp.MatchString(`^-?\d+.?\d*$`, u); num {
return goToLink(u)
}
quickMessage("Loading...", false)
v, err := gopher.Visit(u, options["openhttp"])
if err != nil {
quickMessage("Loading...", true)
return err
}
quickMessage("Loading...", true)
if v.Address.Gophertype == "7" {
err := search(v.Address.Full)
if err != nil {
return err
}
} else if v.Address.IsBinary {
return saveFileFromData(v)
} else {
history.Add(v)
}
updateMainContent()
screen.Windows[0].Scrollposition = 0
screen.ReflashScreen(true)
return nil
}
func goToLink(l string) error {
if num, _ := regexp.MatchString(`^-?\d+$`, l); num && history.Length > 0 {
linkcount := len(history.Collection[history.Position].Links)
item, _ := strconv.Atoi(l)
if item <= linkcount && item > 0 {
linkurl := history.Collection[history.Position].Links[item-1]
quickMessage("Loading...", false)
v, err := gopher.Visit(linkurl, options["openhttp"])
if err != nil {
quickMessage("Loading...", true)
return err
}
quickMessage("Loading...", true)
if v.Address.Gophertype == "7" {
err := search(linkurl)
if err != nil {
return err
}
} else if v.Address.IsBinary {
return saveFileFromData(v)
} else {
history.Add(v)
}
} else {
return fmt.Errorf("Invalid link id: %s", l)
}
} else {
return fmt.Errorf("Invalid link id: %s", l)
}
updateMainContent()
screen.Windows[0].Scrollposition = 0
screen.ReflashScreen(true)
return nil
}
func goHome() error {
if options["homeurl"] != "unset" {
return goToURL(options["homeurl"])
}
return fmt.Errorf("No home address has been set")
}
func doLinkCommand(action, target string) error {
num, err := strconv.Atoi(target)
if err != nil {
return fmt.Errorf("Expected number, got %q", target)
}
switch action {
case "DELETE", "D":
err := settings.Bookmarks.Del(num)
if err != nil {
return err
}
screen.Windows[1].Content = settings.Bookmarks.List()
err = saveConfig()
if err != nil {
return err
}
screen.ReflashScreen(false)
return nil
case "BOOKMARKS", "B":
if num > len(settings.Bookmarks.Links)-1 {
return fmt.Errorf("There is no bookmark with ID %d", num)
}
err := goToURL(settings.Bookmarks.Links[num])
return err
}
return fmt.Errorf("This method has not been built")
}
func doCommandAs(action string, values []string) error {
if len(values) < 2 {
return fmt.Errorf("%q", values)
}
if values[0] == "." {
values[0] = history.Collection[history.Position].Address.Full
}
switch action {
case "ADD", "A":
err := settings.Bookmarks.Add(values)
if err != nil {
return err
}
screen.Windows[1].Content = settings.Bookmarks.List()
err = saveConfig()
if err != nil {
return err
}
screen.ReflashScreen(false)
return nil
case "WRITE", "W":
return saveFile(values[0], strings.Join(values[1:], " "))
case "SET", "S":
if _, ok := options[values[0]]; ok {
options[values[0]] = strings.Join(values[1:], " ")
return saveConfig()
}
return fmt.Errorf("Unable to set %s, it does not exist", values[0])
}
return fmt.Errorf("Unknown command structure")
}
func doCommand(action string, values []string) error {
if length := len(values); length != 1 {
return fmt.Errorf("Expected 1 argument, received %d", length)
}
switch action {
case "CHECK", "C":
err := checkConfigValue(values[0])
if err != nil {
return err
}
return nil
}
return fmt.Errorf("Unknown command structure")
}
func checkConfigValue(setting string) error {
if val, ok := options[setting]; ok {
quickMessage(fmt.Sprintf("%s is set to: %q", setting, val), false)
return nil
}
return fmt.Errorf("Unable to check %q, it does not exist", setting)
}
func doLinkCommandAs(action, target string, values []string) error {
num, err := strconv.Atoi(target)
if err != nil {
return fmt.Errorf("Expected number, got %q", target)
}
links := history.Collection[history.Position].Links
if num >= len(links) {
return fmt.Errorf("Invalid link id: %s", target)
}
switch action {
case "ADD", "A":
newBookmark := append([]string{links[num-1]}, values...)
err := settings.Bookmarks.Add(newBookmark)
if err != nil {
return err
}
screen.Windows[1].Content = settings.Bookmarks.List()
err = saveConfig()
if err != nil {
return err
}
screen.ReflashScreen(false)
return nil
case "WRITE", "W":
return saveFile(links[num-1], strings.Join(values, " "))
}
return fmt.Errorf("This method has not been built")
}
func updateMainContent() {
screen.Windows[0].Content = history.Collection[history.Position].Content
screen.Bars[0].SetMessage(history.Collection[history.Position].Address.Full)
}
func clearInput(incError bool) {
cui.MoveCursorTo(screen.Height-1, 0)
cui.Clear("line")
if incError {
cui.MoveCursorTo(screen.Height, 0)
cui.Clear("line")
}
}
func quickMessage(msg string, clearMsg bool) {
xPos := screen.Width - 2 - len(msg)
if xPos < 2 {
xPos = 2
}
cui.MoveCursorTo(screen.Height, xPos)
if clearMsg {
cui.Clear("right")
} else {
fmt.Print("\033[48;5;21m\033[38;5;15m", msg, "\033[0m")
}
}
// func saveFileFromData(v gopher.View) error {
// quickMessage("Saving file...", false)
// urlsplit := strings.Split(v.Address.Full, "/")
// filename := urlsplit[len(urlsplit)-1]
// saveMsg := fmt.Sprintf("Saved file as %q", options["savelocation"]+filename)
// err := ioutil.WriteFile(options["savelocation"]+filename, []byte(strings.Join(v.Content, "")), 0644)
// if err != nil {
// quickMessage("Saving file...", true)
// return err
// }
// quickMessage(saveMsg, false)
// return nil
// }
// func doLinkCommand(action, target string) error {
// num, err := strconv.Atoi(target)
// if err != nil {
// return fmt.Errorf("Expected number, got %q", target)
// }
// switch action {
// case "DELETE", "D":
// err := settings.Bookmarks.Del(num)
// if err != nil {
// return err
// }
// screen.Windows[1].Content = settings.Bookmarks.List()
// err = saveConfig()
// if err != nil {
// return err
// }
// screen.ReflashScreen(false)
// return nil
// case "BOOKMARKS", "B":
// if num > len(settings.Bookmarks.Links)-1 {
// return fmt.Errorf("There is no bookmark with ID %d", num)
// }
// err := goToURL(settings.Bookmarks.Links[num])
// return err
// }
// return fmt.Errorf("This method has not been built")
// }
// func doCommand(action string, values []string) error {
// if length := len(values); length != 1 {
// return fmt.Errorf("Expected 1 argument, received %d", length)
// }
// switch action {
// case "CHECK", "C":
// err := checkConfigValue(values[0])
// if err != nil {
// return err
// }
// return nil
// }
// return fmt.Errorf("Unknown command structure")
// }
// func doLinkCommandAs(action, target string, values []string) error {
// num, err := strconv.Atoi(target)
// if err != nil {
// return fmt.Errorf("Expected number, got %q", target)
// }
// links := history.Collection[history.Position].Links
// if num >= len(links) {
// return fmt.Errorf("Invalid link id: %s", target)
// }
// switch action {
// case "ADD", "A":
// newBookmark := append([]string{links[num-1]}, values...)
// err := settings.Bookmarks.Add(newBookmark)
// if err != nil {
// return err
// }
// screen.Windows[1].Content = settings.Bookmarks.List()
// err = saveConfig()
// if err != nil {
// return err
// }
// screen.ReflashScreen(false)
// return nil
// case "WRITE", "W":
// return saveFile(links[num-1], strings.Join(values, " "))
// }
// return fmt.Errorf("This method has not been built")
// }
// func updateMainContent() {
// screen.Windows[0].Content = history.Collection[history.Position].Content
// screen.Bars[0].SetMessage(history.Collection[history.Position].Address.Full)
// }
func saveConfig() error {
bkmrks := settings.Bookmarks.IniDump()
bkmrks := bombadillo.BookMarks.IniDump()
// TODO opts becomes a string builder rather than concat
opts := "\n[SETTINGS]\n"
for k, v := range options {
for k, v := range bombadillo.Options {
opts += k
opts += "="
opts += v
opts += "\n"
}
return ioutil.WriteFile(userinfo.HomeDir+"/.bombadillo.ini", []byte(bkmrks+opts), 0644)
return ioutil.WriteFile(bombadillo.Options["configlocation"] + "/.bombadillo.ini", []byte(bkmrks+opts), 0644)
}
func loadConfig() error {
file, err := os.Open(userinfo.HomeDir + "/.bombadillo.ini")
file, err := os.Open(bombadillo.Options["configlocation"] + "/.bombadillo.ini")
if err != nil {
err = saveConfig()
if err != nil {
@ -407,72 +148,29 @@ func loadConfig() error {
confparser := config.NewParser(file)
settings, _ = confparser.Parse()
file.Close()
screen.Windows[1].Content = settings.Bookmarks.List()
for _, v := range settings.Settings {
lowerkey := strings.ToLower(v.Key)
if _, ok := options[lowerkey]; ok {
options[lowerkey] = v.Value
if lowerkey == "configlocation" {
// The config should always be stored in home
// folder. Users cannot really edit this value.
// It is still stored in the ini and as a part
// of the options map.
continue
}
}
return nil
}
func toggleActiveWindow() {
if screen.Windows[1].Show {
if screen.Windows[0].Active {
screen.Windows[0].Active = false
screen.Windows[1].Active = true
screen.Activewindow = 1
} else {
screen.Windows[0].Active = true
screen.Windows[1].Active = false
screen.Activewindow = 0
if _, ok := bombadillo.Options[lowerkey]; ok {
bombadillo.Options[lowerkey] = v.Value
}
screen.Windows[1].DrawWindow()
}
}
func displayError(err error) {
cui.MoveCursorTo(screen.Height, 0)
fmt.Print("\033[41m\033[37m", err, "\033[0m")
return nil
}
func initClient() error {
history.Position = -1
screen = cui.NewScreen()
bombadillo = MakeClient(" ((( Bombadillo ))) ")
cui.SetCharMode()
screen.AddWindow(2, 1, screen.Height-2, screen.Width, false, false, true)
screen.Windows[0].Active = true
screen.AddMsgBar(1, " ((( Bombadillo ))) ", " A fun gopher client!", true)
bookmarksWidth := 40
if screen.Width < 40 {
bookmarksWidth = screen.Width
}
screen.AddWindow(2, screen.Width-bookmarksWidth, screen.Height-2, screen.Width, false, true, false)
return loadConfig()
}
func handleResize() {
oldh, oldw := screen.Height, screen.Width
screen.GetSize()
if screen.Height != oldh || screen.Width != oldw {
screen.Windows[0].Box.Row2 = screen.Height - 2
screen.Windows[0].Box.Col2 = screen.Width
bookmarksWidth := 40
if screen.Width < 40 {
bookmarksWidth = screen.Width
}
screen.Windows[1].Box.Row2 = screen.Height - 2
screen.Windows[1].Box.Col1 = screen.Width - bookmarksWidth
screen.Windows[1].Box.Col2 = screen.Width
screen.DrawAllWindows()
screen.DrawMsgBars()
screen.ClearCommandArea()
}
err := loadConfig()
return err
}
func main() {
@ -480,91 +178,27 @@ func main() {
defer cui.Exit()
err := initClient()
if err != nil {
// if we can't initialize the window,
// we can't do anything!
// if we can't initialize we should bail out
panic(err)
}
mainWindow := screen.Windows[0]
// Start polling for terminal size changes
go bombadillo.GetSize()
if len(os.Args) > 1 {
err = goToURL(os.Args[1])
// If a url was passed, move it down the line
// Goroutine so keypresses can be made during
// page load
go bombadillo.Visit(os.Args[1])
} else {
err = goHome()
}
if err != nil {
displayError(err)
} else {
updateMainContent()
// Otherwise, load the homeurl
// Goroutine so keypresses can be made during
// page load
go bombadillo.Visit(bombadillo.Options["homeurl"])
}
// Loop indefinitely on user input
for {
c := cui.Getch()
handleResize()
switch c {
case 'j', 'J':
screen.Windows[screen.Activewindow].ScrollDown()
screen.ReflashScreen(false)
case 'k', 'K':
screen.Windows[screen.Activewindow].ScrollUp()
screen.ReflashScreen(false)
case 'q', 'Q':
cui.Exit()
case 'g':
screen.Windows[screen.Activewindow].ScrollHome()
screen.ReflashScreen(false)
case 'G':
screen.Windows[screen.Activewindow].ScrollEnd()
screen.ReflashScreen(false)
case 'd':
screen.Windows[screen.Activewindow].PageDown()
screen.ReflashScreen(false)
case 'u':
screen.Windows[screen.Activewindow].PageUp()
screen.ReflashScreen(false)
case 'b':
success := history.GoBack()
if success {
mainWindow.Scrollposition = 0
updateMainContent()
screen.ReflashScreen(true)
}
case 'B':
toggleBookmarks()
case 'f', 'F':
success := history.GoForward()
if success {
mainWindow.Scrollposition = 0
updateMainContent()
screen.ReflashScreen(true)
}
case '\t':
toggleActiveWindow()
case ':', ' ':
cui.MoveCursorTo(screen.Height-1, 0)
entry, err := cui.GetLine()
if err != nil {
displayError(err)
}
// Clear entry line and error line
clearInput(true)
if entry == "" {
continue
}
parser := cmdparse.NewParser(strings.NewReader(entry))
p, err := parser.Parse()
if err != nil {
displayError(err)
} else {
err := routeInput(p)
if err != nil {
displayError(err)
}
}
}
bombadillo.TakeControlInput()
}
}

30
page.go

@ -0,0 +1,30 @@
package main
//------------------------------------------------\\
// + + + T Y P E S + + + \\
//--------------------------------------------------\\
type Page struct {
WrappedContent []string
RawContent string
Links []string
Location Url
ScrollPosition int
}
//------------------------------------------------\\
// + + + R E C E I V E R S + + + \\
//--------------------------------------------------\\
//------------------------------------------------\\
// + + + F U N C T I O N S + + + \\
//--------------------------------------------------\\
func MakePage(url Url, content string) Page {
p := Page{make([]string, 0), content, make([]string, 0), url, 0}
return p
}

54
pages.go

@ -0,0 +1,54 @@
package main
import (
"fmt"
)
//------------------------------------------------\\
// + + + T Y P E S + + + \\
//--------------------------------------------------\\
type Pages struct {
Position int
Length int
History [20]Page
}
//------------------------------------------------\\
// + + + R E C E I V E R S + + + \\
//--------------------------------------------------\\
func (p *Pages) NavigateHistory(qty int) error {
newPosition := p.Position + qty
if newPosition < 0 {
return fmt.Errorf("You are already at the beginning of history")
} else if newPosition > p.Length - 1 {
return fmt.Errorf("Your way is blocked by void, there is nothing forward")
}
p.Position = newPosition
return nil
}
func (p *Pages) Add(pg Page) error {
// TODO add the given page onto the pages struct
// handling truncation of the history as needed.
return fmt.Errorf("")
}
func (p *Pages) Render() ([]string, error) {
// TODO grab the current page as wrappedContent
// May need to handle spacing at end of lines.
return []string{}, fmt.Errorf("")
}
//------------------------------------------------\\
// + + + F U N C T I O N S + + + \\
//--------------------------------------------------\\
func MakePages() Pages {
return Pages{-1, 0, [20]Page{}}
}

107
url.go

@ -0,0 +1,107 @@
package main
import (
"fmt"
"regexp"
"strings"
)
//------------------------------------------------\\
// + + + T Y P E S + + + \\
//--------------------------------------------------\\
type Url struct {
Scheme string
Host string
Port string
Resource string
Full string
Mime string
DownloadOnly bool
}
//------------------------------------------------\\
// + + + R E C E I V E R S + + + \\
//--------------------------------------------------\\
// There are currently no receivers for the Url struct
//------------------------------------------------\\
// + + + F U N C T I O N S + + + \\
//--------------------------------------------------\\
// MakeUrl is a Url constructor that takes in a string
// representation of a url and returns a Url struct and
// an error (or nil).
func MakeUrl(u string) (Url, error) {
var out Url
re := regexp.MustCompile(`^((?P<scheme>gopher|http|https|gemini):\/\/)?(?P<host>[\w\-\.\d]+)(?::(?P<port>\d+)?)?(?:/(?P<type>[01345679gIhisp])?)?(?P<resource>.*)?$`)
match := re.FindStringSubmatch(u)
if valid := re.MatchString(u); !valid {
return out, fmt.Errorf("Invalid url/unable to parse")
}
for i, name := range re.SubexpNames() {
switch name {
case "scheme":
out.Scheme = match[i]
case "host":
out.Host = match[i]
case "port":
out.Port = match[i]
case "type":
out.Mime = match[i]
case "resource":
out.Resource = match[i]
}
}
if out.Scheme == "" {
out.Scheme = "gopher"
}
if out.Host == "" {
return out, fmt.Errorf("no host")
}
if out.Scheme == "gopher" && out.Port == "" {
out.Port = "70"
} else if out.Scheme == "http" && out.Port == "" {
out.