458 lines
10 KiB
Go
458 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"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")
|
|
ErrInvalidMarkArgs = errors.New("mark what?")
|
|
ErrInvalidTourArgs = errors.New("tour what?")
|
|
)
|
|
|
|
func About(state *BrowserState) error {
|
|
_, err := fmt.Println(`
|
|
...
|
|
*=:::.
|
|
---======- -+-.....
|
|
.#@@@@@@@= .+=-:....:++
|
|
+@@@@@@@@# :-::.. ...*+
|
|
:%@@@@@@@@@. =*+++==:....
|
|
.*@@@@@@@@@@= -#%###**++-:..
|
|
-@@@@@@@@@@@% .::-=*#@@#*#@@@@@@@%-.
|
|
..:::----=====++#@@@@@@@@@@@@%%%@@@@@@@@@@%%#**@@@@@@@=
|
|
.-+#%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@-
|
|
=*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@##%@@*%@@@@@@@@@@- ..
|
|
-+*. .#@@@@@@@@@@@@@@@@@@@@@@@@@@@%@@@@@@%%%%###- =- .=.==----==--:...
|
|
...:::::=#@@@@#**+*##@@@#*%%%%%%%%##############******+==-:-=-:.-==-:.. ...
|
|
.:.:-==++++====::-----::---:-+%@@@@@@@@@@@@@#-...
|
|
. ...............=@@@@@@@@@@@@@@+....
|
|
-@@@@@@@@@@@@@#.
|
|
-@@@@@@@@@@@@%-
|
|
-@@%%##@@@@@@=
|
|
=%++*=-=%@@@*.
|
|
=--*%%%::#@#:
|
|
*@+=#-+*%@%-
|
|
.*%****###@+.
|
|
::..:::.::*@@@%%%%%#:
|
|
|
|
x-1 is a browser for small web protocols gemini, gopher, finger, spartan, and nex.
|
|
It also has very limited support for HTTP[S].
|
|
|
|
It was written by TJP and released to the public domain.
|
|
`[1:])
|
|
return err
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
return print(state)
|
|
}
|
|
|
|
func Forward(state *BrowserState) error {
|
|
if state.Forward == nil {
|
|
return ErrNoNextHistory
|
|
}
|
|
state.History = state.Forward
|
|
state.Modal = nil
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
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
|
|
}
|
|
|
|
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]
|
|
|
|
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
|
|
}
|
|
|
|
return Navigate(state, u, idx, conf)
|
|
}
|
|
|
|
func parseURL(str string, state *BrowserState, defaultScheme string) (*url.URL, int, error) {
|
|
if str == "." {
|
|
if state.Url == nil {
|
|
return nil, -1, ErrMustBeOnAPage
|
|
}
|
|
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
|
|
} else if strings.HasPrefix(str, "t:") {
|
|
i, err := strconv.Atoi(str[2:])
|
|
if err != nil {
|
|
return nil, -1, ErrInvalidLink
|
|
}
|
|
|
|
if i < 0 || i >= len(state.CurrentTour.Links) {
|
|
return nil, -1, ErrInvalidTourPos
|
|
}
|
|
return state.CurrentTour.Links[i], -1, nil
|
|
} else if strings.HasPrefix(str, "t[") {
|
|
idx := strings.IndexByte(str, ']')
|
|
if idx < 0 || idx >= len(str)-2 || str[idx+1] != ':' {
|
|
return nil, -1, ErrInvalidLink
|
|
}
|
|
if i, err := strconv.Atoi(str[idx+2:]); err != nil {
|
|
return nil, -1, ErrInvalidLink
|
|
} else {
|
|
tour, err := findTour(state, str[2:idx])
|
|
if err != nil {
|
|
return nil, -1, err
|
|
}
|
|
|
|
if i < 0 || i >= len(tour.Links) {
|
|
return nil, -1, ErrInvalidTourPos
|
|
}
|
|
return tour.Links[i], -1, nil
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
if u.Hostname() == "" {
|
|
return nil, -1, ErrInvalidLink
|
|
}
|
|
|
|
return u, i, nil
|
|
}
|
|
|
|
func print(state *BrowserState) error {
|
|
if state.Quiet {
|
|
return nil
|
|
}
|
|
|
|
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 Print(state *BrowserState) error {
|
|
q := state.Quiet
|
|
defer func() { state.Quiet = q }()
|
|
state.Quiet = false
|
|
return print(state)
|
|
}
|
|
|
|
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 ErrInvalidMarkArgs
|
|
}
|
|
|
|
func TourCmd(state *BrowserState, args []string, conf *Config) error {
|
|
switch args[0] {
|
|
case "add":
|
|
if args[1] == "next" {
|
|
return TourAddNext(state, conf, args[2:])
|
|
}
|
|
return TourAdd(state, conf, args[1:])
|
|
case "show":
|
|
return TourShow(state)
|
|
case "select":
|
|
return TourSelect(state, args[1])
|
|
case "next":
|
|
return TourNext(state, conf)
|
|
case "previous":
|
|
return TourPrevious(state, conf)
|
|
case "clear":
|
|
return TourClear(state)
|
|
case "list":
|
|
return TourList(state)
|
|
case "go":
|
|
return TourGo(state, conf, args[1])
|
|
}
|
|
|
|
return ErrInvalidTourArgs
|
|
}
|
|
|
|
func Pipe(state *BrowserState, cmdStr string) error {
|
|
if state.Body == nil {
|
|
return ErrMustBeOnAPage
|
|
}
|
|
|
|
sh, err := exec.LookPath("sh")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd := exec.Command(sh, "-c", cmdStr)
|
|
cmd.Stdin = bytes.NewBuffer(state.Body)
|
|
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
return cmd.Run()
|
|
}
|