You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
226 lines
6.2 KiB
226 lines
6.2 KiB
package main |
|
|
|
import ( |
|
"fmt" |
|
"io" |
|
"net/url" |
|
"os" |
|
"os/exec" |
|
"regexp" |
|
"strconv" |
|
"strings" |
|
|
|
"github.com/fatih/color" |
|
ln "github.com/peterh/liner" |
|
flag "github.com/spf13/pflag" |
|
) |
|
|
|
var ( |
|
promptColor = color.FgGreen |
|
ErrorColor = color.New(color.FgRed).SprintfFunc() |
|
) |
|
|
|
// flags |
|
var ( |
|
noInteractive = flag.BoolP("no-interactive", "I", false, "don't go to the line-mode interface\n") |
|
appendInput = flag.StringP("input", "i", "", "append input to URL ('?' + percent-encoded input)\n") |
|
helpFlag = flag.BoolP("help", "h", false, "get help on the cli") |
|
searchFlag = flag.StringP("search", "s", "", "search with the search engine (this takes priority over URL and --input)\n") |
|
) |
|
|
|
var ( |
|
// quoteFieldRe greedily matches between matching pairs of '', "", or |
|
// non-word characters. |
|
quoteFieldRe = regexp.MustCompile("'(.*)'|\"(.*)\"|(\\S*)") |
|
) |
|
|
|
// QuotedFields is an alternative to strings.Fields (see: |
|
// https://golang.org/pkg/strings#Fields) that respects spaces between matching |
|
// pairs of quotation delimeters. |
|
// |
|
// For instance, the quoted fields of the string "foo bar 'baz etc'" would be: |
|
// []string{"foo", "bar", "baz etc"} |
|
// |
|
// Whereas the same argument given to strings.Fields, would return: |
|
// []string{"foo", "bar", "'baz", "etc'"} |
|
func QuotedFields(s string) []string { |
|
submatches := quoteFieldRe.FindAllStringSubmatch(s, -1) |
|
out := make([]string, 0, len(submatches)) |
|
for _, matches := range submatches { |
|
// if a leading or trailing space is found, ignore that |
|
if matches[0] == "" { |
|
continue |
|
} |
|
// otherwise, find the first non-empty match (inside balanced |
|
// quotes, or a space-delimited string) |
|
var str string |
|
for _, m := range matches[1:] { |
|
if len(m) > 0 { |
|
str = m |
|
break |
|
} |
|
} |
|
out = append(out, str) |
|
} |
|
return out |
|
} |
|
|
|
// Pager uses `less` to display body |
|
// falls back to fmt.Print if errors encountered |
|
func Pager(body string, conf *Config) { |
|
cmd := exec.Command("less") |
|
stdin, err := cmd.StdinPipe() |
|
if err != nil { |
|
fmt.Print(body) |
|
return |
|
} |
|
cmd.Stdout = os.Stdout |
|
cmd.Stderr = os.Stderr |
|
cmd.Env = append(os.Environ(), "LESS="+conf.LessOpts) |
|
if err := cmd.Start(); err != nil { |
|
fmt.Print(body) |
|
return |
|
} |
|
io.WriteString(stdin, body) |
|
stdin.Close() |
|
cmd.Stdin = os.Stdin |
|
cmd.Wait() |
|
} |
|
|
|
func queryEscape(s string) string { |
|
return strings.Replace(url.QueryEscape(s), "+", "%20", -1) |
|
} |
|
|
|
func main() { |
|
// command-line stuff |
|
flag.Usage = func() { // Usage override |
|
fmt.Fprintf(os.Stderr, "Usage: %s [FLAGS] [URL]\n", os.Args[0]) |
|
fmt.Fprintf(os.Stderr, "\nFlags:\n") |
|
flag.PrintDefaults() |
|
fmt.Fprintf(os.Stderr, "For help on the TUI client, type ? at interactive prompt, or see gelim(1)\n") |
|
} |
|
flag.Parse() |
|
if *helpFlag { // Handling --help myself since pflag prints an ugly ErrHelp |
|
flag.Usage() |
|
return |
|
} |
|
u := "" |
|
cliURL := false // this is to avoid going to c.conf.StartURL if URL is visited from CLI |
|
|
|
c, err := NewClient() |
|
if err != nil { |
|
fmt.Println(ErrorColor("%s", err.Error())) |
|
os.Exit(1) |
|
} |
|
if *searchFlag != "" { |
|
c.Search(*searchFlag) // it's "searchQuery" more like |
|
cliURL = true |
|
} else { // need else because when user use --search we should ignore URL and --input |
|
u = flag.Arg(0) // URL |
|
if u != "" { |
|
if *appendInput != "" { |
|
u = u + "?" + queryEscape(*appendInput) |
|
} |
|
c.HandleURL(u) |
|
cliURL = true |
|
} else { |
|
// if --input used but url arg is not present |
|
if *appendInput != "" { |
|
fmt.Println(ErrorColor("ERROR: --input used without an URL argument")) |
|
// should we print usage? |
|
os.Exit(1) |
|
} |
|
} |
|
} |
|
if *noInteractive { |
|
return |
|
} |
|
|
|
if c.conf.StartURL != "" && !cliURL { |
|
c.HandleURL(c.conf.StartURL) |
|
} |
|
|
|
// and now here comes the line-mode prompts and stuff |
|
rl := c.mainReader |
|
|
|
for { |
|
color.Set(promptColor) |
|
var line string |
|
var err error |
|
if c.promptSuggestion != "" { |
|
line, err = rl.PromptWithSuggestion(c.parsePrompt()+" ", c.promptSuggestion, -1) |
|
c.promptSuggestion = "" |
|
} else { |
|
line, err = rl.Prompt(c.parsePrompt() + " ") |
|
} |
|
color.Unset() |
|
if err != nil { |
|
if err == ln.ErrPromptAborted { |
|
os.Exit(1) |
|
} |
|
fmt.Println(ErrorColor("\nerror reading line input")) |
|
fmt.Println(ErrorColor(err.Error())) |
|
os.Exit(1) // Exiting because it will cause an infinite loop of error if used 'continue' here |
|
} |
|
rl.AppendHistory(line) |
|
line = strings.TrimSpace(line) |
|
if line == "" { |
|
continue |
|
} |
|
// TODO: put arg splitting logic into client.go or cmd.go so it can respect Command.quotedArgs value |
|
lineFields := strings.Fields(line) |
|
cmd := strings.ToLower(lineFields[0]) |
|
var args []string |
|
if len(lineFields) > 1 { |
|
args = lineFields[1:] |
|
} |
|
// Command stuff |
|
if ok := c.Command(cmd, args...); !ok { |
|
if strings.Contains(cmd, ".") || strings.Contains(cmd, "/") { |
|
// look like an URL |
|
u = cmd |
|
parsed, err := url.Parse(u) |
|
if err != nil { |
|
fmt.Println(ErrorColor("invalid url")) |
|
continue |
|
} |
|
// example: |
|
// if current url is example.com, and user would like to visit example.com/foo.txt |
|
// they can type "/foo.txt", and if they use "foo.txt" it would lead to gemini://foo.txt |
|
// which means if current url is example.com/bar/ and user wants example.com/bar/foo.txt, |
|
// they can either use "/bar/foo.txt" or "./foo.txt" |
|
// so if user want to do relative path it has to start with / or . |
|
if (parsed.Scheme == "" || parsed.Host == "") && (!strings.HasPrefix(u, ".")) && (!strings.HasPrefix(u, "/")) { |
|
parsed, err = url.Parse("gemini://" + u) |
|
} |
|
// this allows users to use relative urls at the prompt |
|
if len(c.history) != 0 { |
|
parsed = c.history[len(c.history)-1].ResolveReference(parsed) |
|
} else { |
|
if strings.HasPrefix(u, ".") && strings.HasPrefix(u, "/") { |
|
fmt.Println("no history yet, cannot use relative path") |
|
} |
|
} |
|
c.HandleParsedURL(parsed) |
|
continue |
|
} |
|
// at this point the user input is probably not an url |
|
index, err := strconv.Atoi(cmd) |
|
if err != nil { |
|
// looks like an unknown command |
|
fmt.Println(ErrorColor("Unknown command. Hint: try typing ? and hit enter")) |
|
continue |
|
} |
|
// link index lookup |
|
u, spartanInput := c.GetLinkFromIndex(index) |
|
if u == "" { |
|
continue |
|
} |
|
if spartanInput { |
|
c.Input(u, false) |
|
continue |
|
} |
|
c.HandleURL(u) |
|
} |
|
} |
|
}
|
|
|