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 }