diff --git a/api.go b/api.go index f2d4aa1..ed199a8 100644 --- a/api.go +++ b/api.go @@ -74,10 +74,11 @@ The nice lil shell for {blue}Lua{reset} fanatics! util.SetField(L, hshuser, "data", lua.LString(userDataDir), "XDG data directory") util.Document(L, hshuser, "User directories to store configs and/or modules.") L.SetField(mod, "userDir", hshuser) - + + // hilbish.os table hshos := L.NewTable() info, _ := osinfo.GetOSInfo() - + util.SetField(L, hshos, "family", lua.LString(info.Family), "Family name of the current OS") util.SetField(L, hshos, "name", lua.LString(info.Name), "Pretty name of the current OS") util.SetField(L, hshos, "version", lua.LString(info.Version), "Version of the current OS") @@ -95,11 +96,63 @@ The nice lil shell for {blue}Lua{reset} fanatics! util.Document(L, historyModule, "History interface for Hilbish.") L.SetField(mod, "history", historyModule) + // hilbish.completions table + hshcomp := L.NewTable() + + util.SetField(L, hshcomp, "files", L.NewFunction(luaFileComplete), "Completer for files") + util.SetField(L, hshcomp, "bins", L.NewFunction(luaBinaryComplete), "Completer for executables/binaries") + util.Document(L, hshcomp, "Completions interface for Hilbish.") + L.SetField(mod, "completion", hshcomp) + L.Push(mod) return 1 } +func luaFileComplete(L *lua.LState) int { + query := L.CheckString(1) + ctx := L.CheckString(2) + fields := L.CheckTable(3) + + var fds []string + fields.ForEach(func(k lua.LValue, v lua.LValue) { + fds = append(fds, v.String()) + }) + + completions := fileComplete(query, ctx, fds) + luaComps := L.NewTable() + + for _, comp := range completions { + luaComps.Append(lua.LString(comp)) + } + + L.Push(luaComps) + + return 1 +} + +func luaBinaryComplete(L *lua.LState) int { + query := L.CheckString(1) + ctx := L.CheckString(2) + fields := L.CheckTable(3) + + var fds []string + fields.ForEach(func(k lua.LValue, v lua.LValue) { + fds = append(fds, v.String()) + }) + + completions, _ := binaryComplete(query, ctx, fds) + luaComps := L.NewTable() + + for _, comp := range completions { + luaComps.Append(lua.LString(comp)) + } + + L.Push(luaComps) + + return 1 +} + func setVimMode(mode string) { hooks.Em.Emit("hilbish.vimMode", mode) util.SetField(l, hshMod, "vimMode", lua.LString(mode), "Current Vim mode of Hilbish (nil if not in Vim mode)") diff --git a/complete.go b/complete.go index c22fcc4..56009fb 100644 --- a/complete.go +++ b/complete.go @@ -15,7 +15,7 @@ func fileComplete(query, ctx string, fields []string) []string { completions, _ = matchPath(strings.Replace(query, "~", curuser.HomeDir, 1), query) } } - + if len(completions) == 0 && len(fields) > 1 { completions, _ = matchPath("./" + query, query) } @@ -23,6 +23,55 @@ func fileComplete(query, ctx string, fields []string) []string { return completions } +func binaryComplete(query, ctx string, fields []string) ([]string, string) { + var completions []string + + prefixes := []string{"./", "../", "/", "~/"} + for _, prefix := range prefixes { + if strings.HasPrefix(query, prefix) { + fileCompletions := fileComplete(query, ctx, fields) + if len(fileCompletions) != 0 { + for _, f := range fileCompletions { + name := strings.Replace(query + f, "~", curuser.HomeDir, 1) + if info, err := os.Stat(name); err == nil && info.Mode().Perm() & 0100 == 0 { + continue + } + completions = append(completions, f) + } + } + return completions, "" + } + } + + // filter out executables, but in path + for _, dir := range filepath.SplitList(os.Getenv("PATH")) { + // print dir to stderr for debugging + // 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 + if info, err := os.Stat(match); err == nil && info.Mode().Perm() & 0100 == 0 { + 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) + } + } + + return completions, query +} + func matchPath(path, pref string) ([]string, error) { var entries []string matches, err := filepath.Glob(path + "*") @@ -54,6 +103,6 @@ func matchPath(path, pref string) ([]string, error) { entries = append(entries, name) } } - + return entries, err } diff --git a/exec.go b/exec.go index a482f6a..fc96878 100644 --- a/exec.go +++ b/exec.go @@ -111,7 +111,7 @@ func execCommand(cmd, old string) error { NRet: 1, Protect: true, }, luacmdArgs) - + if err != nil { fmt.Fprintln(os.Stderr, "Error in command:\n\n" + err.Error()) diff --git a/rl.go b/rl.go index d3dc6ff..9db7331 100644 --- a/rl.go +++ b/rl.go @@ -3,9 +3,7 @@ package main import ( "fmt" "io" - "path/filepath" "strings" - "os" "github.com/maxlandon/readline" "github.com/yuin/gopher-lua" @@ -40,13 +38,8 @@ func newLineReader(prompt string) *lineReader { rl.TabCompleter = func(line []rune, pos int, _ readline.DelayedTabContext) (string, []*readline.CompletionGroup) { ctx := string(line) var completions []string - - compGroup := []*readline.CompletionGroup{ - &readline.CompletionGroup{ - TrimSlash: false, - NoSpace: true, - }, - } + + var compGroup []*readline.CompletionGroup ctx = strings.TrimLeft(ctx, " ") if len(ctx) == 0 { @@ -60,62 +53,28 @@ func newLineReader(prompt string) *lineReader { query := fields[len(fields) - 1] ctx = aliases.Resolve(ctx) - - if len(fields) == 1 { - prefixes := []string{"./", "../", "/", "~/"} - for _, prefix := range prefixes { - if strings.HasPrefix(query, prefix) { - fileCompletions := fileComplete(query, ctx, fields) - if len(fileCompletions) != 0 { - for _, f := range fileCompletions { - name := strings.Replace(query + f, "~", curuser.HomeDir, 1) - if info, err := os.Stat(name); err == nil && info.Mode().Perm() & 0100 == 0 { - continue - } - completions = append(completions, f) - } - compGroup[0].Suggestions = completions - } - return "", compGroup - } - } - // filter out executables, but in path - for _, dir := range filepath.SplitList(os.Getenv("PATH")) { - // print dir to stderr for debugging - // 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 - if info, err := os.Stat(match); err == nil && info.Mode().Perm() & 0100 == 0 { - continue - } - // get basename from match - name := filepath.Base(match) - // print name to stderr for debugging - // 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) - } - } - - compGroup[0].Suggestions = completions - return query, compGroup + if len(fields) == 1 { + completions, prefix := binaryComplete(query, ctx, fields) + + compGroup = append(compGroup, &readline.CompletionGroup{ + TrimSlash: false, + NoSpace: true, + Suggestions: completions, + }) + + return prefix, compGroup } else { if completecb, ok := luaCompletions["command." + fields[0]]; ok { + luaFields := l.NewTable() + for _, f := range fields { + luaFields.Append(lua.LString(f)) + } err := l.CallByParam(lua.P{ Fn: completecb, NRet: 1, Protect: true, - }) + }, lua.LString(query), lua.LString(ctx), luaFields) if err != nil { return "", compGroup @@ -124,88 +83,86 @@ func newLineReader(prompt string) *lineReader { luacompleteTable := l.Get(-1) l.Pop(1) + /* + as an example with git, + completion table should be structured like: + { + { + items = { + 'add', + 'clone', + 'init' + }, + type = 'grid' + }, + { + items = { + '-c', + '--git-dir' + }, + type = 'list' + } + } + ^ a table of completion groups. + it is the responsibility of the completer + to work on subcommands and subcompletions + */ if cmpTbl, ok := luacompleteTable.(*lua.LTable); ok { cmpTbl.ForEach(func(key lua.LValue, value lua.LValue) { - // if key is a number (index), we just check and complete that if key.Type() == lua.LTNumber { - // if we have only 2 fields then this is fine - if len(fields) == 2 { - if strings.HasPrefix(value.String(), fields[1]) { - completions = append(completions, value.String()) + // completion group + if value.Type() == lua.LTTable { + luaCmpGroup := value.(*lua.LTable) + compType := luaCmpGroup.RawGet(lua.LString("type")) + compItems := luaCmpGroup.RawGet(lua.LString("items")) + if compType.Type() != lua.LTString { + l.RaiseError("bad type name for completion (expected string, got %v)", compType.Type().String()) } - } - } else if key.Type() == lua.LTString { - if len(fields) == 2 { - if strings.HasPrefix(key.String(), fields[1]) { - completions = append(completions, key.String()) + if compItems.Type() != lua.LTTable { + l.RaiseError("bad items for completion (expected table, got %v)", compItems.Type().String()) } - } else { - // if we have more than 2 fields, we need to check if the key matches - // the current field and if it does, we need to check if the value is a string - // or table (nested sub completions) - if key.String() == fields[1] { - // if value is a table, we need to iterate over it - // and add each value to completions - // check if value is either a table or function - if value.Type() == lua.LTTable { - valueTbl := value.(*lua.LTable) - valueTbl.ForEach(func(key lua.LValue, value lua.LValue) { - val := value.String() - if val == "" { - // complete files - completions = append(completions, fileComplete(query, ctx, fields)...) - } else { - if strings.HasPrefix(val, query) { - completions = append(completions, val) - } - } - }) - } else if value.Type() == lua.LTFunction { - // if value is a function, we need to call it - // and add each value to completions - // completionsCtx is the context we pass to the function, - // removing 2 fields from the fields array - completionsCtx := strings.Join(fields[2:], " ") - err := l.CallByParam(lua.P{ - Fn: value, - NRet: 1, - Protect: true, - }, lua.LString(query), lua.LString(completionsCtx)) - - if err != nil { - return - } - - luacompleteTable := l.Get(-1) - l.Pop(1) - - // just check if its actually a table and add it to the completions - if cmpTbl, ok := luacompleteTable.(*lua.LTable); ok { - cmpTbl.ForEach(func(key lua.LValue, value lua.LValue) { - val := value.String() - if strings.HasPrefix(val, query) { - completions = append(completions, val) - } - }) - } + var items []string + itemDescriptions := make(map[string]string) + compItems.(*lua.LTable).ForEach(func(k lua.LValue, v lua.LValue) { + if k.Type() == lua.LTString { + // ['--flag'] = {'description', '--flag-alias'} + itm := v.(*lua.LTable) + items = append(items, k.String()) + itemDescriptions[k.String()] = itm.RawGet(lua.LNumber(1)).String() } else { - // throw lua error - // complete.cmdname: error message... - l.RaiseError("complete." + fields[0] + ": completion value is not a table or function") + items = append(items, v.String()) } + }) + + var dispType readline.TabDisplayType + switch compType.String() { + case "grid": dispType = readline.TabDisplayGrid + case "list": dispType = readline.TabDisplayList + // need special cases, will implement later + //case "map": dispType = readline.TabDisplayMap } + compGroup = append(compGroup, &readline.CompletionGroup{ + DisplayType: dispType, + Descriptions: itemDescriptions, + Suggestions: items, + TrimSlash: false, + NoSpace: true, + }) } } }) } } - if len(completions) == 0 { + if len(compGroup) == 0 { completions = fileComplete(query, ctx, fields) + compGroup = append(compGroup, &readline.CompletionGroup{ + TrimSlash: false, + NoSpace: true, + Suggestions: completions, + }) } } - - compGroup[0].Suggestions = completions return "", compGroup }