gelim/cmd.go

635 lines
18 KiB
Go

package main
import (
"fmt"
"io"
"os"
"os/exec"
"strconv"
"strings"
"github.com/google/shlex"
)
// Command is the metadata of all (non-meta) commands in the client
type Command struct {
aliases []string
do func(client *Client, args ...string)
help string
quotedArgs bool // Default false
hidden bool
}
func printHelp(style *Style) {
maxWidth := 0
var placeholder string
curWidth := 0
for name, cmd := range commands {
placeholder = ""
firstLine := strings.SplitN(cmd.help, "\n", 2)[0]
parts := strings.SplitN(firstLine, ":", 2)
if len(parts) == 2 {
placeholder = strings.TrimSpace(parts[0])
}
curWidth = len(name) + 1 // 1 for space
if placeholder != "" {
curWidth += len(placeholder) + 3 // <> and a space
}
if curWidth > maxWidth {
maxWidth = curWidth
}
}
minSepSpaceLen := 2 // min space between command and the description
// Here comes the fun part
// We are now *actually* printing the help
fmt.Println(`You can directly enter a url or link-index (number) at the prompt.
Otherwise, there are plenty of useful commands you can use. Arguments
are separated by spaces, and quoting with ' and " is supported like the
shell, escaping quotes is also supported.
You can supply a command name to 'help' to see the help for a specific
command, like 'help tour.
Commands:`)
var spacesBetween int
for name, cmd := range commands {
// TODO: wrap description with... aniswrap?
if cmd.hidden {
continue
}
parts := formatCommandHelp(&cmd, name, false, style)
// FIXME: formatcmd help behaviour changed, alter other places!
spacesBetween = maxWidth + minSepSpaceLen - len(parts[0]) - len(name) - 1
fmt.Printf(" %s %s%s %s\n", name, style.cmdPlaceholder.Sprint(parts[0]), strings.Repeat(" ", spacesBetween), parts[1])
}
fmt.Println("\nMeta commands:")
fmt.Println(" help | ? | h [<cmd>...]")
fmt.Println(" aliases | alias | synonym [<cmd>...]")
}
// Handles placeholders in cmd.help if any, if format is true it will return the placeholder
// string and the help string concatenated, if format is false, it returns them separately.
func formatCommandHelp(cmd *Command, name string, format bool, style *Style) (formatted []string) {
firstLine := strings.SplitN(cmd.help, "\n", 2)[0]
parts := strings.SplitN(firstLine, ":", 2)
var placeholder, desc string
desc = firstLine
if len(parts) == 2 {
placeholder = strings.TrimSpace(parts[0])
desc = strings.TrimSpace(parts[1])
}
left := ""
formatted = make([]string, 2)
if format {
if placeholder != "" {
left = fmt.Sprintf("%s %s", name, style.cmdPlaceholder.Sprint(placeholder))
} else {
left = name
desc = firstLine
}
formatted[0] = style.cmdLabels.Sprint("Usage") + fmt.Sprintf(": %s\n\n", left) + style.cmdSynopsis.Sprint(desc)
return
}
formatted[0] = placeholder
formatted[1] = desc
return
}
// ResolveNonPositiveIndex returns the implied index number based on user's
// configuration for a given non-positive index query
func (c *Client) ResolveNonPositiveIndex(index int, totalLength int) int {
if index == 0 {
if c.conf.Index0Shortcut == 0 {
c.style.ErrorMsg("Behaviour for index 0 is undefined.")
fmt.Println("You can use -1 for accessing the last item, -2 for second last, etc.")
fmt.Println("Configure the behaviour of 0 in the config file.\nExample: index0shortcut = -1, then whenever you use 0 it will be -1 instead.\nThis works for commands history, links, editurl, and tour.")
return 0
}
index = c.conf.Index0Shortcut
}
if index < 0 {
// Because the index is 1-indexed
// if index is -1, the final index is totalLength
index = totalLength + index + 1
}
return index
}
// Commands that reference variable commands, putting them separtely to avoid
// initialization cycle
var metaCommands = map[string]Command{
"help": {
aliases: []string{"h", "?", "hi"},
do: func(c *Client, args ...string) {
if len(args) > 0 {
for i, v := range args {
// Separator
if len(args) > 1 && i > 0 {
fmt.Println("---")
}
// Yes, have to do metaCommands manually
switch v {
case "help", "?", "h", "hi":
fmt.Println("help: You literally just get help :P")
continue
case "alias", "aliases", "synonymn":
fmt.Println("alias: See aliases for a command or all commands")
continue
}
name, cmd, ok := c.LookupCommand(v)
if !ok {
fmt.Println(v, "command not found")
continue
}
formatted := formatCommandHelp(&cmd, name, true, c.style)
fmt.Println(formatted[0])
if len(cmd.aliases) > 0 {
fmt.Println("\n"+c.style.cmdLabels.Sprint("Aliases")+": [", strings.Join(cmd.aliases, ", "), "]")
}
// Extra help for command if the command supports it
if strings.Contains(cmd.help, "\n") {
extra := strings.SplitN(cmd.help, "\n", 2)[1]
if extra != "" {
fmt.Println()
fmt.Println(extra)
}
}
}
return
}
printHelp(c.style)
},
help: "[<cmd...>] : print the usage or the help for a command",
},
"aliases": {
aliases: []string{"alias", "synonym"},
do: func(c *Client, args ...string) {
if len(args) > 0 {
for _, v := range args {
// I'm so tired having to do this stupid switch again and again for metaCommands
// but I can't find a better solution UGH
switch v {
case "help", "?", "h", "hi":
fmt.Println("help ? h hi")
continue
case "alias", "aliases", "synonym":
fmt.Println("alias aliases synonym")
continue
}
name, cmd, ok := c.LookupCommand(v)
if !ok {
fmt.Println(v, "command not found")
}
fmt.Println(name, strings.Join(cmd.aliases, " "))
}
return
}
fmt.Println("todo")
},
help: "<cmd...> : see aliases for a command or all commands",
},
}
var commands = map[string]Command{
"search": {
aliases: []string{"s"},
do: func(c *Client, args ...string) {
c.Search(strings.Join(args, " "))
},
quotedArgs: false,
help: "[<query...>] : search with search engine",
},
"quit": {
aliases: []string{"exit", "x", "q"},
do: func(c *Client, args ...string) {
c.QuitClient(0)
},
help: "exit gelim",
},
"reload": {
aliases: []string{"r"},
do: func(c *Client, args ...string) {
if len(c.history) < 1 {
c.style.ErrorMsg("No history yet!")
return
}
c.HandleParsedURL(c.history[len(c.history)-1])
},
help: "reload current page",
},
"history": {
aliases: []string{"hist", "his"},
do: func(c *Client, args ...string) {
if len(c.history) == 0 {
c.style.WarningMsg("No history yet")
return
}
if len(args) == 0 {
for i, v := range c.history {
fmt.Println(i+1, v.String())
}
return
}
// Ignores all other arguments
index, err := strconv.Atoi(args[0])
if err != nil {
c.style.ErrorMsg("Invalid history index number. Could not convert to integer")
return
}
if index = c.ResolveNonPositiveIndex(index, len(c.history)); index == 0 {
return
}
if len(c.history) < index || index <= 0 {
c.style.ErrorMsg(fmt.Sprintf("%d item(s) in history", len(c.history)))
fmt.Println("Try `history` to view the history")
return
}
// TODO: handle spartan input
c.HandleParsedURL(c.history[index-1])
},
help: `[<index>] : print list of previously visited URLs, or visit an item in history
Examples:
- history
- his 1
- hist -3`,
},
"link": {
aliases: []string{"l", "peek", "links"},
do: func(c *Client, args ...string) {
if len(c.links) == 0 || c.links[0] == "" {
c.style.WarningMsg("There are no links")
return
}
if len(args) < 1 {
for i, v := range c.links {
fmt.Println(i+1, v)
}
return
}
var index int
var err error
for _, arg := range args {
index, err = strconv.Atoi(arg)
if err != nil {
c.style.ErrorMsg(arg + ": Invalid link index")
continue
}
index = c.ResolveNonPositiveIndex(index, len(c.links))
if index == 0 {
continue
}
if index < 1 || index > len(c.links) {
c.style.ErrorMsg(arg + ": Invalid link index")
fmt.Println("Total number of links is", len(c.links))
continue
}
link, _ := c.GetLinkFromIndex(index)
fmt.Println(index, link) // TODO: also save the label in c.links
}
},
help: `[<index>...] : peek what a link index would link to, or see the list of all links
You can use non-positive indexes too, see ` + "`links 0`" + ` for more information
Examples:
- links
- l 1
- l -3
- l 1 2 3`,
},
"back": {
aliases: []string{"b"},
do: func(c *Client, args ...string) {
if len(c.history) < 2 {
c.style.ErrorMsg("nothing to go back to (try `history` to see history)")
return
}
c.HandleParsedURL(c.history[len(c.history)-2])
c.history = c.history[0 : len(c.history)-2]
},
help: "go back in history",
},
"forward": {
aliases: []string{"f"},
do: func(*Client, ...string) {
fmt.Println("not implemented yet!")
},
help: "go forward in history",
hidden: true,
},
"current": {
aliases: []string{"u", "url", "cur"},
do: func(c *Client, args ...string) {
if len(c.history) == 0 {
fmt.Println("No history yet!")
return
}
fmt.Println(c.history[len(c.history)-1])
},
help: "print current url",
},
"copyurl": {
aliases: []string{"cu", "yy"},
do: func(c *Client, args ...string) {
var urlStr string
if len(args) < 1 {
if len(c.history) == 0 {
fmt.Println("No history yet!")
return
}
urlStr = c.history[len(c.history)-1].String()
fmt.Println("url:", urlStr)
c.ClipboardCopy(urlStr)
return
}
var index int
var err error
for i, arg := range args {
index, err = strconv.Atoi(arg)
if err != nil {
c.style.ErrorMsg(arg + ": Invalid link index")
continue
}
index = c.ResolveNonPositiveIndex(index, len(c.links))
if index == 0 {
continue
}
if index < 1 || index > len(c.links) {
c.style.ErrorMsg(arg + ": Invalid link index")
continue
}
link, _ := c.GetLinkFromIndex(index)
if len(args) > 1 && i != 0 {
urlStr += "\n"
}
urlStr += link
fmt.Println("url:", link)
}
c.ClipboardCopy(urlStr)
},
help: `[<index>...] : copy current url or links on page to clipboard
Set config file option clipboardCopyCmd to the command where stdin will be piped,
to let it handle clipboard copying.
(eg: echo 'clipboardCopyCmd = "pbcopy"' >> ~/.config/gelim/config.toml)`,
},
"editurl": {
aliases: []string{"e", "eu", "edit"},
do: func(c *Client, args ...string) {
// TODO: Use a link from current page or from history instead of current url
var link string
if len(args) != 0 {
arg := args[0]
index, err := strconv.Atoi(arg)
if err != nil {
c.style.ErrorMsg(arg + ": Invalid link index")
return
}
index = c.ResolveNonPositiveIndex(index, len(c.links))
if index == 0 {
return
}
if index < 1 || index > len(c.links) {
c.style.ErrorMsg(arg + ": Invalid link index")
return
}
link, _ = c.GetLinkFromIndex(index)
} else {
if len(c.history) != 0 {
link = c.history[len(c.history)-1].String()
} else {
c.style.ErrorMsg("no history yet")
return
}
}
c.promptSuggestion = link
},
help: "[<index>] : edit the current url or a link on the current page",
},
"tour": {
aliases: []string{"t", "loop"},
do: func(c *Client, args ...string) {
if len(args) == 0 { // Just `tour`
if len(c.tourLinks) == 0 {
c.style.ErrorMsg("Nothing to tour")
return
}
if c.tourNext == len(c.tourLinks) {
fmt.Println("End of tour :)")
fmt.Println("Use `tour go 1` to go back to the beginning")
return
}
c.HandleURLWrapper(c.tourLinks[c.tourNext])
c.tourNext++
return
}
// tour commands
switch args[0] {
case "ls", "l":
current := ""
for i, v := range c.tourLinks {
current = ""
if i == c.tourNext {
current = " <--next"
}
fmt.Printf("%d %s%s\n", i+1, v, current)
}
case "clear", "c":
fmt.Println("Cleared", len(c.tourLinks), "items")
c.tourLinks = nil
c.tourNext = 0
case "go", "g":
if len(args) == 1 {
c.style.ErrorMsg("Argument expected for `go` subcommand.")
fmt.Println("Use `tour ls` to list tour items, `tour go N` to go to the Nth item.")
return
}
number, err := strconv.Atoi(args[1])
if err != nil {
c.style.ErrorMsg("Unable to convert " + args[1] + " to integer")
return
}
if number = c.ResolveNonPositiveIndex(number, len(c.tourLinks)); number == 0 {
return
}
if number > len(c.tourLinks) || number < 1 {
c.style.ErrorMsg(fmt.Sprintf("%d item(s) in tour list", len(c.tourLinks)))
fmt.Println("Use `tour ls` to list")
return
}
// Because user provided number is 1-indexed and tourNext is 0-indexed
c.HandleURLWrapper(c.tourLinks[number-1])
c.tourNext = number
case "*", "all":
c.tourLinks = append(c.tourLinks, c.links...)
fmt.Println("Added", len(c.links), "items to tour list")
default: // `tour 1 2 3`, `tour 1,4 7 8 10,`
if len(c.links) == 0 {
c.style.ErrorMsg("No links yet")
return
}
added := 0
for _, v := range args {
if strings.Contains(v, ",") {
// start,end or start,
// Without end will imply until the last link
parts := strings.SplitN(v, ",", 2)
if parts[1] == "" {
// FIXME: avoid extra int->str->int conversion
parts[1] = fmt.Sprint(len(c.links))
}
if parts[0] == "" {
// FIXME: avoid extra int->str->int conversion
parts[0] = "1"
}
start, err := strconv.Atoi(parts[0])
end, err2 := strconv.Atoi(parts[1])
if err != nil || err2 != nil {
c.style.ErrorMsg("Number before or after ',' is not an integer: " + v)
continue
}
if start > end {
start, end = end, start
}
if start <= 0 || end > len(c.links) {
c.style.ErrorMsg("Invalid range: " + v)
continue
}
// start and end are both inclusive for us, but not for go
c.tourLinks = append(c.tourLinks, c.links[start-1:end]...)
added += len(c.links[start-1 : end])
continue
}
// WIll reach here if it's not a range (no ',' in arg)
number, err := strconv.Atoi(v)
if err != nil {
c.style.ErrorMsg("Unable to convert " + v + " to integer")
continue
}
if number = c.ResolveNonPositiveIndex(number, len(c.links)); number == 0 {
continue
}
if number > len(c.links) || number <= 0 {
c.style.ErrorMsg(v + " is not in range of the number of links available")
fmt.Println("Use `links` to see all the links")
continue
}
c.tourLinks = append(c.tourLinks, c.links[number-1])
added += 1
}
fmt.Println("Added", added, "items to tour list")
}
},
help: `[<range or number>...] : loop over selection of links in current page
tour command with no arguments will visit the next link in tour
Subcommands:
- l[s] list items in tour
- c[lear] clear tour list
- g[o] jump to item in tour
Use tour * to add all links. you can use ranges like 1,10 or 10,1 with single links as multiple arguments.
Use tour ls/clear to view items or clear all.
tour go <index> takes you to an item in the tour list
Examples:
- tour ,5 6,7 -1 9 11,
- tour ls
- tour
- tour g 3
- tour clear`,
},
// TODO: didn't have time to finish this lol
// "config": {
// aliases: []string{"c", "conf"},
// do: func(c *Client, args ...string) {
// field := reflect.ValueOf(c.conf).Elem().FieldByName(args[0])
// // if field == 0 {
// // fmt.Println("key", args[0], "not found")
// // return
// // }
// field.Set(reflect.Value{args[1]})
// return
// },
// help: "<key> <value>: set a configuration value for the current gelim session",
// quotedArgs: true,
// },
"page": {
aliases: []string{"p", "print", "view", "display"},
do: func(c *Client, args ...string) {
if c.lastPage == nil {
c.style.ErrorMsg("No previous page to redisplay")
return
}
c.DisplayPage(c.lastPage)
},
help: "view current page again without reloading",
},
"redirects": {
aliases: []string{"redir", "redirstack", "redirect"},
do: func(c *Client, args ...string) {
if c.redir.count > 0 {
// Should be synced with that from PromptRedirect
if c.redir.count > c.redir.historyLen {
fmt.Println("Showing the last", c.redir.historyLen, "redirects:")
}
c.redir.showHistory()
} else {
fmt.Println("No redirects")
}
},
help: "view the redirects that led to current page (if any)",
},
}
// CommandCompleter returns a suitable command to complete an input line
func CommandCompleter(line string) (c []string) {
for name := range commands {
if strings.HasPrefix(name, strings.ToLower(line)) {
c = append(c, name)
}
}
return
}
func (c *Client) ClipboardCopy(content string) (ok bool) {
ok = true
if c.conf.ClipboardCopyCmd == "" {
ok = false
c.style.ErrorMsg("please set a clipboard command in config file option 'clipboardCopyCmd'\nThe content to copy will be piped into that command as stdin")
return
}
parts, err := shlex.Split(c.conf.ClipboardCopyCmd)
if err != nil {
ok = false
c.style.ErrorMsg("Could not parse ClipboardCopyCmd into command and arguments: " + c.conf.ClipboardCopyCmd)
return
}
cmd := exec.Command(parts[0], parts[1:]...)
stdin, err := cmd.StdinPipe()
if err != nil {
ok = false
c.style.ErrorMsg(fmt.Sprintf("Error running command %s with arguments %v: %s", parts[0], parts[1:], err.Error()))
return
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err = cmd.Start(); err != nil {
ok = false
c.style.ErrorMsg(fmt.Sprintf("Error running command %s with arguments %v: %s", parts[0], parts[1:], err.Error()))
return
}
io.WriteString(stdin, content)
stdin.Close()
cmd.Stdin = os.Stdin
cmd.Wait()
fmt.Println("Copied successfully")
return
}