diff options
Diffstat (limited to 'actions.go')
| -rw-r--r-- | actions.go | 342 |
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 +} |
