summaryrefslogtreecommitdiff
path: root/actions.go
diff options
context:
space:
mode:
authortjp <tjp@ctrl-c.club>2024-01-03 08:29:40 -0700
committertjp <tjp@ctrl-c.club>2024-01-03 08:29:40 -0700
commit6c9558c0d2201d933b1d396febeb6e70ceaad058 (patch)
tree565b8b048fc3e63b6c2eb6892a6f356f0c3c15aa /actions.go
parent4b3dd896fb0c157e0d727f39ccb6d940a75d1cce (diff)
working basic navigation and marks
Diffstat (limited to 'actions.go')
-rw-r--r--actions.go342
1 files changed, 342 insertions, 0 deletions
diff --git a/actions.go b/actions.go
new file mode 100644
index 0000000..8ea7ce8
--- /dev/null
+++ b/actions.go
@@ -0,0 +1,342 @@
+package main
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "net/url"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "syscall"
+
+ "tildegit.org/tjp/sliderule"
+)
+
+var client sliderule.Client
+
+func init() {
+ client = sliderule.NewClient(nil)
+}
+
+var (
+ ErrMustBeOnAPage = errors.New("you must be on a page to do that, use the \"go\" command first")
+ ErrNoPreviousHistory = errors.New("there is no previous page in the history")
+ ErrNoNextHistory = errors.New("there is no page to go forward to")
+ ErrOnLastLink = errors.New("already on the last link")
+ ErrOnFirstLink = errors.New("already on the first link")
+ ErrCantMoveRelative = errors.New("next/previous only work after navigating to a link on a page")
+ ErrAlreadyAtTop = errors.New("already at the site root")
+ ErrInvalidNumericLink = errors.New("no link with that number")
+ ErrInvalidLink = errors.New("that doesn't look like a valid URL")
+ ErrSaveNeedsFilename = errors.New("save requires a filename argument")
+ ErrUnrecognizedMark = errors.New("mark what?")
+)
+
+func Navigate(state *BrowserState, target *url.URL, navIndex int, conf *Config) error {
+ hist := state.History
+
+ if hist.Url == nil || target.String() != hist.Url.String() {
+ state.History = &History{
+ Url: target,
+ Depth: hist.Depth + 1,
+ Back: hist,
+ NavIndex: navIndex,
+ }
+ hist.Forward = state.History
+ }
+ state.Modal = nil
+
+ return Reload(state, conf)
+}
+
+func gopherURL(u *url.URL) (string, sliderule.Status) {
+ if u.Scheme != "gopher" || len(u.Path) < 2 || !strings.HasPrefix(u.Path, "/") {
+ return u.String(), 0
+ }
+ itemType := u.Path[1]
+ clone := *u
+ clone.Path = u.Path[2:]
+ return clone.String(), sliderule.Status(itemType)
+}
+
+func Reload(state *BrowserState, conf *Config) error {
+ if state.Url == nil {
+ return ErrMustBeOnAPage
+ }
+
+ urlStr, _ := gopherURL(state.Url)
+ response, err := client.Fetch(urlStr)
+ if err != nil {
+ return err
+ }
+
+ state.DocType = docType(state.Url, response)
+ state.Body, err = io.ReadAll(response.Body)
+ if err != nil {
+ return err
+ }
+
+ state.Formatted, state.Links, err = parseDoc(state.DocType, state.Body, conf)
+ if err != nil {
+ return err
+ }
+
+ return Print(state)
+}
+
+func back(state *BrowserState) error {
+ if state.Back == nil {
+ return ErrNoPreviousHistory
+ }
+ state.History = state.Back
+ state.Modal = nil
+ return nil
+}
+
+func Back(state *BrowserState) error {
+ if err := back(state); err != nil {
+ return err
+ }
+
+ _, _ = fmt.Fprintf(os.Stdout, "Back: %s\n", state.Url.String())
+ return Print(state)
+}
+
+func Forward(state *BrowserState) error {
+ if state.Forward == nil {
+ return ErrNoNextHistory
+ }
+ state.History = state.Forward
+ state.Modal = nil
+
+ _, _ = fmt.Fprintf(os.Stdout, "Forward: %s\n", state.Url.String())
+
+ return Print(state)
+}
+
+func Next(state *BrowserState, conf *Config) error {
+ switch state.NavIndex {
+ case -1:
+ return ErrCantMoveRelative
+ case len(state.Back.Links) - 1:
+ return ErrOnLastLink
+ }
+
+ index := state.NavIndex + 1
+
+ if err := back(state); err != nil {
+ return err
+ }
+
+ u := state.Url.ResolveReference(state.Links[index].Target)
+
+ _, _ = fmt.Fprintf(os.Stdout, "Next: %s\n", u.String())
+
+ return Navigate(state, u, index, conf)
+}
+
+func Previous(state *BrowserState, conf *Config) error {
+ switch state.NavIndex {
+ case -1:
+ return ErrCantMoveRelative
+ case 0:
+ return ErrOnFirstLink
+ }
+
+ index := state.NavIndex - 1
+
+ if err := back(state); err != nil {
+ return err
+ }
+
+ u := state.Url.ResolveReference(state.Links[index].Target)
+
+ _, _ = fmt.Fprintf(os.Stdout, "Previous: %s\n", u.String())
+
+ return Navigate(state, u, index, conf)
+}
+
+func Root(state *BrowserState, tilde bool, conf *Config) error {
+ if state.Url == nil {
+ return ErrMustBeOnAPage
+ }
+
+ u := *state.Url
+ u.RawQuery = ""
+ u.Fragment = ""
+
+ base := "/"
+ if u.Scheme == "gopher" {
+ base = "/1/"
+ }
+
+ if tilde && strings.HasPrefix(u.Path, "/~") {
+ u.Path = base + strings.SplitN(u.Path, "/", 3)[1] + "/"
+ } else {
+ u.Path = base
+ }
+
+ _, _ = fmt.Fprintf(os.Stdout, "Root: %s\n", u.String())
+
+ return Navigate(state, &u, -1, conf)
+}
+
+func Up(state *BrowserState, conf *Config) error {
+ if state.Url == nil {
+ return ErrMustBeOnAPage
+ }
+
+ u := *state.Url
+ u.Path = strings.TrimSuffix(u.Path, "/")
+ if u.Path == "" {
+ return ErrAlreadyAtTop
+ }
+
+ u.Path = u.Path[:strings.LastIndex(u.Path, "/")+1]
+
+ _, _ = fmt.Fprintf(os.Stdout, "Up: %s\n", u.String())
+
+ return Navigate(state, &u, -1, conf)
+}
+
+func Go(state *BrowserState, dest string, conf *Config) error {
+ u, idx, err := parseURL(dest, state, conf.DefaultScheme)
+ if err != nil {
+ return err
+ }
+
+ _, _ = fmt.Fprintf(os.Stdout, "Go: %s\n", u.String())
+
+ return Navigate(state, u, idx, conf)
+}
+
+func parseURL(str string, state *BrowserState, defaultScheme string) (*url.URL, int, error) {
+ if str == "." {
+ return state.Url, -1, nil
+ }
+
+ if strings.HasPrefix(str, "m:") {
+ target, err := findMark(state, str[2:])
+ if err != nil {
+ return nil, -1, err
+ }
+ str = target
+ }
+
+ var u *url.URL
+ i, err := strconv.Atoi(str)
+ if err == nil {
+ if len(state.Links) <= i {
+ return nil, -1, ErrInvalidNumericLink
+ }
+ u = state.Links[i].Target
+ } else {
+ i = -1
+ u, err = url.Parse(str)
+ if err != nil {
+ return nil, -1, ErrInvalidLink
+ }
+ if u.Scheme == "" {
+ u.Scheme = defaultScheme
+ }
+ }
+ if state.Url != nil {
+ u = state.Url.ResolveReference(u)
+ }
+ return u, i, nil
+}
+
+func Print(state *BrowserState) error {
+ if state.Body == nil && state.Modal == nil {
+ return ErrMustBeOnAPage
+ }
+ out := []byte(state.Formatted)
+ if state.Modal != nil {
+ out = state.Modal
+ }
+ _, err := os.Stdout.Write(out)
+ return err
+}
+
+func Links(state *BrowserState, conf *Config) error {
+ if state.Links == nil {
+ return ErrMustBeOnAPage
+ }
+
+ buf := &bytes.Buffer{}
+ for _, link := range state.Links {
+ fmt.Fprintf(buf, "=> %s %s\n", link.Target.String(), link.Text)
+ }
+ formatted, _, err := parseDoc("text/gemini", buf.Bytes(), conf)
+ if err != nil {
+ return err
+ }
+ state.Modal = []byte(formatted)
+
+ return Print(state)
+}
+
+func HistoryCmd(state *BrowserState) error {
+ if state.History == nil {
+ return ErrMustBeOnAPage
+ }
+
+ h := state.History
+ i := 0
+ for h.Forward != nil {
+ h = h.Forward
+ i += 1
+ }
+
+ buf := &bytes.Buffer{}
+ j := 0
+ for h.Url != nil {
+ mark := ""
+ if j == i {
+ mark = "* "
+ }
+ fmt.Fprintf(buf, "%s%s\n", mark, h.Url.String())
+ j += 1
+ h = h.Back
+ }
+ state.Modal = buf.Bytes()
+
+ return Print(state)
+}
+
+func Save(state *BrowserState, filename string, conf *Config) error {
+ if state.Body == nil {
+ return ErrMustBeOnAPage
+ }
+ if filename == "" {
+ return ErrSaveNeedsFilename
+ }
+
+ p := filepath.Join(conf.DownloadFolder, filename)
+ _, err := os.Stat(p)
+ pbase := p
+ i := 1
+ for !errors.Is(err, syscall.ENOENT) {
+ p = pbase + "-" + strconv.Itoa(i)
+ _, err = os.Stat(p)
+ i += 1
+ }
+
+ return os.WriteFile(p, state.Body, 0o644)
+}
+
+func Mark(state *BrowserState, args []string, conf *Config) error {
+ switch args[0] {
+ case "add":
+ return MarkAdd(state, conf, args[1], args[2])
+ case "go":
+ return MarkGo(state, conf, args[1])
+ case "list":
+ return MarkList(state)
+ }
+
+ return ErrUnrecognizedMark
+}