2022-02-27 23:17:51 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2022-04-21 18:01:59 +00:00
|
|
|
"errors"
|
2022-02-27 23:17:51 +00:00
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
"os"
|
2022-04-21 18:01:59 +00:00
|
|
|
|
|
|
|
"hilbish/util"
|
|
|
|
|
|
|
|
rt "github.com/arnodel/golua/runtime"
|
2022-02-27 23:17:51 +00:00
|
|
|
)
|
|
|
|
|
2022-11-25 20:56:35 +00:00
|
|
|
var charEscapeMap = []string{
|
|
|
|
"\"", "\\\"",
|
|
|
|
"'", "\\'",
|
|
|
|
"`", "\\`",
|
|
|
|
" ", "\\ ",
|
|
|
|
"(", "\\(",
|
|
|
|
")", "\\)",
|
|
|
|
"[", "\\[",
|
|
|
|
"]", "\\]",
|
|
|
|
"$", "\\$",
|
|
|
|
"&", "\\&",
|
|
|
|
"*", "\\*",
|
|
|
|
">", "\\>",
|
|
|
|
"<", "\\<",
|
|
|
|
"|", "\\|",
|
|
|
|
}
|
|
|
|
var charEscapeMapInvert = invert(charEscapeMap)
|
|
|
|
var escapeReplaer = strings.NewReplacer(charEscapeMap...)
|
|
|
|
var escapeInvertReplaer = strings.NewReplacer(charEscapeMapInvert...)
|
|
|
|
|
|
|
|
func invert(m []string) []string {
|
|
|
|
newM := make([]string, len(charEscapeMap))
|
|
|
|
for i := range m {
|
|
|
|
if (i + 1) % 2 == 0 {
|
|
|
|
newM[i] = m[i - 1]
|
|
|
|
newM[i - 1] = m[i]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return newM
|
|
|
|
}
|
|
|
|
|
|
|
|
func splitForFile(str string) []string {
|
2022-07-09 14:45:11 +00:00
|
|
|
split := []string{}
|
|
|
|
sb := &strings.Builder{}
|
|
|
|
quoted := false
|
|
|
|
|
2022-11-25 20:56:35 +00:00
|
|
|
for i, r := range str {
|
2022-07-09 14:45:11 +00:00
|
|
|
if r == '"' {
|
|
|
|
quoted = !quoted
|
|
|
|
sb.WriteRune(r)
|
2022-11-25 20:56:35 +00:00
|
|
|
} else if r == ' ' && str[i - 1] == '\\' {
|
|
|
|
sb.WriteRune(r)
|
2022-07-09 14:45:11 +00:00
|
|
|
} else if !quoted && r == ' ' {
|
|
|
|
split = append(split, sb.String())
|
|
|
|
sb.Reset()
|
|
|
|
} else {
|
|
|
|
sb.WriteRune(r)
|
|
|
|
}
|
|
|
|
}
|
2022-08-31 03:38:46 +00:00
|
|
|
if strings.HasSuffix(str, " ") {
|
|
|
|
split = append(split, "")
|
|
|
|
}
|
2022-07-09 14:45:11 +00:00
|
|
|
|
|
|
|
if sb.Len() > 0 {
|
|
|
|
split = append(split, sb.String())
|
|
|
|
}
|
|
|
|
|
|
|
|
return split
|
|
|
|
}
|
|
|
|
|
2022-04-20 17:06:46 +00:00
|
|
|
func fileComplete(query, ctx string, fields []string) ([]string, string) {
|
2022-11-25 20:56:35 +00:00
|
|
|
q := splitForFile(ctx)
|
2023-02-07 19:48:59 +00:00
|
|
|
path := ""
|
|
|
|
if len(q) != 0 {
|
|
|
|
path = q[len(q) - 1]
|
|
|
|
}
|
2022-07-09 14:45:11 +00:00
|
|
|
|
2023-02-07 19:48:59 +00:00
|
|
|
return matchPath(path)
|
2022-02-27 23:17:51 +00:00
|
|
|
}
|
|
|
|
|
2022-03-05 19:59:00 +00:00
|
|
|
func binaryComplete(query, ctx string, fields []string) ([]string, string) {
|
2022-11-25 23:35:26 +00:00
|
|
|
q := splitForFile(ctx)
|
2023-02-07 19:48:59 +00:00
|
|
|
query = ""
|
|
|
|
if len(q) != 0 {
|
|
|
|
query = q[len(q) - 1]
|
|
|
|
}
|
2022-11-25 23:35:26 +00:00
|
|
|
|
2022-03-05 19:59:00 +00:00
|
|
|
var completions []string
|
|
|
|
|
|
|
|
prefixes := []string{"./", "../", "/", "~/"}
|
|
|
|
for _, prefix := range prefixes {
|
|
|
|
if strings.HasPrefix(query, prefix) {
|
2022-04-20 17:06:46 +00:00
|
|
|
fileCompletions, filePref := matchPath(query)
|
2022-03-05 19:59:00 +00:00
|
|
|
if len(fileCompletions) != 0 {
|
|
|
|
for _, f := range fileCompletions {
|
2022-05-01 04:49:59 +00:00
|
|
|
fullPath, _ := filepath.Abs(util.ExpandHome(query + strings.TrimPrefix(f, filePref)))
|
2022-11-25 23:35:26 +00:00
|
|
|
if err := findExecutable(escapeInvertReplaer.Replace(fullPath), false, true); err != nil {
|
2022-03-05 19:59:00 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
completions = append(completions, f)
|
|
|
|
}
|
|
|
|
}
|
2022-04-20 17:06:46 +00:00
|
|
|
return completions, filePref
|
2022-03-05 19:59:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// filter out executables, but in path
|
|
|
|
for _, dir := range filepath.SplitList(os.Getenv("PATH")) {
|
|
|
|
// search for an executable which matches our query string
|
|
|
|
if matches, err := filepath.Glob(filepath.Join(dir, query + "*")); err == nil {
|
|
|
|
// get basename from matches
|
|
|
|
for _, match := range matches {
|
|
|
|
// check if we have execute permissions for our match
|
2022-03-18 00:22:30 +00:00
|
|
|
err := findExecutable(match, true, false)
|
|
|
|
if err != nil {
|
2022-03-05 19:59:00 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
// get basename from match
|
|
|
|
name := filepath.Base(match)
|
|
|
|
// add basename to completions
|
|
|
|
completions = append(completions, name)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// add lua registered commands to completions
|
|
|
|
for cmdName := range commands {
|
|
|
|
if strings.HasPrefix(cmdName, query) {
|
|
|
|
completions = append(completions, cmdName)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-07 22:56:22 +00:00
|
|
|
completions = removeDupes(completions)
|
|
|
|
|
2022-03-05 19:59:00 +00:00
|
|
|
return completions, query
|
|
|
|
}
|
|
|
|
|
2022-04-20 17:06:46 +00:00
|
|
|
func matchPath(query string) ([]string, string) {
|
2022-07-09 14:45:11 +00:00
|
|
|
oldQuery := query
|
|
|
|
query = strings.TrimPrefix(query, "\"")
|
2022-02-27 23:17:51 +00:00
|
|
|
var entries []string
|
2022-04-20 17:06:46 +00:00
|
|
|
var baseName string
|
2022-02-27 23:17:51 +00:00
|
|
|
|
2022-11-25 20:56:35 +00:00
|
|
|
query = escapeInvertReplaer.Replace(query)
|
2022-05-01 04:49:59 +00:00
|
|
|
path, _ := filepath.Abs(util.ExpandHome(filepath.Dir(query)))
|
2022-04-20 17:06:46 +00:00
|
|
|
if string(query) == "" {
|
|
|
|
// filepath base below would give us "."
|
|
|
|
// which would cause a match of only dotfiles
|
|
|
|
path, _ = filepath.Abs(".")
|
|
|
|
} else if !strings.HasSuffix(query, string(os.PathSeparator)) {
|
|
|
|
baseName = filepath.Base(query)
|
|
|
|
}
|
|
|
|
|
|
|
|
files, _ := os.ReadDir(path)
|
2023-02-06 21:36:25 +00:00
|
|
|
for _, entry := range files {
|
|
|
|
// should we handle errors here?
|
2023-02-07 14:42:36 +00:00
|
|
|
file, err := entry.Info()
|
|
|
|
if err == nil && file.Mode() & os.ModeSymlink != 0 {
|
|
|
|
path, err := filepath.EvalSymlinks(filepath.Join(path, file.Name()))
|
|
|
|
if err == nil {
|
|
|
|
file, err = os.Lstat(path)
|
|
|
|
}
|
2023-02-06 21:36:25 +00:00
|
|
|
}
|
|
|
|
|
2022-04-20 17:06:46 +00:00
|
|
|
if strings.HasPrefix(file.Name(), baseName) {
|
|
|
|
entry := file.Name()
|
|
|
|
if file.IsDir() {
|
|
|
|
entry = entry + string(os.PathSeparator)
|
2022-02-27 23:17:51 +00:00
|
|
|
}
|
2022-07-09 14:45:11 +00:00
|
|
|
if !strings.HasPrefix(oldQuery, "\"") {
|
|
|
|
entry = escapeFilename(entry)
|
|
|
|
}
|
2022-04-20 17:06:46 +00:00
|
|
|
entries = append(entries, entry)
|
2022-02-27 23:17:51 +00:00
|
|
|
}
|
|
|
|
}
|
2022-11-30 18:26:43 +00:00
|
|
|
if !strings.HasPrefix(oldQuery, "\"") {
|
|
|
|
baseName = escapeFilename(baseName)
|
|
|
|
}
|
2022-03-05 19:59:00 +00:00
|
|
|
|
2022-04-20 17:06:46 +00:00
|
|
|
return entries, baseName
|
|
|
|
}
|
|
|
|
|
|
|
|
func escapeFilename(fname string) string {
|
2022-11-25 20:56:35 +00:00
|
|
|
return escapeReplaer.Replace(fname)
|
2022-02-27 23:17:51 +00:00
|
|
|
}
|
2022-04-21 18:01:59 +00:00
|
|
|
|
2023-12-02 15:11:28 +00:00
|
|
|
// #interface completion
|
2022-12-15 04:00:54 +00:00
|
|
|
// tab completions
|
|
|
|
// The completions interface deals with tab completions.
|
2022-04-23 01:16:35 +00:00
|
|
|
func completionLoader(rtm *rt.Runtime) *rt.Table {
|
|
|
|
exports := map[string]util.LuaExport{
|
2023-12-26 03:08:29 +00:00
|
|
|
"bins": {hcmpBins, 3, false},
|
|
|
|
"call": {hcmpCall, 4, false},
|
|
|
|
"files": {hcmpFiles, 3, false},
|
|
|
|
"handler": {hcmpHandler, 2, false},
|
2022-04-23 01:16:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
mod := rt.NewTable()
|
|
|
|
util.SetExports(rtm, mod, exports)
|
|
|
|
|
|
|
|
return mod
|
|
|
|
}
|
|
|
|
|
2023-12-02 15:11:28 +00:00
|
|
|
// #interface completion
|
2023-12-26 03:08:29 +00:00
|
|
|
// bins(query, ctx, fields) -> entries (table), prefix (string)
|
|
|
|
// Return binaries/executables based on the provided parameters.
|
|
|
|
// This function is meant to be used as a helper in a command completion handler.
|
|
|
|
// #param query string
|
|
|
|
// #param ctx string
|
|
|
|
// #param fields table
|
|
|
|
/*
|
|
|
|
#example
|
|
|
|
-- an extremely simple completer for sudo.
|
|
|
|
hilbish.complete('command.sudo', function(query, ctx, fields)
|
|
|
|
table.remove(fields, 1)
|
|
|
|
if #fields[1] then
|
|
|
|
-- return commands because sudo runs a command as root..!
|
|
|
|
|
|
|
|
local entries, pfx = hilbish.completion.bins(query, ctx, fields)
|
|
|
|
return {
|
|
|
|
type = 'grid',
|
|
|
|
items = entries
|
|
|
|
}, pfx
|
|
|
|
end
|
|
|
|
|
|
|
|
-- ... else suggest files or anything else ..
|
|
|
|
end)
|
|
|
|
#example
|
|
|
|
*/
|
|
|
|
func hcmpBins(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
|
|
|
query, ctx, fds, err := getCompleteParams(t, c)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
completions, pfx := binaryComplete(query, ctx, fds)
|
|
|
|
luaComps := rt.NewTable()
|
|
|
|
|
|
|
|
for i, comp := range completions {
|
|
|
|
luaComps.Set(rt.IntValue(int64(i + 1)), rt.StringValue(comp))
|
|
|
|
}
|
|
|
|
|
|
|
|
return c.PushingNext(t.Runtime, rt.TableValue(luaComps), rt.StringValue(pfx)), nil
|
2022-04-23 01:16:35 +00:00
|
|
|
}
|
|
|
|
|
2023-12-02 15:11:28 +00:00
|
|
|
// #interface completion
|
2023-02-07 22:18:03 +00:00
|
|
|
// call(name, query, ctx, fields) -> completionGroups (table), prefix (string)
|
2023-12-26 03:08:29 +00:00
|
|
|
// Calls a completer function. This is mainly used to call a command completer, which will have a `name`
|
|
|
|
// in the form of `command.name`, example: `command.git`.
|
|
|
|
// You can check the Completions doc or `doc completions` for info on the `completionGroups` return value.
|
|
|
|
// #param name string
|
|
|
|
// #param query string
|
|
|
|
// #param ctx string
|
|
|
|
// #param fields table
|
|
|
|
func hcmpCall(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
2022-04-21 18:01:59 +00:00
|
|
|
if err := c.CheckNArgs(4); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
completer, err := c.StringArg(0)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
query, err := c.StringArg(1)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
ctx, err := c.StringArg(2)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
fields, err := c.TableArg(3)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var completecb *rt.Closure
|
|
|
|
var ok bool
|
|
|
|
if completecb, ok = luaCompletions[completer]; !ok {
|
|
|
|
return nil, errors.New("completer " + completer + " does not exist")
|
|
|
|
}
|
|
|
|
|
|
|
|
// we must keep the holy 80 cols
|
2023-09-30 23:52:33 +00:00
|
|
|
cont := c.Next()
|
|
|
|
err = rt.Call(l.MainThread(), rt.FunctionValue(completecb),
|
|
|
|
[]rt.Value{rt.StringValue(query), rt.StringValue(ctx), rt.TableValue(fields)},
|
|
|
|
cont)
|
2022-04-21 18:01:59 +00:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-09-30 23:52:33 +00:00
|
|
|
return cont, nil
|
2022-04-21 18:01:59 +00:00
|
|
|
}
|
|
|
|
|
2023-12-02 15:11:28 +00:00
|
|
|
// #interface completion
|
2023-02-07 22:18:03 +00:00
|
|
|
// files(query, ctx, fields) -> entries (table), prefix (string)
|
2023-12-26 03:08:29 +00:00
|
|
|
// Returns file matches based on the provided parameters.
|
|
|
|
// This function is meant to be used as a helper in a command completion handler.
|
|
|
|
// #param query string
|
|
|
|
// #param ctx string
|
|
|
|
// #param fields table
|
|
|
|
func hcmpFiles(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
2022-04-21 18:01:59 +00:00
|
|
|
query, ctx, fds, err := getCompleteParams(t, c)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-04-23 01:16:35 +00:00
|
|
|
completions, pfx := fileComplete(query, ctx, fds)
|
2022-04-21 18:01:59 +00:00
|
|
|
luaComps := rt.NewTable()
|
|
|
|
|
|
|
|
for i, comp := range completions {
|
|
|
|
luaComps.Set(rt.IntValue(int64(i + 1)), rt.StringValue(comp))
|
|
|
|
}
|
|
|
|
|
2022-04-23 01:16:35 +00:00
|
|
|
return c.PushingNext(t.Runtime, rt.TableValue(luaComps), rt.StringValue(pfx)), nil
|
2022-04-21 18:01:59 +00:00
|
|
|
}
|
|
|
|
|
2023-12-02 15:11:28 +00:00
|
|
|
// #interface completion
|
2023-12-26 03:08:29 +00:00
|
|
|
// handler(line, pos)
|
|
|
|
// This function contains the general completion handler for Hilbish. This function handles
|
|
|
|
// completion of everything, which includes calling other command handlers, binaries, and files.
|
|
|
|
// This function can be overriden to supply a custom handler. Note that alias resolution is required to be done in this function.
|
|
|
|
// #param line string The current Hilbish command line
|
|
|
|
// #param pos number Numerical position of the cursor
|
|
|
|
/*
|
|
|
|
#example
|
|
|
|
-- stripped down version of the default implementation
|
|
|
|
function hilbish.completion.handler(line, pos)
|
|
|
|
local query = fields[#fields]
|
|
|
|
|
|
|
|
if #fields == 1 then
|
|
|
|
-- call bins handler here
|
|
|
|
else
|
|
|
|
-- call command completer or files completer here
|
|
|
|
end
|
|
|
|
end
|
|
|
|
#example
|
|
|
|
*/
|
|
|
|
func hcmpHandler(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
|
|
|
return c.Next(), nil
|
2022-04-21 18:01:59 +00:00
|
|
|
}
|
|
|
|
|
2023-12-26 03:08:29 +00:00
|
|
|
|
2022-04-21 18:01:59 +00:00
|
|
|
func getCompleteParams(t *rt.Thread, c *rt.GoCont) (string, string, []string, error) {
|
|
|
|
if err := c.CheckNArgs(3); err != nil {
|
|
|
|
return "", "", []string{}, err
|
|
|
|
}
|
|
|
|
query, err := c.StringArg(0)
|
|
|
|
if err != nil {
|
|
|
|
return "", "", []string{}, err
|
|
|
|
}
|
|
|
|
ctx, err := c.StringArg(1)
|
|
|
|
if err != nil {
|
|
|
|
return "", "", []string{}, err
|
|
|
|
}
|
|
|
|
fields, err := c.TableArg(2)
|
|
|
|
if err != nil {
|
|
|
|
return "", "", []string{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var fds []string
|
|
|
|
util.ForEach(fields, func(k rt.Value, v rt.Value) {
|
|
|
|
if v.Type() == rt.StringType {
|
|
|
|
fds = append(fds, v.AsString())
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
return query, ctx, fds, err
|
|
|
|
}
|