From da45f627e09079eadbf7f14d3539380c7c5a8458 Mon Sep 17 00:00:00 2001 From: sloumdrone Date: Mon, 9 Sep 2019 19:35:16 -0700 Subject: [PATCH] Initial v2 commit, deep in restructuring... maybe not for the better? --- bookmarks.go | 68 ++++ client.go | 541 +++++++++++++++++++++++++++++ footbar.go | 54 +++ gopher/gopher.go | 2 +- gopher/open_browser_darwin.go | 2 +- gopher/open_browser_linux.go | 2 +- gopher/open_browser_other.go | 2 +- gopher/open_browser_windows.go | 2 +- headbar.go | 47 +++ main.go | 606 +++++++-------------------------- page.go | 30 ++ pages.go | 54 +++ url.go | 107 ++++++ 13 files changed, 1026 insertions(+), 491 deletions(-) create mode 100644 bookmarks.go create mode 100644 client.go create mode 100644 footbar.go create mode 100644 headbar.go create mode 100644 page.go create mode 100644 pages.go create mode 100644 url.go diff --git a/bookmarks.go b/bookmarks.go new file mode 100644 index 0000000..3b044fd --- /dev/null +++ b/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)} +} + diff --git a/client.go b/client.go new file mode 100644 index 0000000..8c2bdc0 --- /dev/null +++ b/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 +} diff --git a/footbar.go b/footbar.go new file mode 100644 index 0000000..be1ea60 --- /dev/null +++ b/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", ""} +} + diff --git a/gopher/gopher.go b/gopher/gopher.go index 82bedea..5f488fe 100644 --- a/gopher/gopher.go +++ b/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 } diff --git a/gopher/open_browser_darwin.go b/gopher/open_browser_darwin.go index edafe36..33db791 100644 --- a/gopher/open_browser_darwin.go +++ b/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() } diff --git a/gopher/open_browser_linux.go b/gopher/open_browser_linux.go index 2ce35c9..bea56c3 100644 --- a/gopher/open_browser_linux.go +++ b/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() } diff --git a/gopher/open_browser_other.go b/gopher/open_browser_other.go index 1659ea3..f452ed4 100644 --- a/gopher/open_browser_other.go +++ b/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") } diff --git a/gopher/open_browser_windows.go b/gopher/open_browser_windows.go index b57c9d6..2912217 100644 --- a/gopher/open_browser_windows.go +++ b/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() } diff --git a/headbar.go b/headbar.go new file mode 100644 index 0000000..8aafc55 --- /dev/null +++ b/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} +} + diff --git a/main.go b/main.go index 015fe78..ff354bb 100644 --- a/main.go +++ b/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 - } +// 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 + // } - data, err := gopher.Retrieve(url) - if err != nil { - quickMessage("Saving file...", true) - return err - } + // quickMessage(saveMsg, false) + // return nil +// } - 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 - } +// func doLinkCommand(action, target string) error { + // num, err := strconv.Atoi(target) + // if err != nil { + // return fmt.Errorf("Expected number, got %q", target) + // } - quickMessage(saveMsg, false) - return nil -} + // switch action { + // case "DELETE", "D": + // err := settings.Bookmarks.Del(num) + // if err != nil { + // return err + // } -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) + // screen.Windows[1].Content = settings.Bookmarks.List() + // err = saveConfig() + // if err != nil { + // return err + // } - entry, err := cui.GetLine() - 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 + // } - 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 -} + // return fmt.Errorf("This method has not been built") +// } -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 doCommand(action string, values []string) error { + // if length := len(values); length != 1 { + // return fmt.Errorf("Expected 1 argument, received %d", length) + // } -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 - } + // switch action { + // case "CHECK", "C": + // err := checkConfigValue(values[0]) + // if err != nil { + // return err + // } + // return nil + // } + // return fmt.Errorf("Unknown command structure") +// } - screen.ReflashScreen(false) -} +// func doLinkCommandAs(action, target string, values []string) error { + // num, err := strconv.Atoi(target) + // if err != nil { + // return fmt.Errorf("Expected number, got %q", target) + // } -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) + // links := history.Collection[history.Position].Links + // if num >= len(links) { + // return fmt.Errorf("Invalid link id: %s", target) + // } - default: - return fmt.Errorf("Unknown action %q", a) - } - return nil -} + // switch action { + // case "ADD", "A": + // newBookmark := append([]string{links[num-1]}, values...) + // err := settings.Bookmarks.Add(newBookmark) + // if err != nil { + // return err + // } -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) + // screen.Windows[1].Content = settings.Bookmarks.List() - 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 -} + // err = saveConfig() + // if err != nil { + // return err + // } -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) + // screen.ReflashScreen(false) + // return nil + // case "WRITE", "W": + // return saveFile(links[num-1], strings.Join(values, " ")) + // } - 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 -} + // return fmt.Errorf("This method has not been built") +// } -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 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 + } + + if _, ok := bombadillo.Options[lowerkey]; ok { + bombadillo.Options[lowerkey] = v.Value } } 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 - } - screen.Windows[1].DrawWindow() - } -} - -func displayError(err error) { - cui.MoveCursorTo(screen.Height, 0) - fmt.Print("\033[41m\033[37m", err, "\033[0m") -} - 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() } } diff --git a/page.go b/page.go new file mode 100644 index 0000000..2e4b85e --- /dev/null +++ b/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 +} + diff --git a/pages.go b/pages.go new file mode 100644 index 0000000..5c844e8 --- /dev/null +++ b/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{}} +} + + diff --git a/url.go b/url.go new file mode 100644 index 0000000..b6dfebd --- /dev/null +++ b/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(`^((?Pgopher|http|https|gemini):\/\/)?(?P[\w\-\.\d]+)(?::(?P\d+)?)?(?:/(?P[01345679gIhisp])?)?(?P.*)?$`) + 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.Port = "80" + } else if out.Scheme == "https" && out.Port == "" { + out.Port = "443" + } else if out.Scheme == "gemini" && out.Port == "" { + out.Port = "1965" + } + + if out.Scheme == "gopher" && out.Mime == "" { + out.Mime = "0" + } + + if out.Mime == "" && (out.Resource == "" || out.Resource == "/") && out.Scheme == "gopher" { + out.Mime = "1" + } + + if out.Mime == "7" && strings.Contains(out.Resource, "\t") { + out.Mime = "1" + } + + if out.Scheme == "gopher" { + switch out.Mime { + case "1", "0", "h", "7": + out.DownloadOnly = false + default: + out.DownloadOnly = true + } + } else { + out.Resource = fmt.Sprintf("%s%s", out.Mime, out.Resource) + out.Mime = "" + } + + out.Full = out.Scheme + "://" + out.Host + ":" + out.Port + "/" + out.Mime + out.Resource + + return out, nil +}