794 lines
18 KiB
Go
794 lines
18 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"tildegit.org/tjp/sliderule"
|
|
"tildegit.org/tjp/sliderule/gemini"
|
|
"tildegit.org/tjp/sliderule/gemini/gemtext"
|
|
"tildegit.org/tjp/sliderule/gopher"
|
|
)
|
|
|
|
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")
|
|
ErrSaveNeedsFilename = errors.New("save requires a filename argument")
|
|
ErrInvalidMarkArgs = errors.New("mark what?")
|
|
ErrInvalidTourArgs = errors.New("tour what?")
|
|
ErrOnlyTextGemini = errors.New("that is only supported for text/gemini pages")
|
|
)
|
|
|
|
func ErrInvalidLink(invalidURL string) error {
|
|
return invalidLinkErr(invalidURL)
|
|
}
|
|
|
|
type invalidLinkErr string
|
|
|
|
func (ie invalidLinkErr) Error() string {
|
|
return fmt.Sprintf("that doesn't look like a valid URL: %s", string(ie))
|
|
}
|
|
|
|
func About(_ *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) error {
|
|
if state.Url == nil || target.String() != state.Url.String() {
|
|
pushHistory(state, target, navIndex)
|
|
}
|
|
state.Modal = nil
|
|
|
|
return Reload(state)
|
|
}
|
|
|
|
func pushHistory(state *BrowserState, target *url.URL, navIndex int) {
|
|
hist := state.History
|
|
state.History = &History{
|
|
Url: target,
|
|
Depth: hist.Depth + 1,
|
|
Back: hist,
|
|
NavIndex: navIndex,
|
|
}
|
|
hist.Forward = state.History
|
|
|
|
purgeOldHistory(state)
|
|
}
|
|
|
|
func purgeOldHistory(state *BrowserState) {
|
|
if state.Depth <= state.SavedHistoryDepth {
|
|
return
|
|
}
|
|
|
|
d := state.SavedHistoryDepth
|
|
h := state.History
|
|
for d > 0 {
|
|
h = h.Back
|
|
d -= 1
|
|
}
|
|
|
|
h.Body = nil
|
|
h.Formatted = ""
|
|
h.Links = nil
|
|
}
|
|
|
|
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) error {
|
|
if state.Url == nil {
|
|
return ErrMustBeOnAPage
|
|
}
|
|
|
|
urlStr, itemType := gopherURL(state.Url)
|
|
if itemType == gopher.SearchType && state.Url.RawQuery == "" {
|
|
state.Readline.SetPrompt("query: ")
|
|
line, err := state.Readline.Readline()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
state.Url.RawQuery = url.QueryEscape(strings.TrimRight(line, "\n"))
|
|
urlStr, _ = gopherURL(state.Url)
|
|
}
|
|
|
|
tlsConf := tlsConfig(state)
|
|
|
|
var response *sliderule.Response
|
|
var err error
|
|
if state.Url.Scheme == "spartan" && state.Url.Fragment == "prompt" {
|
|
input, err := externalMessage()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body := io.LimitReader(bytes.NewBuffer(input), int64(len(input)))
|
|
|
|
state.Url.Fragment = ""
|
|
response, err = upload(state, state.Url.String(), body, tlsConf)
|
|
state.Url.Fragment = "prompt"
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
response, err = fetch(state, urlStr, tlsConf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if state.Url.Scheme == "gemini" {
|
|
outer:
|
|
for {
|
|
switch response.Status {
|
|
case gemini.StatusInput:
|
|
state.Readline.SetPrompt(response.Meta.(string) + " ")
|
|
line, err := state.Readline.Readline()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
state.Url = response.Request.URL
|
|
state.Url.RawQuery = url.QueryEscape(strings.TrimRight(line, "\n"))
|
|
response, err = fetch(state, state.Url.String(), tlsConf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case gemini.StatusSensitiveInput:
|
|
line, err := state.Readline.ReadPassword(response.Meta.(string) + " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
state.Url = response.Request.URL
|
|
state.Url.RawQuery = url.QueryEscape(strings.TrimRight(string(line), "\n"))
|
|
response, err = fetch(state, state.Url.String(), tlsConf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case gemini.StatusSuccess:
|
|
break outer
|
|
default:
|
|
return fmt.Errorf("gemini response %s: %s", gemini.StatusName(response.Status), response.Meta.(string))
|
|
}
|
|
}
|
|
}
|
|
|
|
state.DocType = docType(state.Url, response)
|
|
state.Url = returnedURL(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, state.Config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return HandleResource(state)
|
|
}
|
|
|
|
func returnedURL(requested *url.URL, response *sliderule.Response) *url.URL {
|
|
_, gopherType := gopherURL(requested)
|
|
if gopherType == 0 {
|
|
return response.Request.URL
|
|
}
|
|
|
|
u := *response.Request.URL
|
|
u.Path = "/" + string([]byte{byte(gopherType)}) + u.Path
|
|
return &u
|
|
}
|
|
|
|
func requestCtx(timeout time.Duration) (context.Context, context.CancelFunc) {
|
|
ctx := context.Background()
|
|
if timeout > 0 {
|
|
return context.WithTimeout(ctx, timeout)
|
|
}
|
|
return ctx, func() {}
|
|
}
|
|
|
|
func fetch(state *BrowserState, u string, tlsConf *tls.Config) (*sliderule.Response, error) {
|
|
ctx, cancel := requestCtx(state.Timeout.Duration)
|
|
defer cancel()
|
|
|
|
tlsConf.ClientSessionCache = nil
|
|
response, err := sliderule.NewClient(tlsConf).Fetch(ctx, u)
|
|
var tofuErr *TOFUViolation
|
|
|
|
if errors.As(err, &tofuErr) {
|
|
state.Printer.PrintError(err.Error())
|
|
state.Readline.SetPrompt("Trust new certificate instead (y/n)? [n] ")
|
|
line, err := state.Readline.Readline()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if line != "y" {
|
|
return nil, tofuErr
|
|
}
|
|
|
|
tofuStore[tofuErr.domain] = tofuErr.got
|
|
if err := saveTofuStore(tofuStore); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ctx, cancel = requestCtx(state.Timeout.Duration)
|
|
defer cancel()
|
|
return sliderule.NewClient(tlsConf).Fetch(ctx, u)
|
|
} else if err != nil {
|
|
return nil, err
|
|
}
|
|
return response, nil
|
|
}
|
|
|
|
func upload(state *BrowserState, u string, body io.Reader, tlsConf *tls.Config) (*sliderule.Response, error) {
|
|
ctx, cancel := requestCtx(state.Timeout.Duration)
|
|
defer cancel()
|
|
|
|
tlsConf.ClientSessionCache = nil
|
|
response, err := sliderule.NewClient(tlsConf).Upload(ctx, u, body)
|
|
var tofuErr *TOFUViolation
|
|
if errors.As(err, &tofuErr) {
|
|
state.Printer.PrintError(err.Error())
|
|
state.Readline.SetPrompt("Trust new certificate instead (y/n)? [n] ")
|
|
line, err := state.Readline.Readline()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if line != "y" {
|
|
return nil, tofuErr
|
|
}
|
|
|
|
tofuStore[tofuErr.domain] = tofuErr.got
|
|
if err := saveTofuStore(tofuStore); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ctx, cancel = requestCtx(state.Timeout.Duration)
|
|
defer cancel()
|
|
return sliderule.NewClient(tlsConf).Upload(ctx, u, body)
|
|
} else if err != nil {
|
|
return nil, err
|
|
}
|
|
return response, nil
|
|
}
|
|
|
|
func externalMessage() ([]byte, error) {
|
|
tmpf, err := os.CreateTemp("", "*")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = os.Remove(tmpf.Name()) }()
|
|
|
|
prompt := []byte("# enter input below (this line will be ignored)\n")
|
|
|
|
err = (func() error {
|
|
defer func() { _ = tmpf.Close() }()
|
|
|
|
if _, err := tmpf.Write(prompt); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
editor := os.Getenv("EDITOR")
|
|
if editor == "" {
|
|
editor = "vi"
|
|
}
|
|
editor, err = exec.LookPath(editor)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cmd := exec.Command(editor, tmpf.Name())
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
if err := cmd.Run(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tmpf, err = os.Open(tmpf.Name())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = tmpf.Close() }()
|
|
|
|
buf, err := io.ReadAll(tmpf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return bytes.TrimPrefix(buf, prompt), nil
|
|
}
|
|
|
|
func back(state *BrowserState) error {
|
|
if state.Back == nil {
|
|
return ErrNoPreviousHistory
|
|
}
|
|
state.Modal = nil
|
|
state.History = state.Back
|
|
|
|
if state.Body == nil {
|
|
return Reload(state)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func Back(state *BrowserState, num int) error {
|
|
for i := 0; i < num; i += 1 {
|
|
if err := back(state); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return HandleResource(state)
|
|
}
|
|
|
|
func Forward(state *BrowserState, num int) error {
|
|
for i := 0; i < num; i += 1 {
|
|
if state.Forward == nil {
|
|
return ErrNoNextHistory
|
|
}
|
|
state.History = state.Forward
|
|
}
|
|
state.Modal = nil
|
|
|
|
return HandleResource(state)
|
|
}
|
|
|
|
func Next(state *BrowserState) 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)
|
|
}
|
|
|
|
func Previous(state *BrowserState) 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)
|
|
}
|
|
|
|
func Root(state *BrowserState, tilde bool) 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)
|
|
}
|
|
|
|
func Up(state *BrowserState) 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]
|
|
u.RawQuery = ""
|
|
u.Fragment = ""
|
|
|
|
return Navigate(state, &u, -1)
|
|
}
|
|
|
|
func Go(state *BrowserState, dest string) error {
|
|
u, idx, err := parseURL(dest, state, state.DefaultScheme)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return Navigate(state, u, idx)
|
|
}
|
|
|
|
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(str)
|
|
}
|
|
|
|
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(str)
|
|
}
|
|
if i, err := strconv.Atoi(str[idx+2:]); err != nil {
|
|
return nil, -1, ErrInvalidLink(str)
|
|
} 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
|
|
if state.Links[i].Prompt {
|
|
u.Fragment = "prompt"
|
|
}
|
|
} else {
|
|
i = -1
|
|
u, err = url.Parse(str)
|
|
if err != nil {
|
|
return nil, -1, ErrInvalidLink(str)
|
|
}
|
|
if u.Scheme == "" {
|
|
u.Scheme = defaultScheme
|
|
}
|
|
}
|
|
if state.Url != nil {
|
|
u = state.Url.ResolveReference(u)
|
|
}
|
|
|
|
if u.Hostname() == "" {
|
|
return nil, -1, ErrInvalidLink(u.String())
|
|
}
|
|
|
|
return u, i, nil
|
|
}
|
|
|
|
func print(state *BrowserState) error {
|
|
if state.Modal != nil {
|
|
defer func() { state.Modal = nil }()
|
|
return state.Printer.PrintModal(state, state.Modal)
|
|
}
|
|
|
|
if state.Body == nil {
|
|
return ErrMustBeOnAPage
|
|
}
|
|
|
|
if state.Quiet {
|
|
return nil
|
|
}
|
|
|
|
return state.Printer.PrintPage(state, state.Formatted)
|
|
}
|
|
|
|
func Print(state *BrowserState) error {
|
|
q := state.Quiet
|
|
defer func() { state.Quiet = q }()
|
|
state.Quiet = false
|
|
return print(state)
|
|
}
|
|
|
|
func HandleResource(state *BrowserState) error {
|
|
if state.Modal != nil {
|
|
return Print(state)
|
|
}
|
|
|
|
if handler, ok := state.Handlers[state.DocType]; ok {
|
|
return Pipe(state, handler)
|
|
}
|
|
|
|
switch state.DocType {
|
|
case "text/gemini", "text/x-gophermap", "text/plain":
|
|
return print(state)
|
|
}
|
|
|
|
return Save(state, path.Base(state.Url.Path))
|
|
}
|
|
|
|
func Outline(state *BrowserState) error {
|
|
if state.Body == nil {
|
|
return ErrMustBeOnAPage
|
|
}
|
|
|
|
if state.DocType != "text/gemini" {
|
|
return ErrOnlyTextGemini
|
|
}
|
|
|
|
gemdoc, err := gemtext.Parse(bytes.NewBuffer(state.Body))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
b := &bytes.Buffer{}
|
|
for _, line := range gemdoc {
|
|
switch line.Type() {
|
|
case gemtext.LineTypeHeading3, gemtext.LineTypeHeading2, gemtext.LineTypeHeading1:
|
|
if _, err := b.Write(line.Raw()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
formatted, _, err := parseGemtextDoc(b.Bytes(), state.SoftWrap)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
state.Modal = []byte(formatted)
|
|
if len(state.Modal) == 0 {
|
|
state.Modal = []byte("No headers on the current page\n")
|
|
}
|
|
|
|
return Print(state)
|
|
}
|
|
|
|
func Links(state *BrowserState) 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(), state.Config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
state.Modal = []byte(formatted)
|
|
if len(state.Links) == 0 {
|
|
state.Modal = []byte("There are no links on the current page\n")
|
|
}
|
|
|
|
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) error {
|
|
if state.Body == nil {
|
|
return ErrMustBeOnAPage
|
|
}
|
|
if filename == "" {
|
|
return ErrSaveNeedsFilename
|
|
}
|
|
|
|
p := filepath.Join(state.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
|
|
}
|
|
|
|
if err := os.WriteFile(p, state.Body, 0o644); err != nil {
|
|
return err
|
|
}
|
|
state.Modal = []byte(fmt.Sprintf("Saved page to %s\n", p))
|
|
return Print(state)
|
|
}
|
|
|
|
func Mark(state *BrowserState, args []string) error {
|
|
switch args[0] {
|
|
case "add":
|
|
return MarkAdd(state, args[1], args[2])
|
|
case "go":
|
|
return MarkGo(state, args[1])
|
|
case "list":
|
|
return MarkList(state)
|
|
case "delete":
|
|
return MarkDelete(state, args[1])
|
|
}
|
|
|
|
return ErrInvalidMarkArgs
|
|
}
|
|
|
|
func TourCmd(state *BrowserState, args []string) error {
|
|
switch args[0] {
|
|
case "add":
|
|
if args[1] == "next" {
|
|
return TourAddNext(state, args[2:])
|
|
}
|
|
return TourAdd(state, args[1:])
|
|
case "show":
|
|
return TourShow(state)
|
|
case "select":
|
|
return TourSelect(state, args[1])
|
|
case "next":
|
|
return TourNext(state)
|
|
case "previous":
|
|
return TourPrevious(state)
|
|
case "clear":
|
|
return TourClear(state)
|
|
case "list":
|
|
return TourList(state)
|
|
case "go":
|
|
return TourGo(state, args[1])
|
|
}
|
|
|
|
return ErrInvalidTourArgs
|
|
}
|
|
|
|
func IdentityCmd(state *BrowserState, args []string) error {
|
|
switch args[0] {
|
|
case "create":
|
|
return IdentityCreate(state, args[1])
|
|
case "list":
|
|
return IdentityList(state)
|
|
case "delete":
|
|
return IdentityDelete(state, args[1])
|
|
case "use":
|
|
switch args[2] {
|
|
case "domain":
|
|
return IdentityUseDomain(state, args[1], args[3])
|
|
case "folder":
|
|
return IdentityUseFolder(state, args[1], args[3])
|
|
case "page":
|
|
return IdentityUsePage(state, args[1], args[3])
|
|
}
|
|
}
|
|
|
|
return ErrInvalidArgs
|
|
}
|
|
|
|
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()
|
|
}
|