diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b47706e..b357b64 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,9 @@ jobs: goos: windows steps: - name: Checkout sources - uses: actions/checkout@v2 + uses: actions/checkout@v3 + with: + submodules: true - name: Setup Go uses: actions/setup-go@v2 with: @@ -29,10 +31,28 @@ jobs: - uses: actions/upload-artifact@v2 if: matrix.goos == 'windows' with: - name: hilbish-${{ matrix.goos }}-${{ matrix.goarch }}.exe - path: hilbish.exe + name: hilbish-${{ matrix.goos }}-${{ matrix.goarch }} + path: | + hilbish.exe + LICENSE + README.md + CHANGELOG.md + .hilbishrc.lua + nature + libs + docs + emmyLuaDocs - uses: actions/upload-artifact@v2 if: matrix.goos != 'windows' with: name: hilbish-${{ matrix.goos }}-${{ matrix.goarch }} - path: hilbish + path: | + hilbish + LICENSE + README.md + CHANGELOG.md + .hilbishrc.lua + nature + libs + docs + emmyLuaDocs diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..9d2728b --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,60 @@ +name: CodeQL +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '38 2 * * 4' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'go' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e19ef0b..a3a2840 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,13 +30,13 @@ jobs: - goarch: arm64 goos: windows steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: - submodules: recursive + submodules: true - uses: wangyoucao577/go-release-action@v1.25 with: github_token: ${{ secrets.GITHUB_TOKEN }} goos: ${{ matrix.goos }} goarch: ${{ matrix.goarch }} binary_name: hilbish - extra_files: LICENSE README.md CHANGELOG.md .hilbishrc.lua prelude libs docs emmyLuaDocs + extra_files: LICENSE README.md CHANGELOG.md .hilbishrc.lua nature libs docs emmyLuaDocs diff --git a/CHANGELOG.md b/CHANGELOG.md index 563eb7e..f350cff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,63 @@ # 🎀 Changelog +## Unreleased +### Added +- Inline hints, akin to fish and the others. +To make a handler for hint text, you can set the `hilbish.hinter` function. +For more info, look at its docs with the `doc hilbish` command. +- Syntax highlighting function. To make a handler for it, set +`hilbish.highlighter`. Same thing as the hinter, check `doc hilbish` for +more info/docs. +- Ctrl+K deletes from the cursor to the end of the line. ([#128](https://github.com/Rosettea/Hilbish/pull/128)) +- Alt+Backspace as an alternative of Ctrl+W to delete a word. ([#132](https://github.com/Rosettea/Hilbish/pull/132)) +- Enhanced timer API (`doc timers`) +- Don't exit until intervals are stopped/finished when running a non interactive script. +- Ctrl+D deletes character below cursor if line isn't empty instead of exiting. +- Ctrl+Delete to forward delete a word. +- Right prompt ([#140](https://github.com/Rosettea/Hilbish/pull/140)) +- Ctrl+_ to undo in Emacs input mode. +- Emacs style forward/backward word keybinds ([#139](https://github.com/Rosettea/Hilbish/pull/139)) +- `hilbish.completion.call` to call a completion handler (`doc completions`) +- `hilbish.completion.handler` to set a custom handler for completions. This +is for everything/anything as opposed to just adding a single command completion. +[#122](https://github.com/Rosettea/Hilbish/issues/122) +- `fs.abs(path)` to get absolute path. +- Nature module (`doc nature`) + +### Changed +- **Breaking Change:** Upgraded to Lua 5.4. +This is probably one of (if not the) biggest things in this release. +- **Breaking Change:** MacOS config paths now match Linux. +- Overrides on the `hilbish` table are no longer permitted. +- **Breaking Change:** Runner functions are now required to return 3 values: +user input, exit code, and error. User input has been added to the return to +account for runners wanting to prompt for continued input, and to add it +properly to history. + +### Fixed +- If in Vim replace mode, input at the end of the line inserts instead of +replacing the last character. +- Make forward delete work how its supposed to. +- Prompt refresh not working properly. +- Crashing on input in xterm. ([#131](https://github.com/Rosettea/Hilbish/pull/131)) +- Make delete key work on st ([#131](https://github.com/Rosettea/Hilbish/pull/131)) +- `hilbish.login` being the wrong value. +- Put full input in history if prompted for continued input +- Don't put alias expanded command in history (sound familiar?) +- Handle cases of stdin being nonblocking (in the case of [#130](https://github.com/Rosettea/Hilbish/issues/130)) +- Don't prompt for continued input if non interactive +- Don't insert unhandled control keys. +- Handle sh syntax error in alias +- Use invert for completion menu selection highlight instead of specific +colors. Brings an improvement on light themes, or themes that don't follow +certain color rules. +- Home/End keys now go to the actual start/end of the input. +- Input getting cut off on enter in certain cases. +- Go to the next line properly if input reaches end of terminal width. +- Cursor position with CJK characters. ([#145](https://github.com/Rosettea/Hilbish/pull/145)) +- Files with same name as parent folder in completions getting cut off [#136](https://github.com/Rosettea/Hilbish/issues/136)) +- `hilbish.which` now works with commanders and aliases. + ## [1.2.0] - 2022-03-17 ### Added - Job Management additions diff --git a/Makefile b/Makefile index 660bc58..a0c6580 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ build: install: install -v -d "$(DESTDIR)$(BINDIR)/" && install -m 0755 -v hilbish "$(DESTDIR)$(BINDIR)/hilbish" mkdir -p "$(DESTDIR)$(LIBDIR)" - cp -r libs docs emmyLuaDocs prelude .hilbishrc.lua "$(DESTDIR)$(LIBDIR)" + cp -r libs docs emmyLuaDocs nature .hilbishrc.lua "$(DESTDIR)$(LIBDIR)" grep -qxF "$(DESTDIR)$(BINDIR)/hilbish" /etc/shells || echo "$(DESTDIR)$(BINDIR)/hilbish" >> /etc/shells uninstall: diff --git a/aliases.go b/aliases.go index 2af6427..3007cc3 100644 --- a/aliases.go +++ b/aliases.go @@ -72,6 +72,7 @@ func (a *aliasHandler) Loader(rtm *rt.Runtime) *rt.Table { "add": util.LuaExport{hlalias, 2, false}, "list": util.LuaExport{a.luaList, 0, false}, "del": util.LuaExport{a.luaDelete, 1, false}, + "resolve": util.LuaExport{a.luaResolve, 1, false}, } mod := rt.NewTable() @@ -101,3 +102,16 @@ func (a *aliasHandler) luaDelete(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.Next(), nil } + +func (a *aliasHandler) luaResolve(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + alias, err := c.StringArg(0) + if err != nil { + return nil, err + } + resolved := a.Resolve(alias) + + return c.PushingNext1(t.Runtime, rt.StringValue(resolved)), nil +} diff --git a/api.go b/api.go index aa141d6..5510717 100644 --- a/api.go +++ b/api.go @@ -52,7 +52,47 @@ var hilbishLoader = packagelib.Loader{ } func hilbishLoad(rtm *rt.Runtime) (rt.Value, func()) { + fakeMod := rt.NewTable() + modmt := rt.NewTable() mod := rt.NewTable() + + modIndex := func(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + arg := c.Arg(1) + val := mod.Get(arg) + + return c.PushingNext1(t.Runtime, val), nil + } + modNewIndex := func(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + k, err := c.StringArg(1) + if err != nil { + return nil, err + } + + v := c.Arg(2) + if k == "highlighter" { + var err error + // fine to assign, since itll be either nil or a closure + highlighter, err = c.ClosureArg(2) + if err != nil { + return nil, errors.New("hilbish.highlighter has to be a function") + } + } else if k == "hinter" { + var err error + hinter, err = c.ClosureArg(2) + if err != nil { + return nil, errors.New("hilbish.hinter has to be a function") + } + } else if modVal := mod.Get(rt.StringValue(k)); modVal != rt.NilValue { + return nil, errors.New("not allowed to override in hilbish table") + } + mod.Set(rt.StringValue(k), v) + + return c.Next(), nil + } + modmt.Set(rt.StringValue("__newindex"), rt.FunctionValue(rt.NewGoFunction(modNewIndex, "__newindex", 3, false))) + modmt.Set(rt.StringValue("__index"), rt.FunctionValue(rt.NewGoFunction(modIndex, "__index", 2, false))) + fakeMod.SetMetatable(modmt) + util.SetExports(rtm, mod, exports) hshMod = mod @@ -67,17 +107,17 @@ func hilbishLoad(rtm *rt.Runtime) (rt.Value, func()) { The nice lil shell for {blue}Lua{reset} fanatics! Check out the {blue}{bold}guide{reset} command to get started. ` - util.SetField(rtm, mod, "ver", rt.StringValue(version), "Hilbish version") - util.SetField(rtm, mod, "user", rt.StringValue(username), "Username of user") - util.SetField(rtm, mod, "host", rt.StringValue(host), "Host name of the machine") - util.SetField(rtm, mod, "home", rt.StringValue(curuser.HomeDir), "Home directory of the user") - util.SetField(rtm, mod, "dataDir", rt.StringValue(dataDir), "Directory for Hilbish's data files") - util.SetField(rtm, mod, "interactive", rt.BoolValue(interactive), "If this is an interactive shell") - util.SetField(rtm, mod, "login", rt.BoolValue(login), "Whether this is a login shell") - util.SetField(rtm, mod, "greeting", rt.StringValue(greeting), "Hilbish's welcome message for interactive shells. It has Lunacolors formatting.") - util.SetField(rtm, mod, "vimMode", rt.NilValue, "Current Vim mode of Hilbish (nil if not in Vim mode)") - util.SetField(rtm, hshMod, "exitCode", rt.IntValue(0), "Exit code of last exected command") - util.Document(mod, "Hilbish's core API, containing submodules and functions which relate to the shell itself.") + util.SetFieldProtected(fakeMod, mod, "ver", rt.StringValue(version), "Hilbish version") + util.SetFieldProtected(fakeMod, mod, "user", rt.StringValue(username), "Username of user") + util.SetFieldProtected(fakeMod, mod, "host", rt.StringValue(host), "Host name of the machine") + util.SetFieldProtected(fakeMod, mod, "home", rt.StringValue(curuser.HomeDir), "Home directory of the user") + util.SetFieldProtected(fakeMod, mod, "dataDir", rt.StringValue(dataDir), "Directory for Hilbish's data files") + util.SetFieldProtected(fakeMod, mod, "interactive", rt.BoolValue(interactive), "If this is an interactive shell") + util.SetFieldProtected(fakeMod, mod, "login", rt.BoolValue(login), "Whether this is a login shell") + util.SetFieldProtected(fakeMod, mod, "greeting", rt.StringValue(greeting), "Hilbish's welcome message for interactive shells. It has Lunacolors formatting.") + util.SetFieldProtected(fakeMod, mod, "vimMode", rt.NilValue, "Current Vim mode of Hilbish (nil if not in Vim mode)") + util.SetFieldProtected(fakeMod, mod, "exitCode", rt.IntValue(0), "Exit code of last exected command") + util.Document(fakeMod, "Hilbish's core API, containing submodules and functions which relate to the shell itself.") // hilbish.userDir table hshuser := rt.NewTable() @@ -109,15 +149,7 @@ Check out the {blue}{bold}guide{reset} command to get started. util.Document(historyModule, "History interface for Hilbish.") // hilbish.completion table - hshcomp := rt.NewTable() - util.SetField(rtm, hshcomp, "files", - rt.FunctionValue(rt.NewGoFunction(luaFileComplete, "files", 3, false)), - "Completer for files") - - util.SetField(rtm, hshcomp, "bins", - rt.FunctionValue(rt.NewGoFunction(luaBinaryComplete, "bins", 3, false)), - "Completer for executables/binaries") - + hshcomp := completionLoader(rtm) util.Document(hshcomp, "Completions interface for Hilbish.") mod.Set(rt.StringValue("completion"), rt.TableValue(hshcomp)) @@ -137,7 +169,7 @@ Check out the {blue}{bold}guide{reset} command to get started. util.Document(timerModule, "Timer interface, for control of all intervals and timeouts.") mod.Set(rt.StringValue("timers"), rt.TableValue(timerModule)) - return rt.TableValue(mod), nil + return rt.TableValue(fakeMod), nil } func getenv(key, fallback string) string { @@ -148,75 +180,6 @@ func getenv(key, fallback string) string { return value } -func luaFileComplete(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { - query, ctx, fds, err := getCompleteParams(t, c) - if err != nil { - return nil, err - } - - completions, _ := fileComplete(query, ctx, fds) - luaComps := rt.NewTable() - - for i, comp := range completions { - luaComps.Set(rt.IntValue(int64(i + 1)), rt.StringValue(comp)) - } - - return c.PushingNext1(t.Runtime, rt.TableValue(luaComps)), nil -} - -func luaBinaryComplete(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { - query, ctx, fds, err := getCompleteParams(t, c) - if err != nil { - return nil, err - } - - completions, _ := binaryComplete(query, ctx, fds) - luaComps := rt.NewTable() - - for i, comp := range completions { - luaComps.Set(rt.IntValue(int64(i + 1)), rt.StringValue(comp)) - } - - return c.PushingNext1(t.Runtime, rt.TableValue(luaComps)), nil -} - -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 - nextVal := rt.NilValue - for { - next, val, ok := fields.Next(nextVal) - if next == rt.NilValue { - break - } - nextVal = next - - valStr, ok := val.TryString() - if !ok { - continue - } - - fds = append(fds, valStr) - } - - return query, ctx, fds, err -} - func setVimMode(mode string) { util.SetField(l, hshMod, "vimMode", rt.StringValue(mode), "Current Vim mode of Hilbish (nil if not in Vim mode)") hooks.Em.Emit("hilbish.vimMode", mode) @@ -395,21 +358,11 @@ func hlappendPath(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { // check if dir is a table or a string if arg.Type() == rt.TableType { - nextVal := rt.NilValue - for { - next, val, ok := arg.AsTable().Next(nextVal) - if next == rt.NilValue { - break + util.ForEach(arg.AsTable(), func(k rt.Value, v rt.Value) { + if v.Type() == rt.StringType { + appendPath(v.AsString()) } - nextVal = next - - valStr, ok := val.TryString() - if !ok { - continue - } - - appendPath(valStr) - } + }) } else if arg.Type() == rt.StringType { appendPath(arg.AsString()) } else { @@ -544,9 +497,7 @@ func hlinterval(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { // A `scope` is currently only expected to be `command.`, // replacing with the name of the command (for example `command.git`). // `cb` must be a function that returns a table of "completion groups." -// A completion group is a table with the keys `items` and `type`. -// `items` being a table of items and `type` being the display type of -// `grid` (the normal file completion display) or `list` (with a description) +// Check `doc completions` for more information. // --- @param scope string // --- @param cb function func hlcomplete(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { @@ -581,18 +532,27 @@ func hlprependPath(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.Next(), nil } -// which(binName) -// Searches for an executable called `binName` in the directories of $PATH +// which(name) +// Checks if `name` is a valid command // --- @param binName string func hlwhich(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err } - binName, err := c.StringArg(0) + name, err := c.StringArg(0) if err != nil { return nil, err } - path, err := exec.LookPath(binName) + + cmd := aliases.Resolve(name) + + // check for commander + if commands[cmd] != nil { + // they dont resolve to a path, so just send the cmd + return c.PushingNext1(t.Runtime, rt.StringValue(cmd)), nil + } + + path, err := exec.LookPath(cmd) if err != nil { return c.Next(), nil } @@ -601,7 +561,7 @@ func hlwhich(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { } // inputMode(mode) -// Sets the input mode for Hilbish's line reader. Accepts either emacs for vim +// Sets the input mode for Hilbish's line reader. Accepts either emacs or vim // --- @param mode string func hlinputMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { @@ -642,7 +602,6 @@ func hlrunnerMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { switch mode.Type() { case rt.StringType: switch mode.AsString() { - // no fallthrough doesnt work so eh case "hybrid", "hybridRev", "lua", "sh": runnerMode = mode default: return nil, errors.New("execMode: expected either a function or hybrid, hybridRev, lua, sh. Received " + mode.AsString()) } @@ -653,40 +612,24 @@ func hlrunnerMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.Next(), nil } -// hinter(cb) -// Sets the hinter function. This will be called on every key insert to determine -// what text to use as an inline hint. The callback is passed 2 arguments: -// the current line and the position. It is expected to return a string -// which will be used for the hint. -// --- @param cb function +// hinter(line, pos) +// The command line hint handler. It gets called on every key insert to +// determine what text to use as an inline hint. It is passed the current +// line and cursor position. It is expected to return a string which is used +// as the text for the hint. This is by default a shim. To set hints, +// override this function with your custom handler. +// --- @param line string +// --- @param pos int func hlhinter(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { - if err := c.Check1Arg(); err != nil { - return nil, err - } - hinterCb, err := c.ClosureArg(0) - if err != nil { - return nil, err - } - hinter = hinterCb - - return c.Next(), err + return c.Next(), nil } -// highlighter(cb) -// Sets the highlighter function. This is mainly for syntax hightlighting, but in -// reality could set the input of the prompt to display anything. The callback -// is passed the current line as typed and is expected to return a line that will -// be used to display in the line. -// --- @param cb function +// highlighter(line) +// Line highlighter handler. This is mainly for syntax highlighting, but in +// reality could set the input of the prompt to *display* anything. The +// callback is passed the current line and is expected to return a line that +// will be used as the input display. +// --- @param line string func hlhighlighter(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { - if err := c.Check1Arg(); err != nil { - return nil, err - } - highlighterCb, err := c.ClosureArg(0) - if err != nil { - return nil, err - } - highlighter = highlighterCb - - return c.Next(), err + return c.Next(), nil } diff --git a/complete.go b/complete.go index e83fa33..1c7cfe2 100644 --- a/complete.go +++ b/complete.go @@ -1,9 +1,14 @@ package main import ( + "errors" "path/filepath" "strings" "os" + + "hilbish/util" + + rt "github.com/arnodel/golua/runtime" ) func fileComplete(query, ctx string, fields []string) ([]string, string) { @@ -19,7 +24,7 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) { fileCompletions, filePref := matchPath(query) if len(fileCompletions) != 0 { for _, f := range fileCompletions { - fullPath, _ := filepath.Abs(expandHome(query + strings.TrimPrefix(f, filePref))) + fullPath, _ := filepath.Abs(util.ExpandHome(query + strings.TrimPrefix(f, filePref))) if err := findExecutable(fullPath, false, true); err != nil { continue } @@ -66,7 +71,7 @@ func matchPath(query string) ([]string, string) { var entries []string var baseName string - path, _ := filepath.Abs(expandHome(filepath.Dir(query))) + path, _ := filepath.Abs(util.ExpandHome(filepath.Dir(query))) if string(query) == "" { // filepath base below would give us "." // which would cause a match of only dotfiles @@ -112,3 +117,119 @@ func escapeFilename(fname string) string { return r.Replace(fname) } +func completionLoader(rtm *rt.Runtime) *rt.Table { + exports := map[string]util.LuaExport{ + "files": {luaFileComplete, 3, false}, + "bins": {luaBinaryComplete, 3, false}, + "call": {callLuaCompleter, 4, false}, + "handler": {completionHandler, 2, false}, + } + + mod := rt.NewTable() + util.SetExports(rtm, mod, exports) + + return mod +} + +// left as a shim, might doc in the same way as hilbish functions +func completionHandler(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + return c.Next(), nil +} + +func callLuaCompleter(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + 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 + completerReturn, err := rt.Call1(l.MainThread(), + rt.FunctionValue(completecb), rt.StringValue(query), + rt.StringValue(ctx), rt.TableValue(fields)) + + if err != nil { + return nil, err + } + + return c.PushingNext1(t.Runtime, completerReturn), nil +} + +func luaFileComplete(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + query, ctx, fds, err := getCompleteParams(t, c) + if err != nil { + return nil, err + } + + completions, pfx := fileComplete(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 +} + +func luaBinaryComplete(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 +} + +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 +} diff --git a/docs/completions.txt b/docs/completions.txt new file mode 100644 index 0000000..1354dc0 --- /dev/null +++ b/docs/completions.txt @@ -0,0 +1,44 @@ +Hilbish has a pretty good completion system. It has a nice looking menu, +with 2 types of menus: grid (like file completions) or list. + +Like most parts of Hilbish, it's made to be extensible and customizable. +The default handler for completions in general can be overwritten to provide +more advanced completions if needed. + +# Completion Handler +By default, it provides 3 things: for the first argument, binaries (with a +plain name requested to complete, those in $PATH), files, or command +completions. With the default completion handler, it will try to run a +handler for the command or fallback to file completions. + +To overwrite it, just assign a function to `hilbish.completion.handler` +like so: +function hilbish.completion.handler(line, pos) + -- do things +end +It is passed 2 arguments, the entire line, and the current cursor position. +The functions in the completion interface take 3 arguments: query, ctx, +and fields. The `query`, which what the user is currently trying to complete, +`ctx`, being just the entire line, and `fields` being a table of arguments. +It's just `ctx` split up, delimited by spaces. +It's expected to return 2 things: a table of completion groups, and a prefix. +A completion group is defined as a table with 2 keys: `items` and `type`. +The `items` field is just a table of items to use for completions. +The `type` is for the completion menu type, being either `grid` or `list`. +The prefix is what all the completions start with. It should be empty +if the user doesn't have a query. If the beginning of the completion +item does not match the prefix, it will be replaced and fixed properly +in the line. It is case sensitive. + +If you want to overwrite the functionality of the general completion handler, +or make your command completion have files as well (and filter them), +then there is the `files` function, which is mentioned below. + +# Completion Interface +## Functions +- `files(query, ctx, fields)` -> table, prefix: get file completions, based +on the user's query. +- `bins(query, ctx, fields)` -> table, prefix: get binary/executable +completions, based on user query. +- `call(scope, query, ctx, fields)` -> table, prefix: call a completion handler +with `scope`, usually being in the form of `command.` diff --git a/docs/fs.txt b/docs/fs.txt index 648737e..e9f1548 100644 --- a/docs/fs.txt +++ b/docs/fs.txt @@ -1,3 +1,5 @@ +abs(path) > Gives an absolute version of `path`. + cd(dir) > Changes directory to `dir` mkdir(name, recursive) > Makes a directory called `name`. If `recursive` is true, it will create its parent directories. diff --git a/docs/hilbish.txt b/docs/hilbish.txt index e86af79..d9763a0 100644 --- a/docs/hilbish.txt +++ b/docs/hilbish.txt @@ -6,9 +6,7 @@ complete(scope, cb) > Registers a completion handler for `scope`. A `scope` is currently only expected to be `command.`, replacing with the name of the command (for example `command.git`). `cb` must be a function that returns a table of "completion groups." -A completion group is a table with the keys `items` and `type`. -`items` being a table of items and `type` being the display type of -`grid` (the normal file completion display) or `list` (with a description) +Check `doc completions` for more information. cwd() > Returns the current directory of the shell @@ -16,17 +14,18 @@ exec(cmd) > Replaces running hilbish with `cmd` goro(fn) > Puts `fn` in a goroutine -highlighter(cb) > Sets the highlighter function. This is mainly for syntax hightlighting, but in -reality could set the input of the prompt to display anything. The callback -is passed the current line as typed and is expected to return a line that will -be used to display in the line. +highlighter(line) > Line highlighter handler. This is mainly for syntax highlighting, but in +reality could set the input of the prompt to *display* anything. The +callback is passed the current line and is expected to return a line that +will be used as the input display. -hinter(cb) > Sets the hinter function. This will be called on every key insert to determine -what text to use as an inline hint. The callback is passed 2 arguments: -the current line and the position. It is expected to return a string -which will be used for the hint. +hinter(line, pos) > The command line hint handler. It gets called on every key insert to +determine what text to use as an inline hint. It is passed the current +line and cursor position. It is expected to return a string which is used +as the text for the hint. This is by default a shim. To set hints, +override this function with your custom handler. -inputMode(mode) > Sets the input mode for Hilbish's line reader. Accepts either emacs for vim +inputMode(mode) > Sets the input mode for Hilbish's line reader. Accepts either emacs or vim interval(cb, time) > Runs the `cb` function every `time` milliseconds. Returns a `timer` object (see `doc timers`). @@ -59,5 +58,5 @@ will call it to execute user input instead. timeout(cb, time) > Runs the `cb` function after `time` in milliseconds Returns a `timer` object (see `doc timers`). -which(binName) > Searches for an executable called `binName` in the directories of $PATH +which(name) > Checks if `name` is a valid command diff --git a/docs/hooks/hilbish.txt b/docs/hooks/hilbish.txt index 7a438f6..d6d5542 100644 --- a/docs/hooks/hilbish.txt +++ b/docs/hooks/hilbish.txt @@ -4,4 +4,4 @@ `modeName` is the name of the mode changed to (can be `insert`, `normal`, `delete` or `replace`). + `hilbish.vimAction` -> actionName, args > Sent when the user does a "vim action," being something -like yanking or pasting text. See `doc vimMode actions` for more info. +like yanking or pasting text. See `doc vim-mode actions` for more info. diff --git a/docs/nature.txt b/docs/nature.txt new file mode 100644 index 0000000..6c29cbf --- /dev/null +++ b/docs/nature.txt @@ -0,0 +1,13 @@ +A bit after creation, we have the outside nature. Little plants, seeds, +growing to their final phase: a full plant. A lot of Hilbish itself is +written in Go, but there are parts made in Lua, being the `doc` command, +command not executable/found hooks to print a message in the shell, +and other things. + +Hilbish's Lua core module is called `nature`. It's handled after everything +on the Go side initializes, which is what that first sentence was from. + +# Nature Modules +Currently, `nature` provides 1 intended public module: `nature.dirs`. +It is a simple API for managing recent directories and old +current working directory. diff --git a/emmyLuaDocs/fs.lua b/emmyLuaDocs/fs.lua index f94fd2b..cd82d5c 100644 --- a/emmyLuaDocs/fs.lua +++ b/emmyLuaDocs/fs.lua @@ -2,6 +2,10 @@ local fs = {} +--- Gives an absolute version of `path`. +--- @param path string +function fs.abs(path) end + --- Changes directory to `dir` --- @param dir string function fs.cd(dir) end diff --git a/emmyLuaDocs/hilbish.lua b/emmyLuaDocs/hilbish.lua index f8c4380..ca34425 100644 --- a/emmyLuaDocs/hilbish.lua +++ b/emmyLuaDocs/hilbish.lua @@ -15,9 +15,7 @@ function hilbish.appendPath(dir) end --- A `scope` is currently only expected to be `command.`, --- replacing with the name of the command (for example `command.git`). --- `cb` must be a function that returns a table of "completion groups." ---- A completion group is a table with the keys `items` and `type`. ---- `items` being a table of items and `type` being the display type of ---- `grid` (the normal file completion display) or `list` (with a description) +--- Check `doc completions` for more information. --- @param scope string --- @param cb function function hilbish.complete(scope, cb) end @@ -33,21 +31,23 @@ function hilbish.exec(cmd) end --- @param fn function function hilbish.goro(fn) end ---- Sets the highlighter function. This is mainly for syntax hightlighting, but in ---- reality could set the input of the prompt to display anything. The callback ---- is passed the current line as typed and is expected to return a line that will ---- be used to display in the line. ---- @param cb function -function hilbish.highlighter(cb) end +--- Line highlighter handler. This is mainly for syntax highlighting, but in +--- reality could set the input of the prompt to *display* anything. The +--- callback is passed the current line and is expected to return a line that +--- will be used as the input display. +--- @param line string +function hilbish.highlighter(line) end ---- Sets the hinter function. This will be called on every key insert to determine ---- what text to use as an inline hint. The callback is passed 2 arguments: ---- the current line and the position. It is expected to return a string ---- which will be used for the hint. ---- @param cb function -function hilbish.hinter(cb) end +--- The command line hint handler. It gets called on every key insert to +--- determine what text to use as an inline hint. It is passed the current +--- line and cursor position. It is expected to return a string which is used +--- as the text for the hint. This is by default a shim. To set hints, +--- override this function with your custom handler. +--- @param line string +--- @param pos int +function hilbish.hinter(line, pos) end ---- Sets the input mode for Hilbish's line reader. Accepts either emacs for vim +--- Sets the input mode for Hilbish's line reader. Accepts either emacs or vim --- @param mode string function hilbish.inputMode(mode) end @@ -103,7 +103,7 @@ function hilbish.runnerMode(mode) end --- @return table function hilbish.timeout(cb, time) end ---- Searches for an executable called `binName` in the directories of $PATH +--- Checks if `name` is a valid command --- @param binName string function hilbish.which(binName) end diff --git a/exec.go b/exec.go index 90498ab..9d394aa 100644 --- a/exec.go +++ b/exec.go @@ -25,8 +25,63 @@ import ( ) var errNotExec = errors.New("not executable") +var errNotFound = errors.New("not found") var runnerMode rt.Value = rt.StringValue("hybrid") +type execError struct{ + typ string + cmd string + code int + colon bool + err error +} + +func (e execError) Error() string { + return fmt.Sprintf("%s: %s", e.cmd, e.typ) +} + +func (e execError) sprint() error { + sep := " " + if e.colon { + sep = ": " + } + + return fmt.Errorf("hilbish: %s%s%s", e.cmd, sep, e.err.Error()) +} + +func isExecError(err error) (execError, bool) { + if exErr, ok := err.(execError); ok { + return exErr, true + } + + fields := strings.Split(err.Error(), ": ") + knownTypes := []string{ + "not-found", + "not-executable", + } + + if len(fields) > 1 && contains(knownTypes, fields[1]) { + var colon bool + var e error + switch fields[1] { + case "not-found": + e = errNotFound + case "not-executable": + colon = true + e = errNotExec + } + + return execError{ + cmd: fields[0], + typ: fields[1], + colon: colon, + err: e, + }, true + } + + return execError{}, false +} + func runInput(input string, priv bool) { running = true cmdString := aliases.Resolve(input) @@ -43,10 +98,6 @@ func runInput(input string, priv bool) { return } input, exitCode, err = handleSh(input) - if err != nil { - fmt.Fprintln(os.Stderr, err) - } - cmdFinish(exitCode, input, priv) case "hybridRev": _, _, err = handleSh(input) if err == nil { @@ -54,38 +105,25 @@ func runInput(input string, priv bool) { return } input, exitCode, err = handleLua(cmdString) - if err != nil { - fmt.Fprintln(os.Stderr, err) - } - cmdFinish(exitCode, input, priv) case "lua": input, exitCode, err = handleLua(cmdString) - if err != nil { - fmt.Fprintln(os.Stderr, err) - } - cmdFinish(exitCode, input, priv) case "sh": input, exitCode, err = handleSh(input) - if err != nil { - fmt.Fprintln(os.Stderr, err) - } - cmdFinish(exitCode, input, priv) } } else { // can only be a string or function so - term := rt.NewTerminationWith(l.MainThread().CurrentCont(), 2, false) - err := rt.Call(l.MainThread(), runnerMode, []rt.Value{rt.StringValue(cmdString)}, term) + term := rt.NewTerminationWith(l.MainThread().CurrentCont(), 3, false) + err = rt.Call(l.MainThread(), runnerMode, []rt.Value{rt.StringValue(cmdString)}, term) if err != nil { fmt.Fprintln(os.Stderr, err) cmdFinish(124, input, priv) return } - luaexitcode := term.Get(0) - runErr := term.Get(1) - luaInput := term.Get(1) + luaInput := term.Get(0) + luaexitcode := term.Get(1) + runErr := term.Get(2) - var exitCode uint8 if code, ok := luaexitcode.TryInt(); ok { exitCode = uint8(code) } @@ -94,11 +132,19 @@ func runInput(input string, priv bool) { input = inp } - if runErr != rt.NilValue { - fmt.Fprintln(os.Stderr, runErr) + if errStr, ok := runErr.TryString(); ok { + err = fmt.Errorf("%s", errStr) } - cmdFinish(exitCode, input, priv) } + + if err != nil { + if exErr, ok := isExecError(err); ok { + hooks.Em.Emit("command." + exErr.typ, exErr.cmd) + err = exErr.sprint() + } + fmt.Fprintln(os.Stderr, err) + } + cmdFinish(exitCode, input, priv) } func handleLua(cmdString string) (string, uint8, error) { @@ -255,12 +301,20 @@ func execHandle(bg bool) interp.ExecHandlerFunc { err := lookpath(args[0]) if err == errNotExec { - hooks.Em.Emit("command.no-perm", args[0]) - hooks.Em.Emit("command.not-executable", args[0]) - return interp.NewExitStatus(126) + return execError{ + typ: "not-executable", + cmd: args[0], + code: 126, + colon: true, + err: errNotExec, + } } else if err != nil { - hooks.Em.Emit("command.not-found", args[0]) - return interp.NewExitStatus(127) + return execError{ + typ: "not-found", + cmd: args[0], + code: 127, + err: errNotFound, + } } killTimeout := 2 * time.Second diff --git a/golibs/fs/fs.go b/golibs/fs/fs.go index 042f2a8..950e966 100644 --- a/golibs/fs/fs.go +++ b/golibs/fs/fs.go @@ -1,6 +1,7 @@ package fs import ( + "path/filepath" "strconv" "os" "strings" @@ -22,13 +23,14 @@ func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) { "mkdir": util.LuaExport{fmkdir, 2, false}, "stat": util.LuaExport{fstat, 1, false}, "readdir": util.LuaExport{freaddir, 1, false}, + "abs": util.LuaExport{fabs, 1, false}, } mod := rt.NewTable() util.SetExports(rtm, mod, exports) util.Document(mod, `The fs module provides easy and simple access to filesystem functions and other things, and acts an -addition to the Lua standard library's I/O and fs functions.`) +addition to the Lua standard library's I/O and filesystem functions.`) return rt.TableValue(mod), nil } @@ -44,8 +46,9 @@ func fcd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err != nil { return nil, err } + path = util.ExpandHome(strings.TrimSpace(path)) - err = os.Chdir(strings.TrimSpace(path)) + err = os.Chdir(path) if err != nil { return nil, err } @@ -61,7 +64,7 @@ func fmkdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.CheckNArgs(2); err != nil { return nil, err } - dirname, err := c.StringArg(0) + path, err := c.StringArg(0) if err != nil { return nil, err } @@ -69,7 +72,7 @@ func fmkdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err != nil { return nil, err } - path := strings.TrimSpace(dirname) + path = util.ExpandHome(strings.TrimSpace(path)) if recursive { err = os.MkdirAll(path, 0744) @@ -94,6 +97,7 @@ func fstat(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err != nil { return nil, err } + path = util.ExpandHome(path) pathinfo, err := os.Stat(path) if err != nil { @@ -120,6 +124,7 @@ func freaddir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err != nil { return nil, err } + dir = util.ExpandHome(dir) names := rt.NewTable() dirEntries, err := os.ReadDir(dir) @@ -132,3 +137,21 @@ func freaddir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.PushingNext1(t.Runtime, rt.TableValue(names)), nil } + +// abs(path) +// Gives an absolute version of `path`. +// --- @param path string +func fabs(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + path, err := c.StringArg(0) + if err != nil { + return nil, err + } + path = util.ExpandHome(path) + + abspath, err := filepath.Abs(path) + if err != nil { + return nil, err + } + + return c.PushingNext1(t.Runtime, rt.StringValue(abspath)), nil +} diff --git a/lua.go b/lua.go index 3b925c6..8f3c0fb 100644 --- a/lua.go +++ b/lua.go @@ -51,14 +51,16 @@ func luaInit() { // Add more paths that Lua can require from err := util.DoString(l, "package.path = package.path .. " + requirePaths) if err != nil { - fmt.Fprintln(os.Stderr, "Could not add preload paths! Libraries will be missing. This shouldn't happen.") + fmt.Fprintln(os.Stderr, "Could not add Hilbish require paths! Libraries will be missing. This shouldn't happen.") } - err = util.DoFile(l, "prelude/init.lua") - if err != nil { - err = util.DoFile(l, preloadPath) - if err != nil { - fmt.Fprintln(os.Stderr, "Missing preload file, builtins may be missing.") + err1 := util.DoFile(l, "nature/init.lua") + if err1 != nil { + err2 := util.DoFile(l, preloadPath) + if err2 != nil { + fmt.Fprintln(os.Stderr, "Missing nature module, some functionality and builtins will be missing.") + fmt.Fprintln(os.Stderr, "local error:", err1) + fmt.Fprintln(os.Stderr, "global install error:", err2) } } } diff --git a/main.go b/main.go index ff04430..c0fce06 100644 --- a/main.go +++ b/main.go @@ -56,13 +56,13 @@ func main() { defaultConfDir = filepath.Join(confDir, "hilbish") } else { // else do ~ substitution - defaultConfDir = filepath.Join(expandHome(defaultConfDir), "hilbish") + defaultConfDir = filepath.Join(util.ExpandHome(defaultConfDir), "hilbish") } defaultConfPath = filepath.Join(defaultConfDir, "init.lua") if defaultHistDir == "" { defaultHistDir = filepath.Join(userDataDir, "hilbish") } else { - defaultHistDir = expandHome(defaultHistDir) + defaultHistDir = filepath.Join(util.ExpandHome(defaultHistDir), "hilbish") } defaultHistPath = filepath.Join(defaultHistDir, ".hilbish-history") helpflag := getopt.BoolLong("help", 'h', "Prints Hilbish flags") @@ -273,11 +273,6 @@ func handleHistory(cmd string) { // TODO: load history again (history shared between sessions like this ye) } -func expandHome(path string) string { - homedir := curuser.HomeDir - return strings.Replace(path, "~", homedir, 1) -} - func removeDupes(slice []string) []string { all := make(map[string]bool) newSlice := []string{} diff --git a/nature/commands/cd.lua b/nature/commands/cd.lua new file mode 100644 index 0000000..b4d1041 --- /dev/null +++ b/nature/commands/cd.lua @@ -0,0 +1,35 @@ +local bait = require 'bait' +local commander = require 'commander' +local fs = require 'fs' +local dirs = require 'nature.dirs' + +dirs.old = hilbish.cwd() +commander.register('cd', function (args) + if #args > 1 then + print("cd: too many arguments") + return 1 + elseif #args > 0 then + local path = args[1]:gsub('$%$','\0'):gsub('${([%w_]+)}', os.getenv) + :gsub('$([%w_]+)', os.getenv):gsub('%z','$'):gsub('^%s*(.-)%s*$', '%1') + + if path == '-' then + path = dirs.old + print(path) + end + dirs.setOld(hilbish.cwd()) + dirs.push(path) + + local ok, err = pcall(function() fs.cd(path) end) + if not ok then + print(err) + return 1 + end + bait.throw('cd', path) + + return + end + fs.cd(hilbish.home) + bait.throw('cd', hilbish.home) + + dirs.push(hilbish.home) +end) diff --git a/nature/commands/cdr.lua b/nature/commands/cdr.lua new file mode 100644 index 0000000..0438e6f --- /dev/null +++ b/nature/commands/cdr.lua @@ -0,0 +1,39 @@ +local commander = require 'commander' +local fs = require 'fs' +local lunacolors = require 'lunacolors' +local dirs = require 'nature.dirs' + +commander.register('cdr', function(args) + if not args[1] then + print(lunacolors.format [[ +cdr: change directory to one which has been recently visied + +usage: cdr + +to get a list of recent directories, use {green}{underline}cdr list{reset}]]) + return + end + + if args[1] == 'list' then + local recentDirs = dirs.recentDirs + if #recentDirs == 0 then + print 'No directories have been visited.' + return 1 + end + print(table.concat(recentDirs, '\n')) + return + end + + local index = tonumber(args[1]) + if not index then + print(string.format('Received %s as index, which isn\'t a number.', index)) + return 1 + end + + if not dirs.recent(index) then + print(string.format('No recent directory found at index %s.', index)) + return 1 + end + + fs.cd(dirs.recent(index)) +end) diff --git a/nature/commands/doc.lua b/nature/commands/doc.lua new file mode 100644 index 0000000..34e81ae --- /dev/null +++ b/nature/commands/doc.lua @@ -0,0 +1,93 @@ +local commander = require 'commander' +local fs = require 'fs' +local lunacolors = require 'lunacolors' + +commander.register('doc', function(args) + local moddocPath = hilbish.dataDir .. '/docs/' + local modDocFormat = [[ +%s +%s +# Functions +]] + + if #args > 0 then + local mod = args[1] + + local f = io.open(moddocPath .. mod .. '.txt', 'rb') + local funcdocs = nil + if not f then + -- assume subdir + -- dataDir/docs//.txt + moddocPath = moddocPath .. mod .. '/' + local subdocName = args[2] + if not subdocName then + subdocName = 'index' + end + f = io.open(moddocPath .. subdocName .. '.txt', 'rb') + if not f then + print('No documentation found for ' .. mod .. '.') + return + end + funcdocs = f:read '*a' + local moddocs = table.filter(fs.readdir(moddocPath), function(f) return f ~= 'index.txt' end) + local subdocs = table.map(moddocs, function(fname) + return lunacolors.underline(lunacolors.blue(string.gsub(fname, '.txt', ''))) + end) + if subdocName == 'index' then + funcdocs = funcdocs .. '\nSubdocs: ' .. table.concat(subdocs, ', ') + end + end + + if not funcdocs then + funcdocs = f:read '*a' + end + local desc = '' + local ok = pcall(require, mod) + local backtickOccurence = 0 + local formattedFuncs = lunacolors.format(funcdocs:sub(1, #funcdocs - 1):gsub('`', function() + backtickOccurence = backtickOccurence + 1 + if backtickOccurence % 2 == 0 then + return '{reset}' + else + return '{underline}{green}' + end + end)) + + if ok then + local props = {} + local propstr = '' + local modDesc = '' + local modmt = getmetatable(require(mod)) + modDesc = modmt.__doc + if modmt.__docProp then + -- not all modules have docs for properties + props = table.map(modmt.__docProp, function(v, k) + return lunacolors.underline(lunacolors.blue(k)) .. ' > ' .. v + end) + end + if #props > 0 then + propstr = '\n# Properties\n' .. table.concat(props, '\n') .. '\n' + end + desc = string.format(modDocFormat, modDesc, propstr) + end + print(desc .. formattedFuncs) + f:close() + + return + end + local modules = table.map(fs.readdir(moddocPath), function(f) + return lunacolors.underline(lunacolors.blue(string.gsub(f, '.txt', ''))) + end) + + io.write [[ +Welcome to Hilbish's doc tool! Here you can find documentation for builtin +functions and other things. + +Usage: doc
[subdoc] +A section is a module or a literal section and a subdoc is a subsection for it. + +Available sections: ]] + io.flush() + + print(table.concat(modules, ', ')) +end) diff --git a/nature/commands/exit.lua b/nature/commands/exit.lua new file mode 100644 index 0000000..421730c --- /dev/null +++ b/nature/commands/exit.lua @@ -0,0 +1,7 @@ +local bait = require 'bait' +local commander = require 'commander' + +commander.register('exit', function() + bait.throw('hilbish.exit') + os.exit(0) +end) diff --git a/nature/commands/guide.lua b/nature/commands/guide.lua new file mode 100644 index 0000000..1c1e346 --- /dev/null +++ b/nature/commands/guide.lua @@ -0,0 +1,54 @@ +local ansikit = require 'ansikit' +local commander = require 'commander' + +local helpTexts = { +[[ +Hello there! Welcome to Hilbish, the comfy and nice little shell for +Lua users and fans. Hilbish is configured with Lua, and its +scripts are also in Lua. It also runs both Lua and shell script when +interactive (aka normal usage). +]], +[[ +What does that mean for you, the user? It means that if you prefer to +use Lua for scripting instead of shell script but still have ordinary +shell usage for interactive use. +]], +[[ +If this is your first time using Hilbish and Lua, check out the +Programming in Lua book here: https://www.lua.org/pil +After (or if you already know Lua) check out the doc command. +It is an in shell tool for documentation about Hilbish provided +functions and modules. +]], +[[ +If you've updated from a pre-1.0 version (0.7.1 as an example) +you'll want to move your config from ~/.hilbishrc.lua to +]] .. +hilbish.userDir.config .. '/hilbish/init.lua' .. +[[ + +and also change all global functions (prompt, alias) to be +in the hilbish module (hilbish.prompt, hilbish.alias as examples). + +And if this is your first time (most likely), you can copy a config +from ]] .. hilbish.dataDir, +[[ +Since 1.0 is a big release, you'll want to check the changelog +at https://github.com/Rosettea/Hilbish/releases/tag/v1.0.0 +to find more breaking changes. +]] +} +commander.register('guide', function() + ansikit.clear() + ansikit.cursorTo(0, 0) + for _, text in ipairs(helpTexts) do + print(text) + local out = hilbish.read('Hit enter to continue ') + ansikit.clear() + ansikit.cursorTo(0, 0) + if not out then + return + end + end + print 'Hope you enjoy using Hilbish!' +end) diff --git a/nature/commands/init.lua b/nature/commands/init.lua new file mode 100644 index 0000000..e824c7c --- /dev/null +++ b/nature/commands/init.lua @@ -0,0 +1,6 @@ +-- Add command builtins +require 'nature.commands.cd' +require 'nature.commands.cdr' +require 'nature.commands.doc' +require 'nature.commands.exit' +require 'nature.commands.guide' diff --git a/nature/completions.lua b/nature/completions.lua new file mode 100644 index 0000000..d20cc59 --- /dev/null +++ b/nature/completions.lua @@ -0,0 +1,40 @@ +function hilbish.completion.handler(line, pos) + if type(line) ~= 'string' then error '#1 must be a string' end + if type(pos) ~= 'number' then error '#2 must be a number' end + + -- trim leading whitespace + local ctx = line:gsub('^%s*(.-)$', '%1') + if ctx:len() == 0 then return {}, '' end + + local res = hilbish.aliases.resolve(ctx) + local resFields = string.split(res, ' ') + local fields = string.split(ctx, ' ') + if #fields > 1 and #resFields > 1 then + fields = resFields + end + local query = fields[#fields] + + if #fields == 1 then + local comps, pfx = hilbish.completion.bins(query, ctx, fields) + local compGroup = { + items = comps, + type = 'grid' + } + + return {compGroup}, pfx + else + local ok, compGroups, pfx = pcall(hilbish.completion.call, + 'command.' .. #fields[1], query, ctx, fields) + if ok then + return compGroups, pfx + end + + local comps, pfx = hilbish.completion.files(query, ctx, fields) + local compGroup = { + items = comps, + type = 'grid' + } + + return {compGroup}, pfx + end +end diff --git a/nature/dirs.lua b/nature/dirs.lua new file mode 100644 index 0000000..5b7ec86 --- /dev/null +++ b/nature/dirs.lua @@ -0,0 +1,75 @@ +local fs = require 'fs' + +local dirs = {} + +--- Last (current working) directory. Separate from recentDirs mainly for +--- easier use. +dirs.old = '' +--- Table of recent directories. For use, look at public functions. +dirs.recentDirs = {} +--- Size of the recentDirs table. +dirs.recentSize = 10 + +--- Get (and remove) a `num` of entries from recent directories. +--- @param num number +--- @param remove boolean Whether to remove items +function dirRecents(num, remove) + num = num or 1 + local entries = {} + + if #dirs.recentDirs ~= 0 then + for i = 1, num do + local idx = remove and 1 or i + if not dirs.recentDirs[idx] then break end + table.insert(entries, dirs.recentDirs[idx]) + if remove then table.remove(dirs.recentDirs, 1) end + end + end + + if #entries == 1 then + return entries[1] + end + + return entries +end + +--- Look at `num` amount of recent directories, starting from the latest. +--- @param num? number +function dirs.peak(num) + return dirRecents(num) +end + +--- Add `d` to the recent directories. +function dirs.push(d) + dirs.recentDirs[dirs.recentSize + 1] = nil + if dirs.recentDirs[#dirs.recentDirs - 1] ~= d then + ok, d = pcall(fs.abs, d) + assert(ok, 'could not turn "' .. d .. '"into an absolute path') + + table.insert(dirs.recentDirs, 1, d) + end +end + +--- Remove `num` amount of dirs from the recent directories. +--- @param num number +function dirs.pop(num) + return dirRecents(num, true) +end + +--- Get entry from recent directories. +--- @param idx number +function dirs.recent(idx) + return dirs.recentDirs[idx] +end + +--- Sets the old directory. +--- @param d string +function dirs.setOld(d) + ok, d = pcall(fs.abs, d) + assert(ok, 'could not turn "' .. d .. '"into an absolute path') + + os.setenv('OLDPWD', d) + dirs.old = d +end + +return dirs diff --git a/nature/init.lua b/nature/init.lua new file mode 100644 index 0000000..c70f6cc --- /dev/null +++ b/nature/init.lua @@ -0,0 +1,45 @@ +-- Prelude initializes everything else for our shell +local _ = require 'succulent' -- Function additions + +package.path = package.path .. ';' .. hilbish.dataDir .. '/?/init.lua' +.. ';' .. hilbish.dataDir .. '/?/?.lua' + +require 'nature.commands' +require 'nature.completions' +require 'nature.opts' + +local shlvl = tonumber(os.getenv 'SHLVL') +if shlvl ~= nil then + os.setenv('SHLVL', tostring(shlvl + 1)) +else + os.setenv('SHLVL', '0') +end + +do + local virt_G = { } + + setmetatable(_G, { + __index = function (_, key) + local got_virt = virt_G[key] + if got_virt ~= nil then + return got_virt + end + + virt_G[key] = os.getenv(key) + return virt_G[key] + end, + + __newindex = function (_, key, value) + if type(value) == 'string' then + os.setenv(key, value) + virt_G[key] = value + else + if type(virt_G[key]) == 'string' then + os.setenv(key, '') + end + virt_G[key] = value + end + end, + }) +end + diff --git a/nature/opts/autocd.lua b/nature/opts/autocd.lua new file mode 100644 index 0000000..b25732e --- /dev/null +++ b/nature/opts/autocd.lua @@ -0,0 +1,23 @@ +local fs = require 'fs' + +function cdHandle(inp) + local input, exit, err = hilbish.runner.lua(inp) + + if not err then + return input, exit, err + end + + input, exit, err = hilbish.runner.sh(inp) + + if exit ~= 0 and hilbish.opts.autocd then + local ok, stat = pcall(fs.stat, input) + if ok and stat.isDir then + -- discard here to not append the cd, which will be in history + _, exit, err = hilbish.runner.sh('cd ' .. input) + end + end + + return input, exit, err +end + +hilbish.runner.setMode(cdHandle) diff --git a/nature/opts/init.lua b/nature/opts/init.lua new file mode 100644 index 0000000..59d3cbd --- /dev/null +++ b/nature/opts/init.lua @@ -0,0 +1,28 @@ +local opts = {} +hilbish.opts = {} + +setmetatable(hilbish.opts, { + __newindex = function(_, k, v) + if opts[k] == nil then + error(string.format('opt %s does not exist', k)) + end + + opts[k] = v + end, + __index = function(_, k) + return opts[k] + end +}) + +local function setupOpt(name, default) + opts[name] = default + require('nature.opts.' .. name) +end + +local defaultOpts = { + autocd = false +} + +for optsName, default in pairs(defaultOpts) do + setupOpt(optsName, default) +end diff --git a/prelude/init.lua b/prelude/init.lua deleted file mode 100644 index a5b6568..0000000 --- a/prelude/init.lua +++ /dev/null @@ -1,264 +0,0 @@ --- The preload file initializes everything else for our shell -local ansikit = require 'ansikit' -local bait = require 'bait' -local commander = require 'commander' -local fs = require 'fs' -local lunacolors = require 'lunacolors' -local _ = require 'succulent' -- Function additions -local oldDir = hilbish.cwd() - -local shlvl = tonumber(os.getenv 'SHLVL') -if shlvl ~= nil then os.setenv('SHLVL', tostring(shlvl + 1)) else os.setenv('SHLVL', '0') end - --- Builtins -local recentDirs = {} -commander.register('cd', function (args) - if #args > 0 then - local path = table.concat(args, ' '):gsub('$%$','\0'):gsub('${([%w_]+)}', os.getenv) - :gsub('$([%w_]+)', os.getenv):gsub('%z','$'):gsub('^%s*(.-)%s*$', '%1') - - if path == '-' then - path = oldDir - print(path) - end - oldDir = hilbish.cwd() - - local ok, err = pcall(function() fs.cd(path) end) - if not ok then - print(err:sub(17)) - return 1 - end - bait.throw('cd', path) - - -- add to table of recent dirs - recentDirs[11] = nil - if recentDirs[#recentDirs - 1] ~= path then - table.insert(recentDirs, 1, path) - end - - return - end - fs.cd(hilbish.home) - bait.throw('cd', hilbish.home) - - table.insert(recentDirs, 1, hilbish.home) - recentDirs[11] = nil -end) - -commander.register('exit', function() - bait.throw('hilbish.exit') - os.exit(0) -end) - -commander.register('doc', function(args) - local moddocPath = hilbish.dataDir .. '/docs/' - local modDocFormat = [[ -%s -%s -# Functions -]] - - if #args > 0 then - local mod = args[1] - - local f = io.open(moddocPath .. mod .. '.txt', 'rb') - local funcdocs = nil - if not f then - -- assume subdir - -- dataDir/docs//.txt - moddocPath = moddocPath .. mod .. '/' - local subdocName = args[2] - if not subdocName then - subdocName = 'index' - end - f = io.open(moddocPath .. subdocName .. '.txt', 'rb') - if not f then - print('No documentation found for ' .. mod .. '.') - return - end - funcdocs = f:read '*a' - local moddocs = table.filter(fs.readdir(moddocPath), function(f) return f ~= 'index.txt' end) - local subdocs = table.map(moddocs, function(fname) - return lunacolors.underline(lunacolors.blue(string.gsub(fname, '.txt', ''))) - end) - if subdocName == 'index' then - funcdocs = funcdocs .. '\nSubdocs: ' .. table.concat(subdocs, ', ') - end - end - - if not funcdocs then - funcdocs = f:read '*a' - end - local desc = '' - local ok = pcall(require, mod) - local backtickOccurence = 0 - local formattedFuncs = lunacolors.format(funcdocs:sub(1, #funcdocs - 1):gsub('`', function() - backtickOccurence = backtickOccurence + 1 - if backtickOccurence % 2 == 0 then - return '{reset}' - else - return '{underline}{green}' - end - end)) - - if ok then - local props = {} - local propstr = '' - local modDesc = '' - local modmt = getmetatable(require(mod)) - modDesc = modmt.__doc - if modmt.__docProp then - -- not all modules have docs for properties - props = table.map(modmt.__docProp, function(v, k) - return lunacolors.underline(lunacolors.blue(k)) .. ' > ' .. v - end) - end - if #props > 0 then - propstr = '\n# Properties\n' .. table.concat(props, '\n') .. '\n' - end - desc = string.format(modDocFormat, modDesc, propstr) - end - print(desc .. formattedFuncs) - f:close() - - return - end - local modules = table.map(fs.readdir(moddocPath), function(f) - return lunacolors.underline(lunacolors.blue(string.gsub(f, '.txt', ''))) - end) - - io.write [[ -Welcome to Hilbish's doc tool! Here you can find documentation for builtin -functions and other things. - -Usage: doc
[subdoc] -A section is a module or a literal section and a subdoc is a subsection for it. - -Available sections: ]] - - print(table.concat(modules, ', ')) -end) - -local helpTexts = { -[[ -Hello there! Welcome to Hilbish, the comfy and nice little shell for -Lua users and fans. Hilbish is configured with Lua, and its -scripts are also in Lua. It also runs both Lua and shell script when -interactive (aka normal usage). -]], -[[ -What does that mean for you, the user? It means that if you prefer to -use Lua for scripting instead of shell script but still have ordinary -shell usage for interactive use. -]], -[[ -If this is your first time using Hilbish and Lua, check out the -Programming in Lua book here: https://www.lua.org/pil -After (or if you already know Lua) check out the doc command. -It is an in shell tool for documentation about Hilbish provided -functions and modules. -]], -[[ -If you've updated from a pre-1.0 version (0.7.1 as an example) -you'll want to move your config from ~/.hilbishrc.lua to -]] .. -hilbish.userDir.config .. '/hilbish/init.lua' .. -[[ - -and also change all global functions (prompt, alias) to be -in the hilbish module (hilbish.prompt, hilbish.alias as examples). - -And if this is your first time (most likely), you can copy a config -from ]] .. hilbish.dataDir, -[[ -Since 1.0 is a big release, you'll want to check the changelog -at https://github.com/Rosettea/Hilbish/releases/tag/v1.0.0 -to find more breaking changes. -]] -} -commander.register('guide', function() - ansikit.clear() - ansikit.cursorTo(0, 0) - for _, text in ipairs(helpTexts) do - print(text) - local out = hilbish.read('Hit enter to continue ') - ansikit.clear() - ansikit.cursorTo(0, 0) - if not out then - return - end - end - print 'Hope you enjoy using Hilbish!' -end) - -do - local virt_G = { } - - setmetatable(_G, { - __index = function (_, key) - local got_virt = virt_G[key] - if got_virt ~= nil then - return got_virt - end - - virt_G[key] = os.getenv(key) - return virt_G[key] - end, - - __newindex = function (_, key, value) - if type(value) == 'string' then - os.setenv(key, value) - virt_G[key] = value - else - if type(virt_G[key]) == 'string' then - os.setenv(key, '') - end - virt_G[key] = value - end - end, - }) -end - -commander.register('cdr', function(args) - if not args[1] then - print(lunacolors.format [[ -cdr: change directory to one which has been recently visied - -usage: cdr - -to get a list of recent directories, use {green}{underline}cdr list{reset}]]) - return - end - - if args[1] == 'list' then - if #recentDirs == 0 then - print 'No directories have been visited.' - return 1 - end - print(table.concat(recentDirs, '\n')) - return - end - - local index = tonumber(args[1]) - if not index then - print(string.format('received %s as index, which isn\'t a number', index)) - return 1 - end - - if not recentDirs[index] then - print(string.format('no recent directory found at index %s', index)) - return 1 - end - - fs.cd(recentDirs[index]) -end) - --- Hook handles -bait.catch('command.not-found', function(cmd) - print(string.format('hilbish: %s not found', cmd)) -end) - -bait.catch('command.not-executable', function(cmd) - print(string.format('hilbish: %s: not executable', cmd)) -end) - diff --git a/rl.go b/rl.go index 8093273..88adc65 100644 --- a/rl.go +++ b/rl.go @@ -34,8 +34,7 @@ func newLineReader(prompt string, noHist bool) *lineReader { case readline.VimKeys: modeStr = "normal" case readline.VimInsert: modeStr = "insert" case readline.VimDelete: modeStr = "delete" - case readline.VimReplaceOnce: - case readline.VimReplaceMany: modeStr = "replace" + case readline.VimReplaceOnce, readline.VimReplaceMany: modeStr = "replace" } setVimMode(modeStr) } @@ -85,168 +84,91 @@ func newLineReader(prompt string, noHist bool) *lineReader { return highlighted } rl.TabCompleter = func(line []rune, pos int, _ readline.DelayedTabContext) (string, []*readline.CompletionGroup) { - ctx := string(line) + term := rt.NewTerminationWith(l.MainThread().CurrentCont(), 2, false) + compHandle := hshMod.Get(rt.StringValue("completion")).AsTable().Get(rt.StringValue("handler")) + err := rt.Call(l.MainThread(), compHandle, []rt.Value{rt.StringValue(string(line)), + rt.IntValue(int64(pos))}, term) - var compGroup []*readline.CompletionGroup - - ctx = strings.TrimLeft(ctx, " ") - if len(ctx) == 0 { - return "", compGroup + var compGroups []*readline.CompletionGroup + if err != nil { + return "", compGroups } - fields := strings.Split(ctx, " ") - if len(fields) == 0 { - return "", compGroup + luaCompGroups := term.Get(0) + luaPrefix := term.Get(1) + + if luaCompGroups.Type() != rt.TableType { + return "", compGroups } - query := fields[len(fields) - 1] - ctx = aliases.Resolve(ctx) + groups := luaCompGroups.AsTable() + // prefix is optional + pfx, _ := luaPrefix.TryString() - if len(fields) == 1 { - completions, prefix := binaryComplete(query, ctx, fields) + util.ForEach(groups, func(key rt.Value, val rt.Value) { + if key.Type() != rt.IntType || val.Type() != rt.TableType { + return + } - compGroup = append(compGroup, &readline.CompletionGroup{ - TrimSlash: false, - NoSpace: true, - Suggestions: completions, + valTbl := val.AsTable() + luaCompType := valTbl.Get(rt.StringValue("type")) + luaCompItems := valTbl.Get(rt.StringValue("items")) + + if luaCompType.Type() != rt.StringType || luaCompItems.Type() != rt.TableType { + return + } + + items := []string{} + itemDescriptions := make(map[string]string) + + util.ForEach(luaCompItems.AsTable(), func(lkey rt.Value, lval rt.Value) { + if keytyp := lkey.Type(); keytyp == rt.StringType { + // ['--flag'] = {'description', '--flag-alias'} + itemName, ok := lkey.TryString() + vlTbl, okk := lval.TryTable() + if !ok && !okk { + // TODO: error + return + } + + items = append(items, itemName) + itemDescription, ok := vlTbl.Get(rt.IntValue(1)).TryString() + if !ok { + // TODO: error + return + } + itemDescriptions[itemName] = itemDescription + } else if keytyp == rt.IntType { + vlStr, ok := lval.TryString() + if !ok { + // TODO: error + return + } + items = append(items, vlStr) + } else { + // TODO: error + return + } }) - return prefix, compGroup - } else { - if completecb, ok := luaCompletions["command." + fields[0]]; ok { - luaFields := rt.NewTable() - for i, f := range fields { - luaFields.Set(rt.IntValue(int64(i + 1)), rt.StringValue(f)) - } - - // we must keep the holy 80 cols - luacompleteTable, err := rt.Call1(l.MainThread(), - rt.FunctionValue(completecb), rt.StringValue(query), - rt.StringValue(ctx), rt.TableValue(luaFields)) - - if err != nil { - return "", compGroup - } - - /* - 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.TryTable(); ok { - nextVal := rt.NilValue - for { - next, val, ok := cmpTbl.Next(nextVal) - if next == rt.NilValue { - break - } - nextVal = next - - _, ok = next.TryInt() - valTbl, okk := val.TryTable() - if !ok || !okk { - // TODO: error? - break - } - - luaCompType := valTbl.Get(rt.StringValue("type")) - luaCompItems := valTbl.Get(rt.StringValue("items")) - - compType, ok := luaCompType.TryString() - compItems, okk := luaCompItems.TryTable() - if !ok || !okk { - // TODO: error - break - } - - var items []string - itemDescriptions := make(map[string]string) - nxVal := rt.NilValue - for { - nx, vl, _ := compItems.Next(nxVal) - if nx == rt.NilValue { - break - } - nxVal = nx - - if tstr := nx.Type(); tstr == rt.StringType { - // ['--flag'] = {'description', '--flag-alias'} - nxStr, ok := nx.TryString() - vlTbl, okk := vl.TryTable() - if !ok || !okk { - // TODO: error - continue - } - items = append(items, nxStr) - itemDescription, ok := vlTbl.Get(rt.IntValue(1)).TryString() - if !ok { - // TODO: error - continue - } - itemDescriptions[nxStr] = itemDescription - } else if tstr == rt.IntType { - vlStr, okk := vl.TryString() - if !okk { - // TODO: error - continue - } - items = append(items, vlStr) - } else { - // TODO: error - continue - } - } - - var dispType readline.TabDisplayType - switch compType { - 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, - }) - } - } + var dispType readline.TabDisplayType + switch luaCompType.AsString() { + case "grid": dispType = readline.TabDisplayGrid + case "list": dispType = readline.TabDisplayList + // need special cases, will implement later + //case "map": dispType = readline.TabDisplayMap } - if len(compGroup) == 0 { - completions, p := fileComplete(query, ctx, fields) - fcompGroup := []*readline.CompletionGroup{{ - TrimSlash: false, - NoSpace: true, - Suggestions: completions, - }} + compGroups = append(compGroups, &readline.CompletionGroup{ + DisplayType: dispType, + Descriptions: itemDescriptions, + Suggestions: items, + TrimSlash: false, + NoSpace: true, + }) + }) - return p, fcompGroup - } - } - return "", compGroup + return pfx, compGroups } return &lineReader{ diff --git a/util/util.go b/util/util.go index b8c267a..713ea6a 100644 --- a/util/util.go +++ b/util/util.go @@ -3,7 +3,9 @@ package util import ( "bufio" "io" + "strings" "os" + "os/user" rt "github.com/arnodel/golua/runtime" ) @@ -25,19 +27,37 @@ func Document(module *rt.Table, doc string) { // It is accessible via the __docProp metatable. It is a table of the names of the fields. func SetField(rtm *rt.Runtime, module *rt.Table, field string, value rt.Value, doc string) { // TODO: ^ rtm isnt needed, i should remove it + SetFieldDoc(module, field, doc) + module.Set(rt.StringValue(field), value) +} + +// SetFieldDoc sets the __docProp metatable for a field on the +// module. +func SetFieldDoc(module *rt.Table, field, doc string) { mt := module.Metatable() - + if mt == nil { mt = rt.NewTable() - docProp := rt.NewTable() - mt.Set(rt.StringValue("__docProp"), rt.TableValue(docProp)) - module.SetMetatable(mt) } + docProp := mt.Get(rt.StringValue("__docProp")) + if docProp == rt.NilValue { + docPropTbl := rt.NewTable() + mt.Set(rt.StringValue("__docProp"), rt.TableValue(docPropTbl)) + docProp = mt.Get(rt.StringValue("__docProp")) + } docProp.AsTable().Set(rt.StringValue(field), rt.StringValue(doc)) - module.Set(rt.StringValue(field), value) +} + +// SetFieldProtected sets a field in a protected table. A protected table +// is one which has a metatable proxy to ensure no overrides happen to it. +// It sets the field in the table and sets the __docProp metatable on the +// user facing table. +func SetFieldProtected(module, realModule *rt.Table, field string, value rt.Value, doc string) { + SetFieldDoc(module, field, doc) + realModule.Set(rt.StringValue(field), value) } // DoString runs the code string in the Lua runtime. @@ -118,3 +138,31 @@ func HandleStrCallback(t *rt.Thread, c *rt.GoCont) (string, *rt.Closure, error) return name, cb, err } + +// ForEach loops through a Lua table. +func ForEach(tbl *rt.Table, cb func(key rt.Value, val rt.Value)) { + nextVal := rt.NilValue + for { + key, val, _ := tbl.Next(nextVal) + if key == rt.NilValue { + break + } + nextVal = key + + cb(key, val) + } +} + +// ExpandHome expands ~ (tilde) in the path, changing it to the user home +// directory. +func ExpandHome(path string) string { + if strings.HasPrefix(path, "~") { + curuser, _ := user.Current() + homedir := curuser.HomeDir + + return strings.Replace(path, "~", homedir, 1) + } + + return path +} + diff --git a/vars_darwin.go b/vars_darwin.go index b780c23..8ec83ba 100644 --- a/vars_darwin.go +++ b/vars_darwin.go @@ -15,7 +15,7 @@ var ( .. hilbish.userDir.config .. '/hilbish/?/?.lua;' .. hilbish.userDir.config .. '/hilbish/?.lua'` dataDir = "/usr/local/share/hilbish" - preloadPath = dataDir + "/prelude/init.lua" + preloadPath = dataDir + "/nature/init.lua" sampleConfPath = dataDir + "/.hilbishrc.lua" // Path to default/sample config defaultConfDir = getenv("XDG_CONFIG_HOME", "~/.config") ) diff --git a/vars_linux.go b/vars_linux.go index 5ea3ac5..815ba6a 100644 --- a/vars_linux.go +++ b/vars_linux.go @@ -15,7 +15,7 @@ var ( .. hilbish.userDir.config .. '/hilbish/?/?.lua;' .. hilbish.userDir.config .. '/hilbish/?.lua'` dataDir = "/usr/share/hilbish" - preloadPath = dataDir + "/prelude/init.lua" + preloadPath = dataDir + "/nature/init.lua" sampleConfPath = dataDir + "/.hilbishrc.lua" // Path to default/sample config defaultConfDir = "" ) diff --git a/vars_windows.go b/vars_windows.go index 5e9878c..a257baf 100644 --- a/vars_windows.go +++ b/vars_windows.go @@ -9,7 +9,7 @@ var ( .. hilbish.userDir.config .. '\\Hilbish\\libs\\?\\?.lua;' .. hilbish.userDir.config .. '\\Hilbish\\libs\\?.lua;'` dataDir = "~\\Appdata\\Roaming\\Hilbish" // ~ and \ gonna cry? - preloadPath = dataDir + "\\prelude\\init.lua" + preloadPath = dataDir + "\\nature\\init.lua" sampleConfPath = dataDir + "\\hilbishrc.lua" // Path to default/sample config defaultConfDir = "" )