diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dc5e0f2..b47706e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,16 +12,14 @@ jobs: matrix: goos: [linux, windows, darwin] goarch: ["386", amd64, arm64] - exclude: + exclude: - goarch: "386" - goos: darwin + goos: darwin - goarch: arm64 goos: windows steps: - name: Checkout sources uses: actions/checkout@v2 - with: - fetch-depth: 0 - name: Setup Go uses: actions/setup-go@v2 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index d2a28ca..563eb7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,25 +1,56 @@ # 🎀 Changelog -## [1.0.4] - 2021-03-12 +## [1.2.0] - 2022-03-17 +### Added +- Job Management additions + - `job.start` and `job.done` hooks (`doc hooks job`) + - `hilbish.jobs` interface (`get(id)` function gets a job object via `id`, `all()` gets all) +- Customizable runner/exec mode + - However Hilbish runs interactive user input can now be changed Lua side (`doc runner-mode`) + +### Changed +- `vimMode` doc is now `vim-mode` + +### Fixed +- Make sure input which is supposed to go in history goes there +- Cursor is right at the end of input on history search + +## [1.1.0] - 2022-03-17 +### Added +- `hilbish.vimAction` hook (`doc vimMode actions`) +- `command.not-executable` hook (will replace `command.no-perm` in a future release) + +### Fixed +- Check if interactive before adding to history +- Escape in vim mode exits all modes and not only insert +- Make 2nd line in prompt empty if entire prompt is 1 line +- Completion menu doesnt appear if there is only 1 result +- Ignore SIGQUIT, which caused a panic unhandled +- Remove hostname in greeting on Windows +- Handle PATH binaries properly on Windows +- Fix removal of dot in the beginning of folders/files that have them for file complete +- Fix prompt being set to the continue prompt even when exited + +## [1.0.4] - 2022-03-12 ### Fixed - Panic when history directory doesn't exist -## [1.0.3] - 2021-03-12 +## [1.0.3] - 2022-03-12 ### Fixed - Removed duplicate executable suggestions - User input is added to history now instead of what's ran by Hilbish - Formatting issue with prompt on no input -## [1.0.2] - 2021-03-06 +## [1.0.2] - 2022-03-06 ### Fixed - Cases where Hilbish's history directory doesn't exist will no longer cause a panic -## [1.0.1] - 2021-03-06 +## [1.0.1] - 2022-03-06 ### Fixed - Using `hilbish.appendPath` will no longer result in string spam (debugging thing left being) - Prompt gets set properly on startup -## [1.0.0] - 2021-03-06 +## [1.0.0] - 2022-03-06 ### Added - MacOS is now officialy supported, default compile time vars have been added for it @@ -392,6 +423,7 @@ This input for example will prompt for more input to complete: First "stable" release of Hilbish. +[1.1.0]: https://github.com/Rosettea/Hilbish/compare/v1.0.4...v1.1.0 [1.0.4]: https://github.com/Rosettea/Hilbish/compare/v1.0.3...v1.0.4 [1.0.3]: https://github.com/Rosettea/Hilbish/compare/v1.0.2...v1.0.3 [1.0.2]: https://github.com/Rosettea/Hilbish/compare/v1.0.1...v1.0.2 diff --git a/Makefile b/Makefile index 5df094c..660bc58 100644 --- a/Makefile +++ b/Makefile @@ -1,31 +1,30 @@ PREFIX ?= /usr -DESTDIR ?= BINDIR ?= $(PREFIX)/bin LIBDIR ?= $(PREFIX)/share/hilbish -build: - @go build -ldflags "-s -w" +MY_GOFLAGS = -ldflags "-s -w" -dev: - @go build -ldflags "-s -w -X main.version=$(shell git describe --tags)" +all: dev + +dev: MY_GOFLAGS = -ldflags "-s -w -X main.version=$(shell git describe --tags)" +dev: build + +build: + go build $(MY_GOFLAGS) install: - @install -v -d "$(DESTDIR)$(BINDIR)/" && install -m 0755 -v hilbish "$(DESTDIR)$(BINDIR)/hilbish" - @mkdir -p "$(DESTDIR)$(LIBDIR)" - @cp libs docs emmyLuaDocs prelude .hilbishrc.lua "$(DESTDIR)$(LIBDIR)" -r - @grep "$(DESTDIR)$(BINDIR)/hilbish" -qxF /etc/shells || echo "$(DESTDIR)$(BINDIR)/hilbish" >> /etc/shells - @echo "Hilbish Installed" + 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)" + grep -qxF "$(DESTDIR)$(BINDIR)/hilbish" /etc/shells || echo "$(DESTDIR)$(BINDIR)/hilbish" >> /etc/shells uninstall: - @rm -vrf \ + rm -vrf \ "$(DESTDIR)$(BINDIR)/hilbish" \ "$(DESTDIR)$(LIBDIR)" - @sed -i '/hilbish/d' /etc/shells - @echo "Hilbish Uninstalled" + sed -i '/hilbish/d' /etc/shells clean: - @go clean + go clean -all: build install - -.PHONY: install uninstall build dev clean +.PHONY: all dev build install uninstall clean diff --git a/README.md b/README.md index 8df09d1..bb2ca29 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@

- 🌺 The flower shell. A comfy and nice little shell for Lua users and fans! + 🌺 The flower shell. A comfy and nice little shell for Lua fans!

GitHub commit activity @@ -14,12 +14,25 @@

-Hilbish is a Unix-y shell which uses Lua for scripting. Things like the prompt, -general configuration and such are done with Lua. +Hilbish is a extensible shell (framework). It was made to be very customizable +via the Lua programming language. It aims to be easy to use for the casual +people but powerful for those who want to tinker more with their shell, +the thing used to interface with most of the system. -For interactive use, it uses a library to run sh which works on all -platforms Hilbish can be compiled for. It can also act as a Lua REPL if you want -it to be. +The motivation for choosing Lua was that its simpler and better to use +than old shell script. It's fine for basic interactive shell uses, +but that's the only place Hilbish has shell script; everything else is Lua +and aims to be infinitely configurable. If something isn't, open an issue! + +# Table of Contents +- [Screenshots](#Screenshots) +- [Installation](#Installation) + - [Prebuilt Bins](#Prebuilt-binaries) + - [AUR](#AUR) + - [Nixpkgs](#Nixpkgs) + - [Manual Build](#Manual-Build) +- [Getting Started](#Getting-Started) +- [Contributing](#Contributing) # Screenshots
@@ -29,8 +42,6 @@ it to be.
# Installation -**NOTE:** Hilbish is currently only officially supported and tested on Linux - ## Prebuilt binaries Go [here](https://nightly.link/Rosettea/Hilbish/workflows/build/master) for builds on the master branch. @@ -56,7 +67,7 @@ If you're new to nix you should probably read up on how to do that [here](https: ### Prerequisites - [Go 1.17+](https://go.dev) -#### Build +### Build First, clone Hilbish. The recursive is required, as some Lua libraries are submodules. ```sh @@ -78,13 +89,27 @@ make build After you did all that, run `sudo make install` to install Hilbish globally. +# Getting Started +At startup, you should see a message which says to run a `guide` command. +This guide is a *very* simple and basic step through text of what Hilbish is +and where to find documentation. + +Documentation is primarily viewed via the in shell `doc` command. +Autogenerated function docs and general docs about other things are included +there, so be sure to read it. + +Using Hilbish is the same as using any other Linux shell, with an addition +that you can also run Lua. Hilbish can also act as an enhanced Lua REPL +via `hilbish.runnerMode 'lua'`. To switch back to normal, use +`hilbish.runnerMode 'hybrid'`. + # Contributing -Any kind of contributions to Hilbish are welcome! -Read [CONTRIBUTING.md](CONTRIBUTING.md) before getting started. +Any kind of contributions are welcome! Hilbish is very easy to contribute to. +Read [CONTRIBUTING.md](CONTRIBUTING.md) as a guideline to doing so. **Thanks to everyone below who's contributed!** - - + + *Made with [contributors-img](https://contrib.rocks).* diff --git a/aliases.go b/aliases.go index ec1ecae..2af6427 100644 --- a/aliases.go +++ b/aliases.go @@ -4,57 +4,59 @@ import ( "strings" "sync" - "github.com/yuin/gopher-lua" + "hilbish/util" + + rt "github.com/arnodel/golua/runtime" ) -var aliases *hilbishAliases +var aliases *aliasHandler -type hilbishAliases struct { +type aliasHandler struct { aliases map[string]string mu *sync.RWMutex } // initialize aliases map -func NewAliases() *hilbishAliases { - return &hilbishAliases{ +func newAliases() *aliasHandler { + return &aliasHandler{ aliases: make(map[string]string), mu: &sync.RWMutex{}, } } -func (h *hilbishAliases) Add(alias, cmd string) { - h.mu.Lock() - defer h.mu.Unlock() +func (a *aliasHandler) Add(alias, cmd string) { + a.mu.Lock() + defer a.mu.Unlock() - h.aliases[alias] = cmd + a.aliases[alias] = cmd } -func (h *hilbishAliases) All() map[string]string { - return h.aliases +func (a *aliasHandler) All() map[string]string { + return a.aliases } -func (h *hilbishAliases) Delete(alias string) { - h.mu.Lock() - defer h.mu.Unlock() +func (a *aliasHandler) Delete(alias string) { + a.mu.Lock() + defer a.mu.Unlock() - delete(h.aliases, alias) + delete(a.aliases, alias) } -func (h *hilbishAliases) Resolve(cmdstr string) string { - h.mu.RLock() - defer h.mu.RUnlock() +func (a *aliasHandler) Resolve(cmdstr string) string { + a.mu.RLock() + defer a.mu.RUnlock() args := strings.Split(cmdstr, " ") - for h.aliases[args[0]] != "" { - alias := h.aliases[args[0]] + for a.aliases[args[0]] != "" { + alias := a.aliases[args[0]] cmdstr = alias + strings.TrimPrefix(cmdstr, args[0]) cmdArgs, _ := splitInput(cmdstr) args = cmdArgs - if h.aliases[args[0]] == alias { + if a.aliases[args[0]] == alias { break } - if h.aliases[args[0]] != "" { + if a.aliases[args[0]] != "" { continue } } @@ -64,41 +66,38 @@ func (h *hilbishAliases) Resolve(cmdstr string) string { // lua section -func (h *hilbishAliases) Loader(L *lua.LState) *lua.LTable { +func (a *aliasHandler) Loader(rtm *rt.Runtime) *rt.Table { // create a lua module with our functions - hshaliasesLua := map[string]lua.LGFunction{ - "add": h.luaAdd, - "list": h.luaList, - "del": h.luaDelete, + hshaliasesLua := map[string]util.LuaExport{ + "add": util.LuaExport{hlalias, 2, false}, + "list": util.LuaExport{a.luaList, 0, false}, + "del": util.LuaExport{a.luaDelete, 1, false}, } - mod := L.SetFuncs(L.NewTable(), hshaliasesLua) + mod := rt.NewTable() + util.SetExports(rtm, mod, hshaliasesLua) return mod } -func (h *hilbishAliases) luaAdd(L *lua.LState) int { - alias := L.CheckString(1) - cmd := L.CheckString(2) - h.Add(alias, cmd) - - return 0 -} - -func (h *hilbishAliases) luaList(L *lua.LState) int { - aliasesList := L.NewTable() - for k, v := range h.All() { - aliasesList.RawSetString(k, lua.LString(v)) +func (a *aliasHandler) luaList(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + aliasesList := rt.NewTable() + for k, v := range a.All() { + aliasesList.Set(rt.StringValue(k), rt.StringValue(v)) } - L.Push(aliasesList) - - return 1 + return c.PushingNext1(t.Runtime, rt.TableValue(aliasesList)), nil } -func (h *hilbishAliases) luaDelete(L *lua.LState) int { - alias := L.CheckString(1) - h.Delete(alias) +func (a *aliasHandler) luaDelete(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 + } + a.Delete(alias) - return 0 + return c.Next(), nil } diff --git a/api.go b/api.go index 417104c..aa141d6 100644 --- a/api.go +++ b/api.go @@ -4,6 +4,8 @@ package main import ( + "bytes" + "errors" "fmt" "os" "os/exec" @@ -14,182 +16,128 @@ import ( "hilbish/util" - "github.com/yuin/gopher-lua" + rt "github.com/arnodel/golua/runtime" + "github.com/arnodel/golua/lib/packagelib" "github.com/maxlandon/readline" "github.com/blackfireio/osinfo" "mvdan.cc/sh/v3/interp" ) -var exports = map[string]lua.LGFunction { - "alias": hlalias, - "appendPath": hlappendPath, - "complete": hlcomplete, - "cwd": hlcwd, - "exec": hlexec, - "goro": hlgoro, - "multiprompt": hlmlprompt, - "prependPath": hlprependPath, - "prompt": hlprompt, - "inputMode": hlinputMode, - "interval": hlinterval, - "read": hlread, - "run": hlrun, - "timeout": hltimeout, - "which": hlwhich, +var exports = map[string]util.LuaExport{ + "alias": {hlalias, 2, false}, + "appendPath": {hlappendPath, 1, false}, + "complete": {hlcomplete, 2, false}, + "cwd": {hlcwd, 0, false}, + "exec": {hlexec, 1, false}, + "runnerMode": {hlrunnerMode, 1, false}, + "goro": {hlgoro, 1, true}, + "highlighter": {hlhighlighter, 1, false}, + "hinter": {hlhinter, 1, false}, + "multiprompt": {hlmultiprompt, 1, false}, + "prependPath": {hlprependPath, 1, false}, + "prompt": {hlprompt, 1, true}, + "inputMode": {hlinputMode, 1, false}, + "interval": {hlinterval, 2, false}, + "read": {hlread, 1, false}, + "run": {hlrun, 1, true}, + "timeout": {hltimeout, 2, false}, + "which": {hlwhich, 1, false}, } var greeting string -var hshMod *lua.LTable +var hshMod *rt.Table +var hilbishLoader = packagelib.Loader{ + Load: hilbishLoad, + Name: "hilbish", +} -func hilbishLoader(L *lua.LState) int { - mod := L.SetFuncs(L.NewTable(), exports) +func hilbishLoad(rtm *rt.Runtime) (rt.Value, func()) { + mod := rt.NewTable() + util.SetExports(rtm, mod, exports) hshMod = mod host, _ := os.Hostname() username := curuser.Username - greeting = `Welcome to {magenta}Hilbish{reset}, {cyan}` + curuser.Username + `{reset}. -The nice lil shell for {blue}Lua{reset} fanatics! -Check out the {blue}{bold}guide{reset} command to get started. -` - if runtime.GOOS == "windows" { username = strings.Split(username, "\\")[1] // for some reason Username includes the hostname on windows } - util.SetField(L, mod, "ver", lua.LString(version), "Hilbish version") - util.SetField(L, mod, "user", lua.LString(username), "Username of user") - util.SetField(L, mod, "host", lua.LString(host), "Host name of the machine") - util.SetField(L, mod, "home", lua.LString(curuser.HomeDir), "Home directory of the user") - util.SetField(L, mod, "dataDir", lua.LString(dataDir), "Directory for Hilbish's data files") - util.SetField(L, mod, "interactive", lua.LBool(interactive), "If this is an interactive shell") - util.SetField(L, mod, "login", lua.LBool(interactive), "Whether this is a login shell") - util.SetField(L, mod, "greeting", lua.LString(greeting), "Hilbish's welcome message for interactive shells. It has Lunacolors formatting.") - util.SetField(l, mod, "vimMode", lua.LNil, "Current Vim mode of Hilbish (nil if not in Vim mode)") - util.SetField(l, hshMod, "exitCode", lua.LNumber(0), "Exit code of last exected command") - util.Document(L, mod, "Hilbish's core API, containing submodules and functions which relate to the shell itself.") + greeting = `Welcome to {magenta}Hilbish{reset}, {cyan}` + username + `{reset}. +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.") // hilbish.userDir table - hshuser := L.NewTable() + hshuser := rt.NewTable() - util.SetField(L, hshuser, "config", lua.LString(confDir), "User's config directory") - 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) + util.SetField(rtm, hshuser, "config", rt.StringValue(confDir), "User's config directory") + util.SetField(rtm, hshuser, "data", rt.StringValue(userDataDir), "XDG data directory") + util.Document(hshuser, "User directories to store configs and/or modules.") + mod.Set(rt.StringValue("userDir"), rt.TableValue(hshuser)) // hilbish.os table - hshos := L.NewTable() + hshos := rt.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") - util.Document(L, hshos, "OS info interface") - L.SetField(mod, "os", hshos) + util.SetField(rtm, hshos, "family", rt.StringValue(info.Family), "Family name of the current OS") + util.SetField(rtm, hshos, "name", rt.StringValue(info.Name), "Pretty name of the current OS") + util.SetField(rtm, hshos, "version", rt.StringValue(info.Version), "Version of the current OS") + util.Document(hshos, "OS info interface") + mod.Set(rt.StringValue("os"), rt.TableValue(hshos)) // hilbish.aliases table - aliases = NewAliases() - aliasesModule := aliases.Loader(L) - util.Document(L, aliasesModule, "Alias inferface for Hilbish.") - L.SetField(mod, "aliases", aliasesModule) + aliases = newAliases() + aliasesModule := aliases.Loader(rtm) + util.Document(aliasesModule, "Alias inferface for Hilbish.") + mod.Set(rt.StringValue("aliases"), rt.TableValue(aliasesModule)) // hilbish.history table - historyModule := lr.Loader(L) - util.Document(L, historyModule, "History interface for Hilbish.") - L.SetField(mod, "history", historyModule) + historyModule := lr.Loader(rtm) + mod.Set(rt.StringValue("history"), rt.TableValue(historyModule)) + util.Document(historyModule, "History interface for Hilbish.") - // hilbish.completions table - hshcomp := L.NewTable() + // hilbish.completion table + hshcomp := rt.NewTable() + util.SetField(rtm, hshcomp, "files", + rt.FunctionValue(rt.NewGoFunction(luaFileComplete, "files", 3, false)), + "Completer for files") - 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) + util.SetField(rtm, hshcomp, "bins", + rt.FunctionValue(rt.NewGoFunction(luaBinaryComplete, "bins", 3, false)), + "Completer for executables/binaries") - L.Push(mod) + util.Document(hshcomp, "Completions interface for Hilbish.") + mod.Set(rt.StringValue("completion"), rt.TableValue(hshcomp)) - return 1 -} + // hilbish.runner table + runnerModule := runnerModeLoader(rtm) + util.Document(runnerModule, "Runner/exec interface for Hilbish.") + mod.Set(rt.StringValue("runner"), rt.TableValue(runnerModule)) -func luaFileComplete(L *lua.LState) int { - query := L.CheckString(1) - ctx := L.CheckString(2) - fields := L.CheckTable(3) + // hilbish.jobs table + jobs = newJobHandler() + jobModule := jobs.loader(rtm) + util.Document(jobModule, "(Background) job interface.") + mod.Set(rt.StringValue("jobs"), rt.TableValue(jobModule)) + + timers = newTimerHandler() + timerModule := timers.loader(rtm) + util.Document(timerModule, "Timer interface, for control of all intervals and timeouts.") + mod.Set(rt.StringValue("timers"), rt.TableValue(timerModule)) - 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) { - util.SetField(l, hshMod, "vimMode", lua.LString(mode), "Current Vim mode of Hilbish (nil if not in Vim mode)") - hooks.Em.Emit("hilbish.vimMode", mode) -} - -func unsetVimMode() { - util.SetField(l, hshMod, "vimMode", lua.LNil, "Current Vim mode of Hilbish (nil if not in Vim mode)") -} - -// run(cmd) -// Runs `cmd` in Hilbish's sh interpreter. -// --- @param cmd string -func hlrun(L *lua.LState) int { - var exitcode uint8 - cmd := L.CheckString(1) - err := execCommand(cmd, cmd) - - if code, ok := interp.IsExitStatus(err); ok { - exitcode = code - } else if err != nil { - exitcode = 1 - } - - L.Push(lua.LNumber(exitcode)) - return 1 -} - -// cwd() -// Returns the current directory of the shell -func hlcwd(L *lua.LState) int { - cwd, _ := os.Getwd() - - L.Push(lua.LString(cwd)) - - return 1 + return rt.TableValue(mod), nil } func getenv(key, fallback string) string { @@ -200,28 +148,164 @@ 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) +} + +func unsetVimMode() { + util.SetField(l, hshMod, "vimMode", rt.NilValue, "Current Vim mode of Hilbish (nil if not in Vim mode)") +} + +// run(cmd, returnOut) -> exitCode, stdout, stderr +// Runs `cmd` in Hilbish's sh interpreter. +// If returnOut is true, the outputs of `cmd` will be returned as the 2nd and +// 3rd values instead of being outputted to the terminal. +// --- @param cmd string +func hlrun(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + cmd, err := c.StringArg(0) + if err != nil { + return nil, err + } + + var terminalOut bool + if len(c.Etc()) != 0 { + tout := c.Etc()[0] + termOut, ok := tout.TryBool() + terminalOut = termOut + if !ok { + return nil, errors.New("bad argument to run (expected boolean, got " + tout.TypeName() + ")") + } + } else { + terminalOut = true + } + + var exitcode uint8 + stdout, stderr, err := execCommand(cmd, terminalOut) + + if code, ok := interp.IsExitStatus(err); ok { + exitcode = code + } else if err != nil { + exitcode = 1 + } + + stdoutStr := "" + stderrStr := "" + if !terminalOut { + stdoutStr = stdout.(*bytes.Buffer).String() + stderrStr = stderr.(*bytes.Buffer).String() + } + + return c.PushingNext(t.Runtime, rt.IntValue(int64(exitcode)), rt.StringValue(stdoutStr), rt.StringValue(stderrStr)), nil +} + +// cwd() +// Returns the current directory of the shell +func hlcwd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + cwd, _ := os.Getwd() + + return c.PushingNext1(t.Runtime, rt.StringValue(cwd)), nil +} + + // read(prompt) -> input? // Read input from the user, using Hilbish's line editor/input reader. // This is a separate instance from the one Hilbish actually uses. // Returns `input`, will be nil if ctrl + d is pressed, or an error occurs (which shouldn't happen) // --- @param prompt string -func hlread(L *lua.LState) int { - luaprompt := L.CheckString(1) +func hlread(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + luaprompt, err := c.StringArg(0) + if err != nil { + return nil, err + } lualr := newLineReader("", true) lualr.SetPrompt(luaprompt) input, err := lualr.Read() if err != nil { - L.Push(lua.LNil) - return 1 + return c.Next(), nil } - L.Push(lua.LString(input)) - return 1 + return c.PushingNext1(t.Runtime, rt.StringValue(input)), nil } /* -prompt(str) +prompt(str, typ?) Changes the shell prompt to `str` There are a few verbs that can be used in the prompt text. These will be formatted and replaced with the appropriate values. @@ -229,53 +313,110 @@ These will be formatted and replaced with the appropriate values. `%u` - Name of current user `%h` - Hostname of device --- @param str string +--- @param typ string Type of prompt, being left or right. Left by default. */ -func hlprompt(L *lua.LState) int { - prompt = L.CheckString(1) - lr.SetPrompt(fmtPrompt(prompt)) +func hlprompt(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + err := c.Check1Arg() + if err != nil { + return nil, err + } + p, err := c.StringArg(0) + if err != nil { + return nil, err + } + typ := "left" + // optional 2nd arg + if len(c.Etc()) != 0 { + ltyp := c.Etc()[0] + var ok bool + typ, ok = ltyp.TryString() + if !ok { + return nil, errors.New("bad argument to run (expected string, got " + ltyp.TypeName() + ")") + } + } - return 0 + switch typ { + case "left": + prompt = p + lr.SetPrompt(fmtPrompt(prompt)) + case "right": lr.SetRightPrompt(fmtPrompt(p)) + default: return nil, errors.New("expected prompt type to be right or left, got " + typ) + } + + return c.Next(), nil } // multiprompt(str) // Changes the continued line prompt to `str` // --- @param str string -func hlmlprompt(L *lua.LState) int { - multilinePrompt = L.CheckString(1) +func hlmultiprompt(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + prompt, err := c.StringArg(0) + if err != nil { + return nil, err + } + multilinePrompt = prompt - return 0 + return c.Next(), nil } // alias(cmd, orig) -// Sets an alias of `orig` to `cmd` +// Sets an alias of `cmd` to `orig` // --- @param cmd string // --- @param orig string -func hlalias(L *lua.LState) int { - alias := L.CheckString(1) - source := L.CheckString(2) +func hlalias(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.CheckNArgs(2); err != nil { + return nil, err + } + cmd, err := c.StringArg(0) + if err != nil { + return nil, err + } + orig, err := c.StringArg(1) + if err != nil { + return nil, err + } - aliases.Add(alias, source) + aliases.Add(cmd, orig) - return 1 + return c.Next(), nil } // appendPath(dir) // Appends `dir` to $PATH // --- @param dir string|table -func hlappendPath(L *lua.LState) int { +func hlappendPath(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + arg := c.Arg(0) + // check if dir is a table or a string - arg := L.Get(1) - if arg.Type() == lua.LTTable { - arg.(*lua.LTable).ForEach(func(k lua.LValue, v lua.LValue) { - appendPath(v.String()) - }) - } else if arg.Type() == lua.LTString { - appendPath(arg.String()) + if arg.Type() == rt.TableType { + nextVal := rt.NilValue + for { + next, val, ok := arg.AsTable().Next(nextVal) + if next == rt.NilValue { + break + } + nextVal = next + + valStr, ok := val.TryString() + if !ok { + continue + } + + appendPath(valStr) + } + } else if arg.Type() == rt.StringType { + appendPath(arg.AsString()) } else { - L.RaiseError("bad argument to appendPath (expected string or table, got %v)", L.Get(1).Type().String()) + return nil, errors.New("bad argument to appendPath (expected string or table, got " + arg.TypeName() + ")") } - return 0 + return c.Next(), nil } func appendPath(dir string) { @@ -291,8 +432,14 @@ func appendPath(dir string) { // exec(cmd) // Replaces running hilbish with `cmd` // --- @param cmd string -func hlexec(L *lua.LState) int { - cmd := L.CheckString(1) +func hlexec(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + cmd, err := c.StringArg(0) + if err != nil { + return nil, err + } cmdArgs, _ := splitInput(cmd) if runtime.GOOS != "windows" { cmdPath, err := exec.LookPath(cmdArgs[0]) @@ -314,88 +461,82 @@ func hlexec(L *lua.LState) int { os.Exit(0) } - return 0 + return c.Next(), nil } // goro(fn) // Puts `fn` in a goroutine // --- @param fn function -func hlgoro(L *lua.LState) int { - fn := L.CheckFunction(1) - argnum := L.GetTop() - args := make([]lua.LValue, argnum) - for i := 1; i <= argnum; i++ { - args[i - 1] = L.Get(i) +func hlgoro(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + fn, err := c.ClosureArg(0) + if err != nil { + return nil, err } // call fn go func() { - if err := L.CallByParam(lua.P{ - Fn: fn, - NRet: 0, - Protect: true, - }, args...); err != nil { + _, err := rt.Call1(l.MainThread(), rt.FunctionValue(fn), c.Etc()...) + if err != nil { fmt.Fprintln(os.Stderr, "Error in goro function:\n\n", err) } }() - return 0 + return c.Next(), nil } // timeout(cb, time) // Runs the `cb` function after `time` in milliseconds +// Returns a `timer` object (see `doc timers`). // --- @param cb function // --- @param time number -func hltimeout(L *lua.LState) int { - cb := L.CheckFunction(1) - ms := L.CheckInt(2) - - timeout := time.Duration(ms) * time.Millisecond - time.Sleep(timeout) - - if err := L.CallByParam(lua.P{ - Fn: cb, - NRet: 0, - Protect: true, - }); err != nil { - fmt.Fprintln(os.Stderr, "Error in goro function:\n\n", err) +// --- @return table +func hltimeout(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.CheckNArgs(2); err != nil { + return nil, err } - return 0 + cb, err := c.ClosureArg(0) + if err != nil { + return nil, err + } + ms, err := c.IntArg(1) + if err != nil { + return nil, err + } + + interval := time.Duration(ms) * time.Millisecond + timer := timers.create(timerTimeout, interval, cb) + timer.start() + + return c.PushingNext1(t.Runtime, timer.lua()), nil } // interval(cb, time) -// Runs the `cb` function every `time` milliseconds +// Runs the `cb` function every `time` milliseconds. +// Returns a `timer` object (see `doc timers`). // --- @param cb function // --- @param time number -func hlinterval(L *lua.LState) int { - intervalfunc := L.CheckFunction(1) - ms := L.CheckInt(2) +// --- @return table +func hlinterval(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.CheckNArgs(2); err != nil { + return nil, err + } + cb, err := c.ClosureArg(0) + if err != nil { + return nil, err + } + ms, err := c.IntArg(1) + if err != nil { + return nil, err + } + interval := time.Duration(ms) * time.Millisecond + timer := timers.create(timerInterval, interval, cb) + timer.start() - ticker := time.NewTicker(interval) - stop := make(chan lua.LValue) - - go func() { - for { - select { - case <-ticker.C: - if err := L.CallByParam(lua.P{ - Fn: intervalfunc, - NRet: 0, - Protect: true, - }); err != nil { - fmt.Fprintln(os.Stderr, "Error in interval function:\n\n", err) - stop <- lua.LTrue // stop the interval - } - case <-stop: - ticker.Stop() - return - } - } - }() - - L.Push(lua.LChannel(stop)) - return 1 + return c.PushingNext1(t.Runtime, timer.lua()), nil } // complete(scope, cb) @@ -408,20 +549,27 @@ func hlinterval(L *lua.LState) int { // `grid` (the normal file completion display) or `list` (with a description) // --- @param scope string // --- @param cb function -func hlcomplete(L *lua.LState) int { - scope := L.CheckString(1) - cb := L.CheckFunction(2) - +func hlcomplete(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + scope, cb, err := util.HandleStrCallback(t, c) + if err != nil { + return nil, err + } luaCompletions[scope] = cb - return 0 + return c.Next(), nil } // prependPath(dir) // Prepends `dir` to $PATH // --- @param dir string -func hlprependPath(L *lua.LState) int { - dir := L.CheckString(1) +func hlprependPath(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + dir, err := c.StringArg(0) + if err != nil { + return nil, err + } dir = strings.Replace(dir, "~", curuser.HomeDir, 1) pathenv := os.Getenv("PATH") @@ -430,29 +578,40 @@ func hlprependPath(L *lua.LState) int { os.Setenv("PATH", dir + string(os.PathListSeparator) + pathenv) } - return 0 + return c.Next(), nil } // which(binName) // Searches for an executable called `binName` in the directories of $PATH // --- @param binName string -func hlwhich(L *lua.LState) int { - binName := L.CheckString(1) +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) + if err != nil { + return nil, err + } path, err := exec.LookPath(binName) if err != nil { - l.Push(lua.LNil) - return 1 + return c.Next(), nil } - l.Push(lua.LString(path)) - return 1 + return c.PushingNext1(t.Runtime, rt.StringValue(path)), nil } // inputMode(mode) // Sets the input mode for Hilbish's line reader. Accepts either emacs for vim // --- @param mode string -func hlinputMode(L *lua.LState) int { - mode := L.CheckString(1) +func hlinputMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + mode, err := c.StringArg(0) + if err != nil { + return nil, err + } + switch mode { case "emacs": unsetVimMode() @@ -460,7 +619,74 @@ func hlinputMode(L *lua.LState) int { case "vim": setVimMode("insert") lr.rl.InputMode = readline.Vim - default: L.RaiseError("inputMode: expected vim or emacs, received " + mode) + default: + return nil, errors.New("inputMode: expected vim or emacs, received " + mode) } - return 0 + + return c.Next(), nil +} + +// runnerMode(mode) +// Sets the execution/runner mode for interactive Hilbish. This determines whether +// Hilbish wll try to run input as Lua and/or sh or only do one of either. +// Accepted values for mode are hybrid (the default), hybridRev (sh first then Lua), +// sh, and lua. It also accepts a function, to which if it is passed one +// will call it to execute user input instead. +// --- @param mode string|function +func hlrunnerMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + mode := c.Arg(0) + + 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()) + } + case rt.FunctionType: runnerMode = mode + default: return nil, errors.New("execMode: expected either a function or hybrid, hybridRev, lua, sh. Received " + mode.TypeName()) + } + + 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 +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 +} + +// 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 +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 } diff --git a/cmd/docgen/docgen.go b/cmd/docgen/docgen.go index 63e53a6..39a2a76 100644 --- a/cmd/docgen/docgen.go +++ b/cmd/docgen/docgen.go @@ -80,6 +80,9 @@ func main() { if emmyType == "@param" { em.Params = append(em.Params, emmyLinePieces[1]) } + if emmyType == "@vararg" { + em.Params = append(em.Params, "...") // add vararg + } em.Docs = append(em.Docs, d) } else { funcdoc = append(funcdoc, d) @@ -111,6 +114,9 @@ func main() { if emmyType == "@param" { em.Params = append(em.Params, emmyLinePieces[1]) } + if emmyType == "@vararg" { + em.Params = append(em.Params, "...") // add vararg + } em.Docs = append(em.Docs, d) } else { funcdoc = append(funcdoc, d) diff --git a/complete.go b/complete.go index 585c276..e83fa33 100644 --- a/complete.go +++ b/complete.go @@ -2,27 +2,12 @@ package main import ( "path/filepath" - "runtime" "strings" "os" - "unicode" ) -func fileComplete(query, ctx string, fields []string) []string { - var completions []string - - prefixes := []string{"./", "../", "/", "~/"} - for _, prefix := range prefixes { - if strings.HasPrefix(query, prefix) { - completions, _ = matchPath(strings.Replace(query, "~", curuser.HomeDir, 1), query) - } - } - - if len(completions) == 0 && len(fields) > 1 { - completions, _ = matchPath("./" + query, query) - } - - return completions +func fileComplete(query, ctx string, fields []string) ([]string, string) { + return matchPath(query) } func binaryComplete(query, ctx string, fields []string) ([]string, string) { @@ -31,17 +16,17 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) { prefixes := []string{"./", "../", "/", "~/"} for _, prefix := range prefixes { if strings.HasPrefix(query, prefix) { - fileCompletions := fileComplete(query, ctx, fields) + fileCompletions, filePref := matchPath(query) 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 { + fullPath, _ := filepath.Abs(expandHome(query + strings.TrimPrefix(f, filePref))) + if err := findExecutable(fullPath, false, true); err != nil { continue } completions = append(completions, f) } } - return completions, "" + return completions, filePref } } @@ -53,7 +38,8 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) { // 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 { + err := findExecutable(match, true, false) + if err != nil { continue } // get basename from match @@ -76,55 +62,53 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) { return completions, query } -func matchPath(path, pref string) ([]string, error) { +func matchPath(query string) ([]string, string) { var entries []string - matches, err := filepath.Glob(desensitize(path) + "*") - if err == nil { - args := []string{ - "\"", "\\\"", - "'", "\\'", - "`", "\\`", - " ", "\\ ", - "(", "\\(", - ")", "\\)", - "[", "\\[", - "]", "\\]", - } + var baseName string - r := strings.NewReplacer(args...) - for _, match := range matches { - name := filepath.Base(match) - p := filepath.Base(pref) - if pref == "" { - p = "" + path, _ := filepath.Abs(expandHome(filepath.Dir(query))) + 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) + for _, file := range files { + if strings.HasPrefix(strings.ToLower(file.Name()), strings.ToLower(baseName)) { + entry := file.Name() + if file.IsDir() { + entry = entry + string(os.PathSeparator) } - name = strings.TrimPrefix(name, p) - matchFull, _ := filepath.Abs(match) - if info, err := os.Stat(matchFull); err == nil && info.IsDir() { - name = name + string(os.PathSeparator) - } - name = r.Replace(name) - entries = append(entries, name) + entry = escapeFilename(entry) + entries = append(entries, entry) } } - return entries, err + return entries, baseName } -func desensitize(text string) string { - if runtime.GOOS == "windows" { - return text +func escapeFilename(fname string) string { + args := []string{ + "\"", "\\\"", + "'", "\\'", + "`", "\\`", + " ", "\\ ", + "(", "\\(", + ")", "\\)", + "[", "\\[", + "]", "\\]", + "$", "\\$", + "&", "\\&", + "*", "\\*", + ">", "\\>", + "<", "\\<", + "|", "\\|", } - p := strings.Builder{} - - for _, r := range text { - if unicode.IsLetter(r) { - p.WriteString("[" + string(unicode.ToLower(r)) + string(unicode.ToUpper(r)) + "]") - } else { - p.WriteString(string(r)) - } - } - - return p.String() + r := strings.NewReplacer(args...) + return r.Replace(fname) } + diff --git a/docs/hilbish.txt b/docs/hilbish.txt index a32d90e..e86af79 100644 --- a/docs/hilbish.txt +++ b/docs/hilbish.txt @@ -1,4 +1,4 @@ -alias(cmd, orig) > Sets an alias of `orig` to `cmd` +alias(cmd, orig) > Sets an alias of `cmd` to `orig` appendPath(dir) > Appends `dir` to $PATH @@ -16,15 +16,26 @@ 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. + +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. + inputMode(mode) > Sets the input mode for Hilbish's line reader. Accepts either emacs for vim -interval(cb, time) > Runs the `cb` function every `time` milliseconds +interval(cb, time) > Runs the `cb` function every `time` milliseconds. +Returns a `timer` object (see `doc timers`). multiprompt(str) > Changes the continued line prompt to `str` prependPath(dir) > Prepends `dir` to $PATH -prompt(str) > Changes the shell prompt to `str` +prompt(str, typ?) > Changes the shell prompt to `str` There are a few verbs that can be used in the prompt text. These will be formatted and replaced with the appropriate values. `%d` - Current working directory @@ -35,9 +46,18 @@ read(prompt) -> input? > Read input from the user, using Hilbish's line editor/i This is a separate instance from the one Hilbish actually uses. Returns `input`, will be nil if ctrl + d is pressed, or an error occurs (which shouldn't happen) -run(cmd) > Runs `cmd` in Hilbish's sh interpreter. +run(cmd, returnOut) -> exitCode, stdout, stderr > Runs `cmd` in Hilbish's sh interpreter. +If returnOut is true, the outputs of `cmd` will be returned as the 2nd and +3rd values instead of being outputted to the terminal. + +runnerMode(mode) > Sets the execution/runner mode for interactive Hilbish. This determines whether +Hilbish wll try to run input as Lua and/or sh or only do one of either. +Accepted values for mode are hybrid (the default), hybridRev (sh first then Lua), +sh, and lua. It also accepts a function, to which if it is passed one +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 diff --git a/docs/hooks/job.txt b/docs/hooks/job.txt new file mode 100644 index 0000000..497df1c --- /dev/null +++ b/docs/hooks/job.txt @@ -0,0 +1,13 @@ +Note: A `job` is a table with the following keys: +- cmd: command string +- running: boolean whether the job is running +- id: unique id for the job +- pid: process id for the job +- exitCode: exit code of the job +In ordinary cases you'd prefer to use the id instead of pid. The id is unique to +Hilbish and is how you get jobs with the `hilbish.jobs` interface. + ++ `job.start` -> job > Thrown when a new background job starts. + ++ `job.done` -> job > Thrown when a background jobs exits. + diff --git a/docs/runner-mode.txt b/docs/runner-mode.txt new file mode 100644 index 0000000..9df1095 --- /dev/null +++ b/docs/runner-mode.txt @@ -0,0 +1,42 @@ +Hilbish is *unique,* when interactive it first attempts to run input as +Lua and then tries shell script. But if you're normal, you wouldn't +really be using Hilbish anyway but you'd also not want this +(or maybe want Lua only in some cases.) + +The "runner mode" of Hilbish is customizable via `hilbish.runnerMode`, +which determines how Hilbish will run user input. By default, this is +set to `hybrid` which is the previously mentioned behaviour of running Lua +first then going to shell script. If you want the reverse order, you can +set it to `hybridRev` and for isolated modes there is `sh` and `lua` +respectively. + +You can also set it to a function, which will be called everytime Hilbish +needs to run interactive input. For example, you can set this to a simple +function to compile and evaluate Fennel, and now you can run Fennel. +You can even mix it with sh to make a hybrid mode with Lua replaced by +Fennel. + +An example: +hilbish.runnerMode(function(input) + local ok = pcall(fennel.eval, input) + if ok then + return input, 0, nil + end + + return hilbish.runner.sh(input) +end) + +The `hilbish.runner` interface is an alternative to using `hilbish.runnerMode` +and also provides the sh and Lua runner functions that Hilbish itself uses. +A runner function is expected to return 3 values: the input, exit code, and an error. +The input return is there incase you need to prompt for more input. +If you don't, just return the input passed to the runner function. +The exit code has to be a number, it will be 0 otherwise and the error can be +`nil` to indicate no error. + +## Functions +These are the functions for the `hilbish.runner` interface + ++ setMode(mode) > The same as `hilbish.runnerMode` ++ sh(input) -> input, code, err > Runs `input` in Hilbish's sh interpreter ++ lua(input) -> input, code, err > Evals `input` as Lua code diff --git a/docs/terminal.txt b/docs/terminal.txt index 80af3f4..7683bbb 100644 --- a/docs/terminal.txt +++ b/docs/terminal.txt @@ -1,9 +1,9 @@ -setRaw() > Puts the terminal in raw mode - restoreState() > Restores the last saved state of the terminal saveState() > Saves the current state of the terminal +setRaw() > Puts the terminal in raw mode + size() > Gets the dimensions of the terminal. Returns a table with `width` and `height` Note: this is not the size in relation to the dimensions of the display diff --git a/docs/timers.txt b/docs/timers.txt new file mode 100644 index 0000000..c5a456b --- /dev/null +++ b/docs/timers.txt @@ -0,0 +1,30 @@ +If you ever want to run a piece of code on a timed interval, or want to wait +a few seconds, you don't have to rely on timing tricks, as Hilbish has a +timer API to set intervals and timeouts. + +These are the simple functions `hilbish.interval` and `hilbish.timeout` (doc +accessible with `doc hilbish`). But if you want slightly more control over +them, there is the `hilbish.timers` interface. It allows you to get +a timer via ID. + +# Timer Interface +## Functions +- `get(id)` -> timer: get a timer via its id +- `create(type, ms, callback)` -> timer: creates a timer, adding it to the timer pool. +`type` is the type of timer it will be. 0 is an interval, 1 is a timeout. +`ms` is the time it will run for in seconds. callback is the function called +when the timer is triggered. + +# Timer Object +Those previously mentioned functions return a `timer` object, to which you can +stop and start a timer again. The functions of the timers interface also +return a timer object. + +## Properties +- `duration`: amount of time the timer runs for in milliseconds +- `running`: whether the timer is running or not +- `type`: the type of timer (0 is interval, 1 is timeout) + +## Functions +- `stop()`: stops the timer. returns an error if it's already stopped +- `start()`: starts the timer. returns an error if it's already started diff --git a/docs/vim-mode/actions.txt b/docs/vim-mode/actions.txt new file mode 100644 index 0000000..9dfb7b2 --- /dev/null +++ b/docs/vim-mode/actions.txt @@ -0,0 +1,16 @@ +Vim actions are essentially just when a user uses a Vim keybind. +Things like yanking and pasting are Vim actions. +This is not an "offical Vim thing," just a Hilbish thing. + +The `hilbish.vimAction` hook is thrown whenever a Vim action occurs. +It passes 2 arguments: the action name, and an array (table) of args +relating to it. + +Here is documentation for what the table of args will hold for an +appropriate Vim action. + +- `yank`: register, yankedText +The first argument for the yank action is the register yankedText goes to. + +- `paste`: register, pastedText +The first argument for the paste action is the register pastedText is taken from. diff --git a/docs/vim-mode/index.txt b/docs/vim-mode/index.txt new file mode 100644 index 0000000..a30fe74 --- /dev/null +++ b/docs/vim-mode/index.txt @@ -0,0 +1,4 @@ +Hilbish has a Vim binding input mode accessible for use. +It can be enabled with the `hilbish.inputMode` function (check `doc hilbish`). + +This is documentation for everything relating to it. diff --git a/emmyLuaDocs/bait.lua b/emmyLuaDocs/bait.lua index 5a37afc..01ca774 100644 --- a/emmyLuaDocs/bait.lua +++ b/emmyLuaDocs/bait.lua @@ -15,6 +15,6 @@ function bait.catchOnce(name, cb) end --- Throws a hook with `name` with the provided `args` --- @param name string --- @vararg any -function bait.throw(name) end +function bait.throw(name, ...) end return bait diff --git a/emmyLuaDocs/hilbish.lua b/emmyLuaDocs/hilbish.lua index d0d5068..f8c4380 100644 --- a/emmyLuaDocs/hilbish.lua +++ b/emmyLuaDocs/hilbish.lua @@ -2,7 +2,7 @@ local hilbish = {} ---- Sets an alias of `orig` to `cmd` +--- Sets an alias of `cmd` to `orig` --- @param cmd string --- @param orig string function hilbish.alias(cmd, orig) end @@ -33,18 +33,34 @@ 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 + +--- 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 + --- Sets the input mode for Hilbish's line reader. Accepts either emacs for vim --- @param mode string function hilbish.inputMode(mode) end ---- Runs the `cb` function every `time` milliseconds +--- Runs the `cb` function every `time` milliseconds. +--- Returns a `timer` object (see `doc timers`). --- @param cb function --- @param time number +--- @return table function hilbish.interval(cb, time) end --- Changes the continued line prompt to `str` --- @param str string -function hilbish.mlprompt(str) end +function hilbish.multiprompt(str) end --- Prepends `dir` to $PATH --- @param dir string @@ -57,7 +73,8 @@ function hilbish.prependPath(dir) end --- `%u` - Name of current user --- `%h` - Hostname of device --- @param str string -function hilbish.prompt(str) end +--- @param typ string Type of prompt, being left or right. Left by default. +function hilbish.prompt(str, typ) end --- Read input from the user, using Hilbish's line editor/input reader. --- This is a separate instance from the one Hilbish actually uses. @@ -66,12 +83,24 @@ function hilbish.prompt(str) end function hilbish.read(prompt) end --- Runs `cmd` in Hilbish's sh interpreter. +--- If returnOut is true, the outputs of `cmd` will be returned as the 2nd and +--- 3rd values instead of being outputted to the terminal. --- @param cmd string function hilbish.run(cmd) end +--- Sets the execution/runner mode for interactive Hilbish. This determines whether +--- Hilbish wll try to run input as Lua and/or sh or only do one of either. +--- Accepted values for mode are hybrid (the default), hybridRev (sh first then Lua), +--- sh, and lua. It also accepts a function, to which if it is passed one +--- will call it to execute user input instead. +--- @param mode string|function +function hilbish.runnerMode(mode) end + --- Runs the `cb` function after `time` in milliseconds +--- Returns a `timer` object (see `doc timers`). --- @param cb function --- @param time number +--- @return table function hilbish.timeout(cb, time) end --- Searches for an executable called `binName` in the directories of $PATH diff --git a/emmyLuaDocs/terminal.lua b/emmyLuaDocs/terminal.lua index 57073ae..2266ac6 100644 --- a/emmyLuaDocs/terminal.lua +++ b/emmyLuaDocs/terminal.lua @@ -2,15 +2,15 @@ local terminal = {} ---- Puts the terminal in raw mode -function terminal.raw() end - --- Restores the last saved state of the terminal function terminal.restoreState() end --- Saves the current state of the terminal function terminal.saveState() end +--- Puts the terminal in raw mode +function terminal.setRaw() end + --- Gets the dimensions of the terminal. Returns a table with `width` and `height` --- Note: this is not the size in relation to the dimensions of the display function terminal.size() end diff --git a/exec.go b/exec.go index f1b7c88..90498ab 100644 --- a/exec.go +++ b/exec.go @@ -1,30 +1,109 @@ package main import ( + "bytes" "context" + "errors" + "os/exec" "fmt" + "io" "os" "path/filepath" + "runtime" "strings" + "syscall" "time" "hilbish/util" - "github.com/yuin/gopher-lua" + rt "github.com/arnodel/golua/runtime" "mvdan.cc/sh/v3/shell" //"github.com/yuin/gopher-lua/parse" "mvdan.cc/sh/v3/interp" "mvdan.cc/sh/v3/syntax" + "mvdan.cc/sh/v3/expand" ) -func runInput(input, origInput string) { +var errNotExec = errors.New("not executable") +var runnerMode rt.Value = rt.StringValue("hybrid") + +func runInput(input string, priv bool) { running = true cmdString := aliases.Resolve(input) - hooks.Em.Emit("command.preexec", input, cmdString) + var exitCode uint8 + var err error + if runnerMode.Type() == rt.StringType { + switch runnerMode.AsString() { + case "hybrid": + _, _, err = handleLua(cmdString) + if err == nil { + cmdFinish(0, input, priv) + 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 { + cmdFinish(0, input, priv) + 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) + 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) + + var exitCode uint8 + if code, ok := luaexitcode.TryInt(); ok { + exitCode = uint8(code) + } + + if inp, ok := luaInput.TryString(); ok { + input = inp + } + + if runErr != rt.NilValue { + fmt.Fprintln(os.Stderr, runErr) + } + cmdFinish(exitCode, input, priv) + } +} + +func handleLua(cmdString string) (string, uint8, error) { // First try to load input, essentially compiling to bytecode - fn, err := l.LoadString(cmdString) + chunk, err := l.CompileAndLoadLuaChunk("", []byte(cmdString), rt.TableValue(l.GlobalEnv())) if err != nil && noexecute { fmt.Println(err) /* if lerr, ok := err.(*lua.ApiError); ok { @@ -33,62 +112,102 @@ func runInput(input, origInput string) { } } */ - return + return cmdString, 125, err } // And if there's no syntax errors and -n isnt provided, run if !noexecute { - l.Push(fn) - err = l.PCall(0, lua.MultRet, nil) + if chunk != nil { + _, err = rt.Call1(l.MainThread(), rt.FunctionValue(chunk)) + } } if err == nil { - cmdFinish(0, cmdString, origInput) - return + return cmdString, 0, nil } - // Last option: use sh interpreter - err = execCommand(cmdString, origInput) + return cmdString, 125, err +} + +func handleSh(cmdString string) (string, uint8, error) { + _, _, err := execCommand(cmdString, true) if err != nil { // If input is incomplete, start multiline prompting if syntax.IsIncomplete(err) { + if !interactive { + return cmdString, 126, err + } for { cmdString, err = continuePrompt(strings.TrimSuffix(cmdString, "\\")) if err != nil { break } - err = execCommand(cmdString, origInput) - if syntax.IsIncomplete(err) || strings.HasSuffix(input, "\\") { + _, _, err = execCommand(cmdString, true) + if syntax.IsIncomplete(err) || strings.HasSuffix(cmdString, "\\") { continue } else if code, ok := interp.IsExitStatus(err); ok { - cmdFinish(code, cmdString, origInput) + return cmdString, code, nil } else if err != nil { - fmt.Fprintln(os.Stderr, err) - cmdFinish(1, cmdString, origInput) + return cmdString, 126, err } else { - cmdFinish(0, cmdString, origInput) + return cmdString, 0, nil } - break } } else { if code, ok := interp.IsExitStatus(err); ok { - cmdFinish(code, cmdString, origInput) + return cmdString, code, nil } else { - cmdFinish(126, cmdString, origInput) - fmt.Fprintln(os.Stderr, err) + return cmdString, 126, err } } - } else { - cmdFinish(0, cmdString, origInput) } + + return cmdString, 0, nil } // Run command in sh interpreter -func execCommand(cmd, old string) error { +func execCommand(cmd string, terminalOut bool) (io.Writer, io.Writer, error) { file, err := syntax.NewParser().Parse(strings.NewReader(cmd), "") if err != nil { - return err + return nil, nil, err } - exechandle := func(ctx context.Context, args []string) error { + runner, _ := interp.New() + + var stdout io.Writer + var stderr io.Writer + if terminalOut { + interp.StdIO(os.Stdin, os.Stdout, os.Stderr)(runner) + } else { + stdout = new(bytes.Buffer) + stderr = new(bytes.Buffer) + interp.StdIO(os.Stdin, stdout, stderr)(runner) + } + buf := new(bytes.Buffer) + printer := syntax.NewPrinter() + + var bg bool + for _, stmt := range file.Stmts { + bg = false + if stmt.Background { + bg = true + printer.Print(buf, stmt.Cmd) + + stmtStr := buf.String() + buf.Reset() + jobs.add(stmtStr) + } + + interp.ExecHandler(execHandle(bg))(runner) + err = runner.Run(context.TODO(), stmt) + if err != nil { + return stdout, stderr, err + } + } + + return stdout, stderr, nil +} + +func execHandle(bg bool) interp.ExecHandlerFunc { + return func(ctx context.Context, args []string) error { _, argstring := splitInput(strings.Join(args, " ")) // i dont really like this but it works if aliases.All()[args[0]] != "" { @@ -101,74 +220,176 @@ func execCommand(cmd, old string) error { // If alias was found, use command alias argstring = aliases.Resolve(argstring) - args, _ = shell.Fields(argstring, nil) + var err error + args, err = shell.Fields(argstring, nil) + if err != nil { + return err + } } // If command is defined in Lua then run it - luacmdArgs := l.NewTable() - for _, str := range args[1:] { - luacmdArgs.Append(lua.LString(str)) + luacmdArgs := rt.NewTable() + for i, str := range args[1:] { + luacmdArgs.Set(rt.IntValue(int64(i + 1)), rt.StringValue(str)) } if commands[args[0]] != nil { - err := l.CallByParam(lua.P{ - Fn: commands[args[0]], - NRet: 1, - Protect: true, - }, luacmdArgs) - + luaexitcode, err := rt.Call1(l.MainThread(), rt.FunctionValue(commands[args[0]]), rt.TableValue(luacmdArgs)) if err != nil { - fmt.Fprintln(os.Stderr, - "Error in command:\n\n" + err.Error()) + fmt.Fprintln(os.Stderr, "Error in command:\n" + err.Error()) return interp.NewExitStatus(1) } - luaexitcode := l.Get(-1) var exitcode uint8 - l.Pop(1) - - if code, ok := luaexitcode.(lua.LNumber); luaexitcode != lua.LNil && ok { + if code, ok := luaexitcode.TryInt(); ok { exitcode = uint8(code) + } else if luaexitcode != rt.NilValue { + // deregister commander + delete(commands, args[0]) + fmt.Fprintf(os.Stderr, "Commander did not return number for exit code. %s, you're fired.\n", args[0]) } - cmdFinish(exitcode, argstring, old) return interp.NewExitStatus(exitcode) } err := lookpath(args[0]) - if err == os.ErrPermission { + if err == errNotExec { hooks.Em.Emit("command.no-perm", args[0]) + hooks.Em.Emit("command.not-executable", args[0]) return interp.NewExitStatus(126) } else if err != nil { hooks.Em.Emit("command.not-found", args[0]) return interp.NewExitStatus(127) } - return interp.DefaultExecHandler(2 * time.Second)(ctx, args) - } - runner, _ := interp.New( - interp.StdIO(os.Stdin, os.Stdout, os.Stderr), - interp.ExecHandler(exechandle), - ) - err = runner.Run(context.TODO(), file) + killTimeout := 2 * time.Second + // from here is basically copy-paste of the default exec handler from + // sh/interp but with our job handling + hc := interp.HandlerCtx(ctx) + path, err := interp.LookPathDir(hc.Dir, hc.Env, args[0]) + if err != nil { + fmt.Fprintln(hc.Stderr, err) + return interp.NewExitStatus(127) + } - return err + env := hc.Env + envList := make([]string, 0, 64) + env.Each(func(name string, vr expand.Variable) bool { + if !vr.IsSet() { + // If a variable is set globally but unset in the + // runner, we need to ensure it's not part of the final + // list. Seems like zeroing the element is enough. + // This is a linear search, but this scenario should be + // rare, and the number of variables shouldn't be large. + for i, kv := range envList { + if strings.HasPrefix(kv, name+"=") { + envList[i] = "" + } + } + } + if vr.Exported && vr.Kind == expand.String { + envList = append(envList, name+"="+vr.String()) + } + return true + }) + cmd := exec.Cmd{ + Path: path, + Args: args, + Env: envList, + Dir: hc.Dir, + Stdin: hc.Stdin, + Stdout: hc.Stdout, + Stderr: hc.Stderr, + } + + err = cmd.Start() + var j *job + if bg { + j = jobs.getLatest() + j.setHandle(cmd.Process) + } + if err == nil { + if bg { + j.start(cmd.Process.Pid) + } + + if done := ctx.Done(); done != nil { + go func() { + <-done + + if killTimeout <= 0 || runtime.GOOS == "windows" { + cmd.Process.Signal(os.Kill) + return + } + + // TODO: don't temporarily leak this goroutine + // if the program stops itself with the + // interrupt. + go func() { + time.Sleep(killTimeout) + cmd.Process.Signal(os.Kill) + }() + cmd.Process.Signal(os.Interrupt) + }() + } + + err = cmd.Wait() + } + + var exit uint8 + switch x := err.(type) { + case *exec.ExitError: + // started, but errored - default to 1 if OS + // doesn't have exit statuses + if status, ok := x.Sys().(syscall.WaitStatus); ok { + if status.Signaled() { + if ctx.Err() != nil { + return ctx.Err() + } + exit = uint8(128 + status.Signal()) + goto end + } + exit = uint8(status.ExitStatus()) + goto end + } + exit = 1 + goto end + case *exec.Error: + // did not start + fmt.Fprintf(hc.Stderr, "%v\n", err) + exit = 127 + goto end + case nil: + goto end + default: + return err + } + end: + if bg { + j.exitCode = int(exit) + j.finish() + } + return interp.NewExitStatus(exit) + } } -// custom lookpath function so we know if a command is found *and* has execute permission -func lookpath(file string) error { - skip := []string{"./", "/", "../", "~/"} +func lookpath(file string) error { // custom lookpath function so we know if a command is found *and* is executable + var skip []string + if runtime.GOOS == "windows" { + skip = []string{"./", "../", "~/", "C:"} + } else { + skip = []string{"./", "/", "../", "~/"} + } for _, s := range skip { if strings.HasPrefix(file, s) { - err := findExecutable(file) - return err + return findExecutable(file, false, false) } } for _, dir := range filepath.SplitList(os.Getenv("PATH")) { path := filepath.Join(dir, file) - err := findExecutable(path) - if err == os.ErrPermission { + err := findExecutable(path, true, false) + if err == errNotExec { return err } else if err == nil { return nil @@ -178,17 +399,6 @@ func lookpath(file string) error { return os.ErrNotExist } -func findExecutable(name string) error { - f, err := os.Stat(name) - if err != nil { - return err - } - if m := f.Mode(); !m.IsDir() && m & 0111 != 0 { - return nil - } - return os.ErrPermission -} - func splitInput(input string) ([]string, string) { // end my suffering // TODO: refactor this garbage @@ -242,11 +452,14 @@ func splitInput(input string) ([]string, string) { return cmdArgs, cmdstr.String() } -func cmdFinish(code uint8, cmdstr, oldInput string) { +func cmdFinish(code uint8, cmdstr string, private bool) { // if input has space at the beginning, dont put in history - if interactive && !strings.HasPrefix(oldInput, " ") { - handleHistory(strings.TrimSpace(oldInput)) + if interactive && !private { + handleHistory(cmdstr) } - util.SetField(l, hshMod, "exitCode", lua.LNumber(code), "Exit code of last exected command") - hooks.Em.Emit("command.exit", code, cmdstr) + util.SetField(l, hshMod, "exitCode", rt.IntValue(int64(code)), "Exit code of last exected command") + // using AsValue (to convert to lua type) on an interface which is an int + // results in it being unknown in lua .... ???? + // so we allow the hook handler to take lua runtime Values + hooks.Em.Emit("command.exit", rt.IntValue(int64(code)), cmdstr) } diff --git a/execfile_unix.go b/execfile_unix.go new file mode 100644 index 0000000..3160b85 --- /dev/null +++ b/execfile_unix.go @@ -0,0 +1,24 @@ +// +build linux darwin + +package main + +import ( + "os" +) + +func findExecutable(path string, inPath, dirs bool) error { + f, err := os.Stat(path) + if err != nil { + return err + } + if dirs { + if m := f.Mode(); m & 0111 != 0 { + return nil + } + } else { + if m := f.Mode(); !m.IsDir() && m & 0111 != 0 { + return nil + } + } + return errNotExec +} diff --git a/execfile_windows.go b/execfile_windows.go new file mode 100644 index 0000000..502a595 --- /dev/null +++ b/execfile_windows.go @@ -0,0 +1,37 @@ +// +build windows + +package main + +import ( + "path/filepath" + "os" +) + +func findExecutable(path string, inPath, dirs bool) error { + nameExt := filepath.Ext(path) + pathExts := filepath.SplitList(os.Getenv("PATHEXT")) + if inPath { + if nameExt == "" { + for _, ext := range pathExts { + _, err := os.Stat(path + ext) + if err == nil { + return nil + } + } + } else { + _, err := os.Stat(path) + if err == nil { + if contains(pathExts, nameExt) { return nil } + return errNotExec + } + } + } else { + _, err := os.Stat(path) + if err == nil { + if contains(pathExts, nameExt) { return nil } + return errNotExec + } + } + + return os.ErrNotExist +} diff --git a/gallery/terminal.png b/gallery/terminal.png index 21043cf..b2ff38c 100644 Binary files a/gallery/terminal.png and b/gallery/terminal.png differ diff --git a/go.mod b/go.mod index 4ee71db..e2c9f10 100644 --- a/go.mod +++ b/go.mod @@ -1,21 +1,32 @@ module hilbish -go 1.16 +go 1.17 require ( + github.com/arnodel/golua v0.0.0-20220221163911-dfcf252b6f86 github.com/blackfireio/osinfo v1.0.3 github.com/chuckpreslar/emission v0.0.0-20170206194824-a7ddd980baf9 github.com/maxlandon/readline v0.1.0-beta.0.20211027085530-2b76cabb8036 github.com/pborman/getopt v1.1.0 - github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 - golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 - layeh.com/gopher-luar v1.0.10 mvdan.cc/sh/v3 v3.4.3 ) +require ( + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/arnodel/strftime v0.1.6 // indirect + github.com/evilsocket/islazy v1.10.6 // indirect + github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect + golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect + golang.org/x/text v0.3.6 // indirect +) + replace mvdan.cc/sh/v3 => github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20220306140409-795a84b00b4e replace github.com/maxlandon/readline => ./readline replace layeh.com/gopher-luar => github.com/layeh/gopher-luar v1.0.10 + +replace github.com/arnodel/golua => github.com/Rosettea/golua v0.0.0-20220419183026-6d22d6fec5ac diff --git a/go.sum b/go.sum index 58dcc6f..bc3a192 100644 --- a/go.sum +++ b/go.sum @@ -1,31 +1,24 @@ -github.com/Rosettea/readline-1 v0.0.0-20220302012429-9ce5d23760f7 h1:LoY+kBKqMQqBcilRpVvifBTVve84asa3btpx3D/+IvM= -github.com/Rosettea/readline-1 v0.0.0-20220302012429-9ce5d23760f7/go.mod h1:QiUAvbhg8PzCA4hlafCUl0bKD/0VmcocM4AjqtszAJs= -github.com/Rosettea/readline-1 v0.0.0-20220305004552-071c22768119 h1:rGsc30WTD5hk+oiXrAKsAIwZn5qBeTAdr29y3HhJh9E= -github.com/Rosettea/readline-1 v0.0.0-20220305004552-071c22768119/go.mod h1:QiUAvbhg8PzCA4hlafCUl0bKD/0VmcocM4AjqtszAJs= -github.com/Rosettea/readline-1 v0.0.0-20220305123014-31d4d4214c93 h1:SmOkAEm3O7si8CURZSsSN0ZxCQ8IGiiulw8LMZ1V1Yc= -github.com/Rosettea/readline-1 v0.0.0-20220305123014-31d4d4214c93/go.mod h1:QiUAvbhg8PzCA4hlafCUl0bKD/0VmcocM4AjqtszAJs= -github.com/Rosettea/readline-1 v0.1.0-beta.0.20211207003625-341c7985ad7d h1:KBttN41h/tPahmpaZavviwQ8q4rCkt5CD0HdVmfgPVA= -github.com/Rosettea/readline-1 v0.1.0-beta.0.20211207003625-341c7985ad7d/go.mod h1:QiUAvbhg8PzCA4hlafCUl0bKD/0VmcocM4AjqtszAJs= -github.com/Rosettea/readline-1 v0.1.0-beta.0.20220228022904-61f5e4493011 h1:+a61iNamZiO3Xru+l/1qtpKqqltVfWEm2r/rxH9hXxY= -github.com/Rosettea/readline-1 v0.1.0-beta.0.20220228022904-61f5e4493011/go.mod h1:QiUAvbhg8PzCA4hlafCUl0bKD/0VmcocM4AjqtszAJs= -github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20211022004519-f67a49cb50f5 h1:ygwVRX8gf5MHA0VzSgOdscCEoAJLjM8joEotfQPgAd0= -github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20211022004519-f67a49cb50f5/go.mod h1:R09vh/04ILvP2Gj8/Z9Jd0Dh0ZIvaucowMEs6abQpWs= +github.com/Rosettea/golua v0.0.0-20220419183026-6d22d6fec5ac h1:dtXrgjch8PQyf7C90anZUquB5U3dr8AcMGJofeuirrI= +github.com/Rosettea/golua v0.0.0-20220419183026-6d22d6fec5ac/go.mod h1:9jzpYPiU2is0HVGCiuIOBSXdergHUW44IEjmuN1UrIE= github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20220306140409-795a84b00b4e h1:P2XupP8SaylWaudD1DqbWtZ3mIa8OsE9635LmR+Q+lg= github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20220306140409-795a84b00b4e/go.mod h1:R09vh/04ILvP2Gj8/Z9Jd0Dh0ZIvaucowMEs6abQpWs= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= +github.com/arnodel/edit v0.0.0-20220202110212-dfc8d7a13890/go.mod h1:AcpttpuZBaL9xl8/CX+Em4fBTUbwIkJ66RiAsJlNrBk= +github.com/arnodel/strftime v0.1.6 h1:0hc0pUvk8KhEMXE+htyaOUV42zNcf/csIbjzEFCJqsw= +github.com/arnodel/strftime v0.1.6/go.mod h1:5NbK5XqYK8QpRZpqKNt4OlxLtIB8cotkLk4KTKzJfWs= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/blackfireio/osinfo v1.0.3 h1:Yk2t2GTPjBcESv6nDSWZKO87bGMQgO+Hi9OoXPpxX8c= github.com/blackfireio/osinfo v1.0.3/go.mod h1:Pd987poVNmd5Wsx6PRPw4+w7kLlf9iJxoRKPtPAjOrA= github.com/chuckpreslar/emission v0.0.0-20170206194824-a7ddd980baf9 h1:xz6Nv3zcwO2Lila35hcb0QloCQsc38Al13RNEzWRpX4= github.com/chuckpreslar/emission v0.0.0-20170206194824-a7ddd980baf9/go.mod h1:2wSM9zJkl1UQEFZgSd68NfCgRz1VL1jzy/RjCg+ULrs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.15 h1:cKRCLMj3Ddm54bKSpemfQ8AtYFBhAI2MPmdys22fBdc= github.com/creack/pty v1.1.15/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/evilsocket/islazy v1.10.6 h1:MFq000a1ByoumoJWlytqg0qon0KlBeUfPsDjY0hK0bo= github.com/evilsocket/islazy v1.10.6/go.mod h1:OrwQGYg3DuZvXUfmH+KIZDjwTCbrjy48T24TUpGqVVw= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU= github.com/google/renameio v1.0.1/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -35,34 +28,36 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/layeh/gopher-luar v1.0.10 h1:8NIv4MX1Arz96kK4buGK1D87DyDxKZyq6KKvJ2diHp0= -github.com/layeh/gopher-luar v1.0.10/go.mod h1:TPnIVCZ2RJBndm7ohXyaqfhzjlZ+OA2SZR/YwL8tECk= +github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 h1:LiZB1h0GIcudcDci2bxbqI6DXV8bF8POAnArqvRrIyw= github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0/go.mod h1:F/7q8/HZz+TXjlsoZQQKVYvXTZaFH4QRa3y+j1p7MS0= github.com/pborman/getopt v1.1.0 h1:eJ3aFZroQqq0bWmraivjQNt6Dmm5M0h2JcDW38/Azb0= github.com/pborman/getopt v1.1.0/go.mod h1:FxXoW1Re00sQG/+KIkuSqRL/LwQgSkv7uyac+STFsbk= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.1-0.20210923151022-86f73c517451 h1:d1PiN4RxzIFXCJTvRkvSkKqwtRAl5ZV4lATKtQI0B7I= github.com/rogpeppe/go-internal v1.8.1-0.20210923151022-86f73c517451/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= -github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= -github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 h1:k/gmLsJDWwWqbLCur2yWnJzwQEKRcAHXo6seXGuSwWw= -github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210925032602-92d5a993a665/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7 h1:BXxu8t6QN0G1uff4bzZzSkpsax8+ALqTGUtz08QrV00= -golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 h1:nhht2DYV/Sn3qOayu8lM+cU1ii9sTLUeBQwQQfUHtrs= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210916214954-140adaaadfaf/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= mvdan.cc/editorconfig v0.2.0/go.mod h1:lvnnD3BNdBYkhq+B4uBuFFKatfp02eB6HixDvEz91C0= diff --git a/golibs/bait/bait.go b/golibs/bait/bait.go index 491d9ab..3112903 100644 --- a/golibs/bait/bait.go +++ b/golibs/bait/bait.go @@ -4,13 +4,14 @@ import ( "fmt" "hilbish/util" + rt "github.com/arnodel/golua/runtime" + "github.com/arnodel/golua/lib/packagelib" "github.com/chuckpreslar/emission" - "github.com/yuin/gopher-lua" - "layeh.com/gopher-luar" ) type Bait struct{ Em *emission.Emitter + Loader packagelib.Loader } func New() Bait { @@ -19,15 +20,27 @@ func New() Bait { emitter.Off(hookname, hookfunc) fmt.Println(err) }) - return Bait{ + b := Bait{ Em: emitter, } + b.Loader = packagelib.Loader{ + Load: b.loaderFunc, + Name: "bait", + } + + return b } -func (b *Bait) Loader(L *lua.LState) int { - mod := L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{}) +func (b *Bait) loaderFunc(rtm *rt.Runtime) (rt.Value, func()) { + exports := map[string]util.LuaExport{ + "catch": util.LuaExport{b.bcatch, 2, false}, + "catchOnce": util.LuaExport{b.bcatchOnce, 2, false}, + "throw": util.LuaExport{b.bthrow, 1, true}, + } + mod := rt.NewTable() + util.SetExports(rtm, mod, exports) - util.Document(L, mod, + util.Document(mod, `Bait is the event emitter for Hilbish. Why name it bait? Because it throws hooks that you can catch (emits events that you can listen to) and because why not, fun naming @@ -36,35 +49,81 @@ in on hooks to know when certain things have happened, like when you've changed directory, a command has failed, etc. To find all available hooks, see doc hooks.`) - L.SetField(mod, "throw", luar.New(L, b.bthrow)) - L.SetField(mod, "catch", luar.New(L, b.bcatch)) - L.SetField(mod, "catchOnce", luar.New(L, b.bcatchOnce)) + return rt.TableValue(mod), nil +} - L.Push(mod) - - return 1 +func handleHook(t *rt.Thread, c *rt.GoCont, name string, catcher *rt.Closure, args ...interface{}) { + funcVal := rt.FunctionValue(catcher) + var luaArgs []rt.Value + for _, arg := range args { + var luarg rt.Value + switch arg.(type) { + case rt.Value: luarg = arg.(rt.Value) + default: luarg = rt.AsValue(arg) + } + luaArgs = append(luaArgs, luarg) + } + _, err := rt.Call1(t, funcVal, luaArgs...) + if err != nil { + e := rt.NewError(rt.StringValue(err.Error())) + e = e.AddContext(c.Next(), 1) + // panicking here won't actually cause hilbish to panic and instead will + // print the error and remove the hook (look at emission recover from above) + panic(e) + } } // throw(name, ...args) // Throws a hook with `name` with the provided `args` // --- @param name string // --- @vararg any -func (b *Bait) bthrow(name string, args ...interface{}) { - b.Em.Emit(name, args...) +func (b *Bait) bthrow(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + name, err := c.StringArg(0) + if err != nil { + return nil, err + } + ifaceSlice := make([]interface{}, len(c.Etc())) + for i, v := range c.Etc() { + ifaceSlice[i] = v + } + b.Em.Emit(name, ifaceSlice...) + + return c.Next(), nil } // catch(name, cb) // Catches a hook with `name`. Runs the `cb` when it is thrown // --- @param name string // --- @param cb function -func (b *Bait) bcatch(name string, catcher func(...interface{})) { - b.Em.On(name, catcher) +func (b *Bait) bcatch(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + name, catcher, err := util.HandleStrCallback(t, c) + if err != nil { + return nil, err + } + + b.Em.On(name, func(args ...interface{}) { + handleHook(t, c, name, catcher, args...) + }) + + return c.Next(), nil } // catchOnce(name, cb) // Same as catch, but only runs the `cb` once and then removes the hook // --- @param name string // --- @param cb function -func (b *Bait) bcatchOnce(name string, catcher func(...interface{})) { - b.Em.Once(name, catcher) +func (b *Bait) bcatchOnce(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + name, catcher, err := util.HandleStrCallback(t, c) + if err != nil { + return nil, err + } + + b.Em.Once(name, func(args ...interface{}) { + handleHook(t, c, name, catcher, args...) + }) + + return c.Next(), nil } diff --git a/golibs/commander/commander.go b/golibs/commander/commander.go index 3fbc1a5..d279e0c 100644 --- a/golibs/commander/commander.go +++ b/golibs/commander/commander.go @@ -3,52 +3,68 @@ package commander import ( "hilbish/util" + rt "github.com/arnodel/golua/runtime" + "github.com/arnodel/golua/lib/packagelib" "github.com/chuckpreslar/emission" - "github.com/yuin/gopher-lua" ) type Commander struct{ Events *emission.Emitter + Loader packagelib.Loader } func New() Commander { - return Commander{ + c := Commander{ Events: emission.NewEmitter(), } + c.Loader = packagelib.Loader{ + Load: c.loaderFunc, + Name: "commander", + } + + return c } -func (c *Commander) Loader(L *lua.LState) int { - exports := map[string]lua.LGFunction{ - "register": c.cregister, - "deregister": c.cderegister, +func (c *Commander) loaderFunc(rtm *rt.Runtime) (rt.Value, func()) { + exports := map[string]util.LuaExport{ + "register": util.LuaExport{c.cregister, 2, false}, + "deregister": util.LuaExport{c.cderegister, 1, false}, } - mod := L.SetFuncs(L.NewTable(), exports) - util.Document(L, mod, "Commander is Hilbish's custom command library, a way to write commands in Lua.") - L.Push(mod) + mod := rt.NewTable() + util.SetExports(rtm, mod, exports) + util.Document(mod, "Commander is Hilbish's custom command library, a way to write commands in Lua.") - return 1 + return rt.TableValue(mod), nil } // register(name, cb) // Register a command with `name` that runs `cb` when ran // --- @param name string // --- @param cb function -func (c *Commander) cregister(L *lua.LState) int { - cmdName := L.CheckString(1) - cmd := L.CheckFunction(2) +func (c *Commander) cregister(t *rt.Thread, ct *rt.GoCont) (rt.Cont, error) { + cmdName, cmd, err := util.HandleStrCallback(t, ct) + if err != nil { + return nil, err + } c.Events.Emit("commandRegister", cmdName, cmd) - return 0 + return ct.Next(), err } // deregister(name) // Deregisters any command registered with `name` // --- @param name string -func (c *Commander) cderegister(L *lua.LState) int { - cmdName := L.CheckString(1) +func (c *Commander) cderegister(t *rt.Thread, ct *rt.GoCont) (rt.Cont, error) { + if err := ct.Check1Arg(); err != nil { + return nil, err + } + cmdName, err := ct.StringArg(0) + if err != nil { + return nil, err + } c.Events.Emit("commandDeregister", cmdName) - return 0 + return ct.Next(), err } diff --git a/golibs/fs/fs.go b/golibs/fs/fs.go index f23c3d7..042f2a8 100644 --- a/golibs/fs/fs.go +++ b/golibs/fs/fs.go @@ -1,5 +1,3 @@ -// 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. package fs import ( @@ -8,51 +6,70 @@ import ( "strings" "hilbish/util" - "github.com/yuin/gopher-lua" + + rt "github.com/arnodel/golua/runtime" + "github.com/arnodel/golua/lib/packagelib" ) -func Loader(L *lua.LState) int { - mod := L.SetFuncs(L.NewTable(), exports) +var Loader = packagelib.Loader{ + Load: loaderFunc, + Name: "fs", +} - util.Document(L, mod, `The fs module provides easy and simple access to +func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) { + exports := map[string]util.LuaExport{ + "cd": util.LuaExport{fcd, 1, false}, + "mkdir": util.LuaExport{fmkdir, 2, false}, + "stat": util.LuaExport{fstat, 1, false}, + "readdir": util.LuaExport{freaddir, 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.`) - L.Push(mod) - return 1 -} - -var exports = map[string]lua.LGFunction{ - "cd": fcd, - "mkdir": fmkdir, - "stat": fstat, - "readdir": freaddir, + return rt.TableValue(mod), nil } // cd(dir) // Changes directory to `dir` // --- @param dir string -func fcd(L *lua.LState) int { - path := L.CheckString(1) - - err := os.Chdir(strings.TrimSpace(path)) +func fcd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + path, err := c.StringArg(0) if err != nil { - e := err.(*os.PathError).Err.Error() - L.RaiseError(e + ": " + path) + return nil, err } - return 0 + err = os.Chdir(strings.TrimSpace(path)) + if err != nil { + return nil, err + } + + return c.Next(), err } // mkdir(name, recursive) // Makes a directory called `name`. If `recursive` is true, it will create its parent directories. // --- @param name string // --- @param recursive boolean -func fmkdir(L *lua.LState) int { - dirname := L.CheckString(1) - recursive := L.ToBool(2) +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) + if err != nil { + return nil, err + } + recursive, err := c.BoolArg(1) + if err != nil { + return nil, err + } path := strings.TrimSpace(dirname) - var err error if recursive { err = os.MkdirAll(path, 0744) @@ -60,51 +77,58 @@ func fmkdir(L *lua.LState) int { err = os.Mkdir(path, 0744) } if err != nil { - L.RaiseError(err.Error() + ": " + path) + return nil, err } - return 0 + return c.Next(), err } // stat(path) // Returns info about `path` // --- @param path string -func fstat(L *lua.LState) int { - path := L.CheckString(1) +func fstat(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + path, err := c.StringArg(0) + if err != nil { + return nil, err + } pathinfo, err := os.Stat(path) if err != nil { - L.RaiseError(err.Error() + ": " + path) - return 0 + return nil, err } - statTbl := L.NewTable() - L.SetField(statTbl, "name", lua.LString(pathinfo.Name())) - L.SetField(statTbl, "size", lua.LNumber(pathinfo.Size())) - L.SetField(statTbl, "mode", lua.LString("0" + strconv.FormatInt(int64(pathinfo.Mode().Perm()), 8))) - L.SetField(statTbl, "isDir", lua.LBool(pathinfo.IsDir())) - L.Push(statTbl) - - return 1 + statTbl := rt.NewTable() + statTbl.Set(rt.StringValue("name"), rt.StringValue(pathinfo.Name())) + statTbl.Set(rt.StringValue("size"), rt.IntValue(pathinfo.Size())) + statTbl.Set(rt.StringValue("mode"), rt.StringValue("0" + strconv.FormatInt(int64(pathinfo.Mode().Perm()), 8))) + statTbl.Set(rt.StringValue("isDir"), rt.BoolValue(pathinfo.IsDir())) + + return c.PushingNext1(t.Runtime, rt.TableValue(statTbl)), nil } // readdir(dir) // Returns a table of files in `dir` // --- @param dir string // --- @return table -func freaddir(L *lua.LState) int { - dir := L.CheckString(1) - names := L.NewTable() +func freaddir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + dir, err := c.StringArg(0) + if err != nil { + return nil, err + } + names := rt.NewTable() dirEntries, err := os.ReadDir(dir) if err != nil { - L.RaiseError(err.Error() + ": " + dir) - return 0 + return nil, err } - for _, entry := range dirEntries { - names.Append(lua.LString(entry.Name())) + for i, entry := range dirEntries { + names.Set(rt.IntValue(int64(i + 1)), rt.StringValue(entry.Name())) } - L.Push(names) - - return 1 + return c.PushingNext1(t.Runtime, rt.TableValue(names)), nil } diff --git a/golibs/terminal/terminal.go b/golibs/terminal/terminal.go index b52523e..df1755c 100644 --- a/golibs/terminal/terminal.go +++ b/golibs/terminal/terminal.go @@ -5,76 +5,78 @@ import ( "hilbish/util" + rt "github.com/arnodel/golua/runtime" + "github.com/arnodel/golua/lib/packagelib" "golang.org/x/term" - "github.com/yuin/gopher-lua" ) var termState *term.State - -func Loader(L *lua.LState) int { - mod := L.SetFuncs(L.NewTable(), exports) - util.Document(L, mod, "The terminal library is a simple and lower level library for certain terminal interactions.") - - L.Push(mod) - - return 1 +var Loader = packagelib.Loader{ + Load: loaderFunc, + Name: "terminal", } -var exports = map[string]lua.LGFunction{ - "setRaw": termraw, - "restoreState": termrestoreState, - "size": termsize, - "saveState": termsaveState, +func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) { + exports := map[string]util.LuaExport{ + "setRaw": util.LuaExport{termsetRaw, 0, false}, + "restoreState": util.LuaExport{termrestoreState, 0, false}, + "size": util.LuaExport{termsize, 0, false}, + "saveState": util.LuaExport{termsaveState, 0, false}, + } + + mod := rt.NewTable() + util.SetExports(rtm, mod, exports) + util.Document(mod, "The terminal library is a simple and lower level library for certain terminal interactions.") + + return rt.TableValue(mod), nil } // size() // Gets the dimensions of the terminal. Returns a table with `width` and `height` // Note: this is not the size in relation to the dimensions of the display -func termsize(L *lua.LState) int { +func termsize(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { w, h, err := term.GetSize(int(os.Stdin.Fd())) if err != nil { - L.RaiseError(err.Error()) - return 0 + return nil, err } - dimensions := L.NewTable() - L.SetField(dimensions, "width", lua.LNumber(w)) - L.SetField(dimensions, "height", lua.LNumber(h)) - L.Push(dimensions) - return 1 + dimensions := rt.NewTable() + dimensions.Set(rt.StringValue("width"), rt.IntValue(int64(w))) + dimensions.Set(rt.StringValue("height"), rt.IntValue(int64(h))) + + return c.PushingNext1(t.Runtime, rt.TableValue(dimensions)), nil } // saveState() // Saves the current state of the terminal -func termsaveState(L *lua.LState) int { +func termsaveState(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { state, err := term.GetState(int(os.Stdin.Fd())) if err != nil { - L.RaiseError(err.Error()) - return 0 + return nil, err } termState = state - return 0 + return c.Next(), nil } // restoreState() // Restores the last saved state of the terminal -func termrestoreState(L *lua.LState) int { +func termrestoreState(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { err := term.Restore(int(os.Stdin.Fd()), termState) if err != nil { - L.RaiseError(err.Error()) + return nil, err } - return 0 + return c.Next(), nil } // setRaw() // Puts the terminal in raw mode -func termraw(L *lua.LState) int { +func termsetRaw(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { _, err := term.MakeRaw(int(os.Stdin.Fd())) if err != nil { - L.RaiseError(err.Error()) + return nil, err } - return 0 + return c.Next(), nil } diff --git a/history.go b/history.go index 2fcec56..b666515 100644 --- a/history.go +++ b/history.go @@ -78,3 +78,9 @@ func (h *fileHistory) Len() int { func (h *fileHistory) Dump() interface{} { return h.items } + +func (h *fileHistory) clear() { + h.items = []string{} + h.f.Truncate(0) + h.f.Sync() +} diff --git a/job.go b/job.go new file mode 100644 index 0000000..b1d50b3 --- /dev/null +++ b/job.go @@ -0,0 +1,142 @@ +package main + +import ( + "sync" + "os" + + "hilbish/util" + + rt "github.com/arnodel/golua/runtime" +) + +var jobs *jobHandler + +type job struct { + cmd string + running bool + id int + pid int + exitCode int + proc *os.Process +} + +func (j *job) start(pid int) { + j.pid = pid + j.running = true + hooks.Em.Emit("job.start", j.lua()) +} + +func (j *job) stop() { + // finish will be called in exec handle + j.proc.Kill() +} + +func (j *job) finish() { + j.running = false + hooks.Em.Emit("job.done", j.lua()) +} + +func (j *job) setHandle(handle *os.Process) { + j.proc = handle +} + +func (j *job) lua() rt.Value { + jobFuncs := map[string]util.LuaExport{ + "stop": {j.luaStop, 0, false}, + } + luaJob := rt.NewTable() + util.SetExports(l, luaJob, jobFuncs) + + luaJob.Set(rt.StringValue("cmd"), rt.StringValue(j.cmd)) + luaJob.Set(rt.StringValue("running"), rt.BoolValue(j.running)) + luaJob.Set(rt.StringValue("id"), rt.IntValue(int64(j.id))) + luaJob.Set(rt.StringValue("pid"), rt.IntValue(int64(j.pid))) + luaJob.Set(rt.StringValue("exitCode"), rt.IntValue(int64(j.exitCode))) + + return rt.TableValue(luaJob) +} + +func (j *job) luaStop(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if j.running { + j.stop() + } + + return c.Next(), nil +} + +type jobHandler struct { + jobs map[int]*job + latestID int + mu *sync.RWMutex +} + +func newJobHandler() *jobHandler { + return &jobHandler{ + jobs: make(map[int]*job), + latestID: 0, + mu: &sync.RWMutex{}, + } +} + +func (j *jobHandler) add(cmd string) { + j.mu.Lock() + defer j.mu.Unlock() + + j.latestID++ + j.jobs[j.latestID] = &job{ + cmd: cmd, + running: false, + id: j.latestID, + } +} + +func (j *jobHandler) getLatest() *job { + j.mu.RLock() + defer j.mu.RUnlock() + + return j.jobs[j.latestID] +} + +func (j *jobHandler) loader(rtm *rt.Runtime) *rt.Table { + jobFuncs := map[string]util.LuaExport{ + "all": {j.luaAllJobs, 0, false}, + "get": {j.luaGetJob, 1, false}, + } + + luaJob := rt.NewTable() + util.SetExports(rtm, luaJob, jobFuncs) + + return luaJob +} + +func (j *jobHandler) luaGetJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + j.mu.RLock() + defer j.mu.RUnlock() + + if err := c.Check1Arg(); err != nil { + return nil, err + } + jobID, err := c.IntArg(0) + if err != nil { + return nil, err + } + + job := j.jobs[int(jobID)] + if job == nil { + return c.Next(), nil + } + + return c.PushingNext1(t.Runtime, job.lua()), nil +} + +func (j *jobHandler) luaAllJobs(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + j.mu.RLock() + defer j.mu.RUnlock() + + jobTbl := rt.NewTable() + for id, job := range j.jobs { + jobTbl.Set(rt.IntValue(int64(id)), job.lua()) + } + + return c.PushingNext1(t.Runtime, rt.TableValue(jobTbl)), nil +} diff --git a/libs/ansikit/init.lua b/libs/ansikit/init.lua index 5f7b9de..60bb8bf 100644 --- a/libs/ansikit/init.lua +++ b/libs/ansikit/init.lua @@ -89,21 +89,25 @@ end ansikit.print = function(text) io.write(ansikit.format(text)) + io.flush() return ansikit end ansikit.printCode = function(code, terminate) io.write(ansikit.getCode(code, terminate)) + io.flush() return ansikit end ansikit.printCSI = function(code, endc) io.write(ansikit.getCSI(code, endc)) + io.flush() return ansikit end ansikit.println = function(text) - print(ansikit.print(text)) + io.write(ansikit.format(text) .. "\n") + io.flush() return ansikit end diff --git a/libs/lunacolors b/libs/lunacolors index 5a59d0f..b362397 160000 --- a/libs/lunacolors +++ b/libs/lunacolors @@ -1 +1 @@ -Subproject commit 5a59d0f4543eb982593750c52f7393e2fd2d15f9 +Subproject commit b362397a83e4516415c809c7d690b52e79a95f6e diff --git a/lua.go b/lua.go index 2636332..3b925c6 100644 --- a/lua.go +++ b/lua.go @@ -4,40 +4,42 @@ import ( "fmt" "os" + "hilbish/util" "hilbish/golibs/bait" "hilbish/golibs/commander" "hilbish/golibs/fs" "hilbish/golibs/terminal" - "github.com/yuin/gopher-lua" + rt "github.com/arnodel/golua/runtime" + "github.com/arnodel/golua/lib" ) var minimalconf = `hilbish.prompt '& '` func luaInit() { - l = lua.NewState() - l.OpenLibs() + l = rt.New(os.Stdout) + lib.LoadAll(l) + lib.LoadLibs(l, hilbishLoader) // yes this is stupid, i know - l.PreloadModule("hilbish", hilbishLoader) - l.DoString("hilbish = require 'hilbish'") + util.DoString(l, "hilbish = require 'hilbish'") // Add fs and terminal module module to Lua - l.PreloadModule("fs", fs.Loader) - l.PreloadModule("terminal", terminal.Loader) + lib.LoadLibs(l, fs.Loader) + lib.LoadLibs(l, terminal.Loader) cmds := commander.New() // When a command from Lua is added, register it for use - cmds.Events.On("commandRegister", func(cmdName string, cmd *lua.LFunction) { + cmds.Events.On("commandRegister", func(cmdName string, cmd *rt.Closure) { commands[cmdName] = cmd }) cmds.Events.On("commandDeregister", func(cmdName string) { delete(commands, cmdName) }) - l.PreloadModule("commander", cmds.Loader) + lib.LoadLibs(l, cmds.Loader) hooks = bait.New() - l.PreloadModule("bait", hooks.Loader) + lib.LoadLibs(l, hooks.Loader) // Add Ctrl-C handler hooks.Em.On("signal.sigint", func() { @@ -46,29 +48,28 @@ func luaInit() { } }) - l.SetGlobal("complete", l.NewFunction(hlcomplete)) - // Add more paths that Lua can require from - l.DoString("package.path = package.path .. " + requirePaths) - - err := l.DoFile("prelude/init.lua") + err := util.DoString(l, "package.path = package.path .. " + requirePaths) if err != nil { - err = l.DoFile(preloadPath) + fmt.Fprintln(os.Stderr, "Could not add preload 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.") + fmt.Fprintln(os.Stderr, "Missing preload file, builtins may be missing.") } } } + func runConfig(confpath string) { if !interactive { return } - err := l.DoFile(confpath) + err := util.DoFile(l, confpath) if err != nil { - fmt.Fprintln(os.Stderr, err, - "\nAn error has occured while loading your config! Falling back to minimal default config.") - - l.DoString(minimalconf) + fmt.Fprintln(os.Stderr, err, "\nAn error has occured while loading your config! Falling back to minimal default config.") + util.DoString(l, minimalconf) } } diff --git a/main.go b/main.go index 55db5e4..ff04430 100644 --- a/main.go +++ b/main.go @@ -10,20 +10,21 @@ import ( "runtime" "strings" + "hilbish/util" "hilbish/golibs/bait" + rt "github.com/arnodel/golua/runtime" "github.com/pborman/getopt" - "github.com/yuin/gopher-lua" "github.com/maxlandon/readline" "golang.org/x/term" ) var ( - l *lua.LState + l *rt.Runtime lr *lineReader - commands = map[string]*lua.LFunction{} - luaCompletions = map[string]*lua.LFunction{} + commands = map[string]*rt.Closure{} + luaCompletions = map[string]*rt.Closure{} confDir string userDataDir string @@ -43,7 +44,7 @@ func main() { // i honestly dont know what directories to use for this switch runtime.GOOS { - case "linux": + case "linux", "darwin": userDataDir = getenv("XDG_DATA_HOME", curuser.HomeDir + "/.local/share") default: // this is fine on windows, dont know about others @@ -55,7 +56,7 @@ func main() { defaultConfDir = filepath.Join(confDir, "hilbish") } else { // else do ~ substitution - defaultConfDir = expandHome(defaultHistDir) + defaultConfDir = filepath.Join(expandHome(defaultConfDir), "hilbish") } defaultConfPath = filepath.Join(defaultConfDir, "init.lua") if defaultHistDir == "" { @@ -142,27 +143,28 @@ func main() { scanner := bufio.NewScanner(bufio.NewReader(os.Stdin)) for scanner.Scan() { text := scanner.Text() - runInput(text, text) + runInput(text, true) } + exit(0) } if *cmdflag != "" { - runInput(*cmdflag, *cmdflag) + runInput(*cmdflag, true) } if getopt.NArgs() > 0 { - luaArgs := l.NewTable() - for _, arg := range getopt.Args() { - luaArgs.Append(lua.LString(arg)) + luaArgs := rt.NewTable() + for i, arg := range getopt.Args() { + luaArgs.Set(rt.IntValue(int64(i)), rt.StringValue(arg)) } - l.SetGlobal("args", luaArgs) - err := l.DoFile(getopt.Arg(0)) + l.GlobalEnv().Set(rt.StringValue("args"), rt.TableValue(luaArgs)) + err := util.DoFile(l, getopt.Arg(0)) if err != nil { fmt.Fprintln(os.Stderr, err) - os.Exit(1) + exit(1) } - os.Exit(0) + exit(0) } initialized = true @@ -185,7 +187,10 @@ input: fmt.Println("^C") continue } - oldInput := input + var priv bool + if strings.HasPrefix(input, " ") { + priv = true + } input = strings.TrimSpace(input) if len(input) == 0 { @@ -198,6 +203,8 @@ input: for { input, err = continuePrompt(input) if err != nil { + running = true + lr.SetPrompt(fmtPrompt(prompt)) goto input // continue inside nested loop } if !strings.HasSuffix(input, "\\") { @@ -206,7 +213,7 @@ input: } } - runInput(input, oldInput) + runInput(input, priv) termwidth, _, err := term.GetSize(0) if err != nil { @@ -268,8 +275,7 @@ func handleHistory(cmd string) { func expandHome(path string) string { homedir := curuser.HomeDir - - return strings.Replace(defaultHistDir, "~", homedir, 1) + return strings.Replace(path, "~", homedir, 1) } func removeDupes(slice []string) []string { @@ -284,3 +290,21 @@ func removeDupes(slice []string) []string { return newSlice } + +func contains(s []string, e string) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +} + +func exit(code int) { + // wait for all timers to finish before exiting + for { + if timers.running == 0 { + os.Exit(code) + } + } +} diff --git a/prelude/init.lua b/prelude/init.lua index a9935e6..a5b6568 100644 --- a/prelude/init.lua +++ b/prelude/init.lua @@ -8,7 +8,7 @@ local _ = require 'succulent' -- Function additions local oldDir = hilbish.cwd() local shlvl = tonumber(os.getenv 'SHLVL') -if shlvl ~= nil then os.setenv('SHLVL', shlvl + 1) else os.setenv('SHLVL', 0) end +if shlvl ~= nil then os.setenv('SHLVL', tostring(shlvl + 1)) else os.setenv('SHLVL', '0') end -- Builtins local recentDirs = {} @@ -168,6 +168,9 @@ 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. @@ -214,14 +217,6 @@ do end end, }) - - bait.catch('command.exit', function () - for key, value in pairs(virt_G) do - if type(value) == 'string' then - virt_G[key] = os.getenv(key) - end - end - end) end commander.register('cdr', function(args) @@ -263,7 +258,7 @@ bait.catch('command.not-found', function(cmd) print(string.format('hilbish: %s not found', cmd)) end) -bait.catch('command.no-perm', function(cmd) - print(string.format('hilbish: %s: no permission', cmd)) +bait.catch('command.not-executable', function(cmd) + print(string.format('hilbish: %s: not executable', cmd)) end) diff --git a/readline/codes.go b/readline/codes.go index 8787f07..492bc72 100644 --- a/readline/codes.go +++ b/readline/codes.go @@ -34,32 +34,39 @@ const ( charCtrlHat // ^^ charCtrlUnderscore // ^_ charBackspace2 = 127 // ASCII 1963 - ) // Escape sequences var ( - seqUp = string([]byte{27, 91, 65}) - seqDown = string([]byte{27, 91, 66}) - seqForwards = string([]byte{27, 91, 67}) - seqBackwards = string([]byte{27, 91, 68}) - seqHome = string([]byte{27, 91, 72}) - seqHomeSc = string([]byte{27, 91, 49, 126}) - seqEnd = string([]byte{27, 91, 70}) - seqEndSc = string([]byte{27, 91, 52, 126}) - seqDelete = string([]byte{27, 91, 51, 126}) - seqShiftTab = string([]byte{27, 91, 90}) - seqAltQuote = string([]byte{27, 34}) // Added for showing registers ^[" - seqAltR = string([]byte{27, 114}) // Used for alternative history + seqUp = string([]byte{27, 91, 65}) + seqDown = string([]byte{27, 91, 66}) + seqForwards = string([]byte{27, 91, 67}) + seqBackwards = string([]byte{27, 91, 68}) + seqHome = string([]byte{27, 91, 72}) + seqHomeSc = string([]byte{27, 91, 49, 126}) + seqEnd = string([]byte{27, 91, 70}) + seqEndSc = string([]byte{27, 91, 52, 126}) + seqDelete = string([]byte{27, 91, 51, 126}) + seqDelete2 = string([]byte{27, 91, 80}) + seqCtrlDelete = string([]byte{27, 91, 51, 59, 53, 126}) + seqCtrlDelete2 = string([]byte{27, 91, 77}) + seqAltDelete = string([]byte{27, 91, 51, 59, 51, 126}) + seqShiftTab = string([]byte{27, 91, 90}) + seqAltQuote = string([]byte{27, 34}) // Added for showing registers ^[" + seqAltB = string([]byte{27, 98}) + seqAltD = string([]byte{27, 100}) + seqAltF = string([]byte{27, 102}) + seqAltR = string([]byte{27, 114}) // Used for alternative history + seqAltBackspace = string([]byte{27, 127}) ) const ( seqPosSave = "\x1b[s" seqPosRestore = "\x1b[u" - seqClearLineAfer = "\x1b[0k" - seqClearLineBefore = "\x1b[1k" - seqClearLine = "\x1b[2k" + seqClearLineAfer = "\x1b[0K" + seqClearLineBefore = "\x1b[1K" + seqClearLine = "\x1b[2K" seqClearScreenBelow = "\x1b[0J" seqClearScreen = "\x1b[2J" // Clears screen fully seqCursorTopLeft = "\x1b[H" // Clears screen and places cursor on top-left @@ -78,6 +85,7 @@ const ( seqBold = "\x1b[1m" seqUnderscore = "\x1b[4m" seqBlink = "\x1b[5m" + seqInvert = "\x1b[7m" ) // Text colours diff --git a/readline/comp-grid.go b/readline/comp-grid.go index 8abdc63..2679835 100644 --- a/readline/comp-grid.go +++ b/readline/comp-grid.go @@ -121,7 +121,7 @@ func (g *CompletionGroup) writeGrid(rl *Instance) (comp string) { } if (x == g.tcPosX && y == g.tcPosY) && (g.isCurrent) { - comp += seqCtermFg255 + seqFgBlackBright + comp += seqInvert } comp += fmt.Sprintf("%-"+cellWidth+"s %s", g.Suggestions[i], seqReset) diff --git a/readline/comp-list.go b/readline/comp-list.go index ce158f4..42add2f 100644 --- a/readline/comp-list.go +++ b/readline/comp-list.go @@ -188,7 +188,7 @@ func (g *CompletionGroup) writeList(rl *Instance) (comp string) { // function highlights the cell depending on current selector place. highlight := func(y int, x int) string { if y == g.tcPosY && x == g.tcPosX && g.isCurrent { - return seqCtermFg255 + seqFgBlackBright + return seqInvert } return "" } diff --git a/readline/comp-map.go b/readline/comp-map.go index d183eaa..42b56cf 100644 --- a/readline/comp-map.go +++ b/readline/comp-map.go @@ -101,7 +101,7 @@ func (g *CompletionGroup) writeMap(rl *Instance) (comp string) { // Highlighting function highlight := func(y int) string { if y == g.tcPosY && g.isCurrent { - return seqCtermFg255 + seqFgBlackBright + return seqInvert } return "" } diff --git a/readline/cursor.go b/readline/cursor.go index 262d939..f313ef4 100644 --- a/readline/cursor.go +++ b/readline/cursor.go @@ -100,12 +100,12 @@ func moveCursorBackwards(i int) { printf("\x1b[%dD", i) } -func (rl *Instance) backspace() { +func (rl *Instance) backspace(forward bool) { if len(rl.line) == 0 || rl.pos == 0 { return } - rl.deleteBackspace() + rl.deleteBackspace(forward) } func (rl *Instance) moveCursorByAdjust(adjust int) { diff --git a/readline/events.go b/readline/events.go index a89e5b7..5d63076 100644 --- a/readline/events.go +++ b/readline/events.go @@ -7,7 +7,7 @@ type EventReturn struct { ForwardKey bool ClearHelpers bool CloseReadline bool - HintText []rune + InfoText []rune NewLine []rune NewPos int } diff --git a/readline/hint.go b/readline/hint.go index 37d99ac..6c6a67c 100644 --- a/readline/hint.go +++ b/readline/hint.go @@ -4,10 +4,12 @@ import "regexp" // SetHintText - a nasty function to force writing a new hint text. It does not update helpers, it just renders // them, so the hint will survive until the helpers (thus including the hint) will be updated/recomputed. +/* func (rl *Instance) SetHintText(s string) { rl.hintText = []rune(s) rl.renderHelpers() } +*/ func (rl *Instance) getHintText() { @@ -27,7 +29,7 @@ func (rl *Instance) getHintText() { // writeHintText - only writes the hint text and computes its offsets. func (rl *Instance) writeHintText() { if len(rl.hintText) == 0 { - rl.hintY = 0 + //rl.hintY = 0 return } @@ -41,16 +43,16 @@ func (rl *Instance) writeHintText() { wrapped, hintLen := WrapText(string(rl.hintText), width) offset += hintLen - rl.hintY = offset +// rl.hintY = offset hintText := string(wrapped) if len(hintText) > 0 { - print("\r" + rl.HintFormatting + string(hintText) + seqReset) + print(rl.HintFormatting + string(hintText) + seqReset) } } func (rl *Instance) resetHintText() { - rl.hintY = 0 + //rl.hintY = 0 rl.hintText = []rune{} } diff --git a/readline/history.go b/readline/history.go index 8aff2a8..41200c6 100644 --- a/readline/history.go +++ b/readline/history.go @@ -183,13 +183,13 @@ func (rl *Instance) completeHistory() (hist []*CompletionGroup) { return } history = rl.altHistory - rl.histHint = []rune(rl.altHistName + ": ") + rl.histInfo = []rune(rl.altHistName + ": ") } else { if rl.mainHistory == nil { return } history = rl.mainHistory - rl.histHint = []rune(rl.mainHistName + ": ") + rl.histInfo = []rune(rl.mainHistName + ": ") } hist[0].init(rl) diff --git a/readline/info.go b/readline/info.go new file mode 100644 index 0000000..269157d --- /dev/null +++ b/readline/info.go @@ -0,0 +1,56 @@ +package readline + +import "regexp" + +// SetInfoText - a nasty function to force writing a new info text. It does not update helpers, it just renders +// them, so the info will survive until the helpers (thus including the info) will be updated/recomputed. +func (rl *Instance) SetInfoText(s string) { + rl.infoText = []rune(s) + rl.renderHelpers() +} + +func (rl *Instance) getInfoText() { + + if !rl.modeAutoFind && !rl.modeTabFind { + // Return if no infos provided by the user/engine + if rl.InfoText == nil { + rl.resetInfoText() + return + } + // The info text also works with the virtual completion line system. + // This way, the info is also refreshed depending on what we are pointing + // at with our cursor. + rl.infoText = rl.InfoText(rl.getCompletionLine()) + } +} + +// writeInfoText - only writes the info text and computes its offsets. +func (rl *Instance) writeInfoText() { + if len(rl.infoText) == 0 { + rl.infoY = 0 + return + } + + width := GetTermWidth() + + // Wraps the line, and counts the number of newlines in the string, + // adjusting the offset as well. + re := regexp.MustCompile(`\r?\n`) + newlines := re.Split(string(rl.infoText), -1) + offset := len(newlines) + + wrapped, infoLen := WrapText(string(rl.infoText), width) + offset += infoLen + rl.infoY = offset + + infoText := string(wrapped) + + if len(infoText) > 0 { + print("\r" + rl.InfoFormatting + string(infoText) + seqReset) + } +} + +func (rl *Instance) resetInfoText() { + rl.infoY = 0 + rl.infoText = []rune{} +} diff --git a/readline/instance.go b/readline/instance.go index 48e4398..ec6b861 100644 --- a/readline/instance.go +++ b/readline/instance.go @@ -30,11 +30,13 @@ type Instance struct { Multiline bool // If set to true, the shell will have a two-line prompt. MultilinePrompt string // If multiline is true, this is the content of the 2nd line. - mainPrompt string // If multiline true, the full prompt string / If false, the 1st line of the prompt - realPrompt []rune // The prompt that is actually on the same line as the beginning of the input line. - defaultPrompt []rune - promptLen int - stillOnRefresh bool // True if some logs have printed asynchronously since last loop. Check refresh prompt funcs + mainPrompt string // If multiline true, the full prompt string / If false, the 1st line of the prompt + rightPrompt string + rightPromptLen int + realPrompt []rune // The prompt that is actually on the same line as the beginning of the input line. + defaultPrompt []rune + promptLen int + stillOnRefresh bool // True if some logs have printed asynchronously since last loop. Check refresh prompt funcs // // Input Line --------------------------------------------------------------------------------- @@ -110,7 +112,7 @@ type Instance struct { searchMode FindMode // Used for varying hints, and underlying functions called regexSearch *regexp.Regexp // Holds the current search regex match mainHist bool // Which history stdin do we want - histHint []rune // We store a hist hint, for dual history sources + histInfo []rune // We store a piece of hist info, for dual history sources // // History ----------------------------------------------------------------------------------- @@ -134,19 +136,33 @@ type Instance struct { histNavIdx int // Used for quick history navigation. // - // Hints ------------------------------------------------------------------------------------- + // Info ------------------------------------------------------------------------------------- - // HintText is a helper function which displays hint text the prompt. - // HintText takes the line input from the promt and the cursor position. + // InfoText is a helper function which displays infio text below the prompt. + // InfoText takes the line input from the prompt and the cursor position. + // It returns the info text to display. + InfoText func([]rune, int) []rune + + // InfoColor is any ANSI escape codes you wish to use for info formatting. By + // default this will just be blue. + InfoFormatting string + + infoText []rune // The actual info text + infoY int // Offset to info, if it spans multiple lines + + // + // Hints ----------------------------------------------------------------------------------- + + // HintText is a helper function which displays hint text right after the user's input. + // It takes the line input and cursor position. // It returns the hint text to display. HintText func([]rune, int) []rune - // HintColor any ANSI escape codes you wish to use for hint formatting. By - // default this will just be blue. + // HintFormatting is just a string to use as the formatting for the hint. By default + // this will be a grey color. HintFormatting string - hintText []rune // The actual hint text - hintY int // Offset to hints, if it spans multiple lines + hintText []rune // // Vim Operatng Parameters ------------------------------------------------------------------- @@ -205,7 +221,8 @@ func NewInstance() *Instance { rl.HistoryAutoWrite = true // Others - rl.HintFormatting = seqFgBlue + rl.InfoFormatting = seqFgBlue + rl.HintFormatting = "\x1b[2m" rl.evtKeyPress = make(map[string]func(string, []rune, int) *EventReturn) rl.TempDirectory = os.TempDir() diff --git a/readline/line.go b/readline/line.go index 134116f..be5ef2c 100644 --- a/readline/line.go +++ b/readline/line.go @@ -57,9 +57,9 @@ func (rl *Instance) echo() { // Print the input line with optional syntax highlighting if rl.SyntaxHighlighter != nil { - print(rl.SyntaxHighlighter(line) + " ") + print(rl.SyntaxHighlighter(line)) } else { - print(string(line) + " ") + print(string(line)) } } @@ -125,14 +125,14 @@ func (rl *Instance) deleteX() { rl.updateHelpers() } -func (rl *Instance) deleteBackspace() { +func (rl *Instance) deleteBackspace(forward bool) { switch { case len(rl.line) == 0: return - case rl.pos == 0: - rl.line = rl.line[1:] + case forward: + rl.line = append(rl.line[:rl.pos], rl.line[rl.pos+1:]...) case rl.pos > len(rl.line): - rl.backspace() // There is an infite loop going on here... + rl.backspace(forward) // There is an infite loop going on here... case rl.pos == len(rl.line): rl.pos-- rl.line = rl.line[:rl.pos] @@ -176,3 +176,48 @@ func (rl *Instance) deleteToBeginning() { rl.line = rl.line[rl.pos:] rl.pos = 0 } + +func (rl *Instance) deleteToEnd() { + rl.resetVirtualComp(false) + // Keep everything before the cursor + rl.line = rl.line[:rl.pos] +} + +// @TODO(Renzix): move to emacs sepecific file +func (rl *Instance) emacsForwardWord(tokeniser tokeniser) (adjust int) { + split, index, pos := tokeniser(rl.line, rl.pos) + if len(split) == 0 { + return + } + + word := strings.TrimSpace(split[index]) + + switch { + case len(split) == 0: + return + case pos == len(word) && index != len(split)-1: + extrawhitespace := len(strings.TrimLeft(split[index], " ")) - len(word) + word = split[index+1] + adjust = len(word) + extrawhitespace + default: + adjust = len(word) - pos + } + return +} + +func (rl *Instance) emacsBackwardWord(tokeniser tokeniser) (adjust int) { + split, index, pos := tokeniser(rl.line, rl.pos) + if len(split) == 0 { + return + } + + switch { + case len(split) == 0: + return + case pos == 0 && index != 0: + adjust = len(split[index-1]) + default: + adjust = pos + } + return +} diff --git a/readline/prompt.go b/readline/prompt.go index 2f3b80a..699abec 100644 --- a/readline/prompt.go +++ b/readline/prompt.go @@ -11,6 +11,13 @@ import ( // It also calculates the runes in the string as well as any non-printable escape codes. func (rl *Instance) SetPrompt(s string) { rl.mainPrompt = s + rl.computePrompt() +} + +// SetRightPrompt sets the right prompt. +func (rl *Instance) SetRightPrompt(s string) { + rl.rightPrompt = s + " " + rl.computePrompt() } // RefreshPromptLog - A simple function to print a string message (a log, or more broadly, @@ -20,7 +27,7 @@ func (rl *Instance) RefreshPromptLog(log string) (err error) { // We adjust cursor movement, depending on which mode we're currently in. if !rl.modeTabCompletion { rl.tcUsedY = 1 - // Account for the hint line + // Account for the info line } else if rl.modeTabCompletion && rl.modeAutoFind { rl.tcUsedY = 0 } else { @@ -40,7 +47,7 @@ func (rl *Instance) RefreshPromptLog(log string) (err error) { moveCursorUp(1) } rl.stillOnRefresh = true - moveCursorUp(rl.hintY + rl.tcUsedY) + moveCursorUp(rl.infoY + rl.tcUsedY) moveCursorBackwards(GetTermWidth()) print("\r\n" + seqClearScreenBelow) @@ -68,12 +75,11 @@ func (rl *Instance) RefreshPromptLog(log string) (err error) { // RefreshPromptInPlace - Refreshes the prompt in the very same place he is. func (rl *Instance) RefreshPromptInPlace(prompt string) (err error) { - // We adjust cursor movement, depending on which mode we're currently in. // Prompt data intependent if !rl.modeTabCompletion { rl.tcUsedY = 1 - // Account for the hint line + // Account for the info line } else if rl.modeTabCompletion && rl.modeAutoFind { rl.tcUsedY = 0 } else { @@ -82,7 +88,7 @@ func (rl *Instance) RefreshPromptInPlace(prompt string) (err error) { // Update the prompt if a special has been passed. if prompt != "" { - rl.mainPrompt = prompt + rl.SetPrompt(prompt) } if rl.Multiline { @@ -91,7 +97,7 @@ func (rl *Instance) RefreshPromptInPlace(prompt string) (err error) { // Clear the input line and everything below print(seqClearLine) - moveCursorUp(rl.hintY + rl.tcUsedY) + moveCursorUp(rl.infoY + rl.tcUsedY) moveCursorBackwards(GetTermWidth()) print("\r\n" + seqClearScreenBelow) @@ -118,7 +124,7 @@ func (rl *Instance) RefreshPromptCustom(prompt string, offset int, clearLine boo // We adjust cursor movement, depending on which mode we're currently in. if !rl.modeTabCompletion { rl.tcUsedY = 1 - } else if rl.modeTabCompletion && rl.modeAutoFind { // Account for the hint line + } else if rl.modeTabCompletion && rl.modeAutoFind { // Account for the info line rl.tcUsedY = 0 } else { rl.tcUsedY = 1 @@ -137,7 +143,7 @@ func (rl *Instance) RefreshPromptCustom(prompt string, offset int, clearLine boo // Update the prompt if a special has been passed. if prompt != "" { - rl.mainPrompt = prompt + rl.SetPrompt(prompt) } // Add a new line if needed @@ -185,6 +191,7 @@ func (rl *Instance) computePrompt() (prompt []rune) { // Strip color escapes rl.promptLen = getRealLength(string(rl.realPrompt)) + rl.rightPromptLen = getRealLength(string(rl.rightPrompt)) return } @@ -205,3 +212,11 @@ func getRealLength(s string) (l int) { stripped := ansi.Strip(s) return uniseg.GraphemeClusterCount(stripped) } + +func (rl *Instance) echoRightPrompt() { + if rl.fullX < GetTermWidth() - rl.rightPromptLen - 1 { + moveCursorForwards(GetTermWidth()) + moveCursorBackwards(rl.rightPromptLen) + print(rl.rightPrompt) + } +} diff --git a/readline/readline.go b/readline/readline.go index d2f8622..d5619e6 100644 --- a/readline/readline.go +++ b/readline/readline.go @@ -2,9 +2,11 @@ package readline import ( "bytes" + "errors" "fmt" "os" "regexp" + "syscall" ) var rxMultiline = regexp.MustCompile(`[\r\n]+`) @@ -38,11 +40,12 @@ func (rl *Instance) Readline() (string, error) { rl.modeViMode = VimInsert rl.pos = 0 rl.posY = 0 + rl.tcPrefix = "" - // Completion && hints init - rl.resetHintText() + // Completion && infos init + rl.resetInfoText() rl.resetTabCompletion() - rl.getHintText() + rl.getInfoText() // History Init // We need this set to the last command, so that we can access it quickly @@ -62,7 +65,7 @@ func (rl *Instance) Readline() (string, error) { return string(rl.line), nil } - // Finally, print any hints or completions + // Finally, print any info or completions // if the TabCompletion engines so desires rl.renderHelpers() @@ -76,6 +79,12 @@ func (rl *Instance) Readline() (string, error) { var err error i, err = os.Stdin.Read(b) if err != nil { + if errors.Is(err, syscall.EAGAIN) { + err = syscall.SetNonblock(syscall.Stdin, false) + if err == nil { + continue + } + } return "", err } } @@ -127,8 +136,8 @@ func (rl *Instance) Readline() (string, error) { rl.updateHelpers() } - if len(ret.HintText) > 0 { - rl.hintText = ret.HintText + if len(ret.InfoText) > 0 { + rl.infoText = ret.InfoText rl.clearHelpers() rl.renderHelpers() } @@ -160,9 +169,18 @@ func (rl *Instance) Readline() (string, error) { rl.clearHelpers() return "", CtrlC - case charEOF: - rl.clearHelpers() - return "", EOF + case charEOF: // ctrl d + if len(rl.line) == 0 { + rl.clearHelpers() + return "", EOF + } + if rl.modeTabFind { + rl.backspaceTabFind() + } else { + if (rl.pos < len(rl.line)) { + rl.deleteBackspace(true) + } + } // Clear screen case charCtrlL: @@ -173,8 +191,8 @@ func (rl *Instance) Readline() (string, error) { } print(seqClearScreenBelow) - rl.resetHintText() - rl.getHintText() + rl.resetInfoText() + rl.getInfoText() rl.renderHelpers() // Line Editing ------------------------------------------------------------------------------------ @@ -188,6 +206,16 @@ func (rl *Instance) Readline() (string, error) { rl.resetHelpers() rl.updateHelpers() + case charCtrlK: + if rl.modeTabCompletion { + rl.resetVirtualComp(true) + } + // Delete everything after the cursor position + rl.saveBufToRegister(rl.line[rl.pos:]) + rl.deleteToEnd() + rl.resetHelpers() + rl.updateHelpers() + case charBackspace, charBackspace2: // When currently in history completion, we refresh and automatically // insert the first (filtered) candidate, virtually @@ -213,7 +241,7 @@ func (rl *Instance) Readline() (string, error) { // Vim mode has different behaviors if rl.InputMode == Vim { if rl.modeViMode == VimInsert { - rl.backspace() + rl.backspace(false) } else if rl.pos != 0 { rl.pos-- } @@ -222,7 +250,7 @@ func (rl *Instance) Readline() (string, error) { } // Else emacs deletes a character - rl.backspace() + rl.backspace(false) rl.renderHelpers() } @@ -387,6 +415,10 @@ func (rl *Instance) Readline() (string, error) { rl.renderHelpers() } + case charCtrlUnderscore: + rl.undoLast() + rl.viUndoSkipAppend = true + case '\r': fallthrough case '\n': @@ -516,22 +548,27 @@ func (rl *Instance) editorInput(r []rune) { case VimReplaceMany: for _, char := range r { - rl.deleteX() + if rl.pos != len(rl.line) { + rl.deleteX() + } rl.insert([]rune{char}) } rl.refreshVimStatus() default: - // For some reason Ctrl+k messes with the input line, so ignore it. - if r[0] == 11 { + // Don't insert control keys + if r[0] >= 1 && r[0] <= 31 { return } // We reset the history nav counter each time we come here: // We don't need it when inserting text. rl.histNavIdx = 0 rl.insert(r) + rl.writeHintText() } + rl.echoRightPrompt() + if len(rl.multisplit) == 0 { rl.syntaxCompletion() } @@ -625,6 +662,8 @@ func (rl *Instance) escapeSeq(r []rune) { } rl.mainHist = true rl.walkHistory(1) + moveCursorForwards(len(rl.line) - rl.pos) + rl.pos = len(rl.line) case seqDown: if rl.modeTabCompletion { @@ -636,6 +675,8 @@ func (rl *Instance) escapeSeq(r []rune) { } rl.mainHist = true rl.walkHistory(-1) + moveCursorForwards(len(rl.line) - rl.pos) + rl.pos = len(rl.line) case seqForwards: if rl.modeTabCompletion { @@ -647,8 +688,7 @@ func (rl *Instance) escapeSeq(r []rune) { } if (rl.modeViMode == VimInsert && rl.pos < len(rl.line)) || (rl.modeViMode != VimInsert && rl.pos < len(rl.line)-1) { - moveCursorForwards(1) - rl.pos++ + rl.moveCursorByAdjust(1) } rl.updateHelpers() rl.viUndoSkipAppend = true @@ -663,10 +703,7 @@ func (rl *Instance) escapeSeq(r []rune) { rl.renderHelpers() return } - if rl.pos > 0 { - moveCursorBackwards(1) - rl.pos-- - } + rl.moveCursorByAdjust(-1) rl.viUndoSkipAppend = true rl.updateHelpers() @@ -689,32 +726,64 @@ func (rl *Instance) escapeSeq(r []rune) { rl.updateHelpers() return case seqCtrlRightArrow: + rl.insert(rl.hintText) rl.moveCursorByAdjust(rl.viJumpW(tokeniseLine)) rl.updateHelpers() return - case seqDelete: + case seqDelete,seqDelete2: if rl.modeTabFind { rl.backspaceTabFind() } else { - rl.deleteBackspace() + if (rl.pos < len(rl.line)) { + rl.deleteBackspace(true) + } } + case seqHome, seqHomeSc: if rl.modeTabCompletion { return } - moveCursorBackwards(rl.pos) - rl.pos = 0 + rl.moveCursorByAdjust(-rl.pos) + rl.updateHelpers() rl.viUndoSkipAppend = true case seqEnd, seqEndSc: if rl.modeTabCompletion { return } - moveCursorForwards(len(rl.line) - rl.pos) - rl.pos = len(rl.line) + rl.moveCursorByAdjust(len(rl.line) - rl.pos) + rl.updateHelpers() rl.viUndoSkipAppend = true + case seqAltB: + if rl.modeTabCompletion { + return + } + + // This is only available in Insert mode + if rl.modeViMode != VimInsert { + return + } + + move := rl.emacsBackwardWord(tokeniseLine) + rl.moveCursorByAdjust(-move) + rl.updateHelpers() + + case seqAltF: + if rl.modeTabCompletion { + return + } + + // This is only available in Insert mode + if rl.modeViMode != VimInsert { + return + } + + move := rl.emacsForwardWord(tokeniseLine) + rl.moveCursorByAdjust(move) + rl.updateHelpers() + case seqAltR: rl.resetVirtualComp(false) // For some modes only, if we are in vim Keys mode, @@ -733,6 +802,36 @@ func (rl *Instance) escapeSeq(r []rune) { rl.updateTabFind([]rune{}) rl.viUndoSkipAppend = true + case seqAltBackspace: + if rl.modeTabCompletion { + rl.resetVirtualComp(false) + } + // This is only available in Insert mode + if rl.modeViMode != VimInsert { + return + } + + rl.saveToRegister(rl.viJumpB(tokeniseLine)) + rl.viDeleteByAdjust(rl.viJumpB(tokeniseLine)) + rl.updateHelpers() + + case seqCtrlDelete, seqCtrlDelete2, seqAltD: + if rl.modeTabCompletion { + rl.resetVirtualComp(false) + } + rl.saveToRegister(rl.emacsForwardWord(tokeniseLine)) + // vi delete, emacs forward, funny huh + rl.viDeleteByAdjust(rl.emacsForwardWord(tokeniseLine)) + rl.updateHelpers() + + case seqAltDelete: + if rl.modeTabCompletion { + rl.resetVirtualComp(false) + } + rl.saveToRegister(-rl.emacsBackwardWord(tokeniseLine)) + rl.viDeleteByAdjust(-rl.emacsBackwardWord(tokeniseLine)) + rl.updateHelpers() + default: if rl.modeTabFind { return @@ -768,6 +867,8 @@ func (rl *Instance) escapeSeq(r []rune) { } func (rl *Instance) carridgeReturn() { + rl.moveCursorByAdjust(len(rl.line)) + rl.updateHelpers() rl.clearHelpers() print("\r\n") if rl.HistoryAutoWrite { diff --git a/readline/register.go b/readline/register.go index 5cc03af..4372b89 100644 --- a/readline/register.go +++ b/readline/register.go @@ -259,9 +259,9 @@ func (r *registers) resetRegister() { // The user can show registers completions and insert, no matter the cursor position. func (rl *Instance) completeRegisters() (groups []*CompletionGroup) { - // We set the hint exceptionally - hint := BLUE + "-- registers --" + RESET - rl.hintText = []rune(hint) + // We set the info exceptionally + info := BLUE + "-- registers --" + RESET + rl.infoText = []rune(info) // Make the groups anonRegs := &CompletionGroup{ diff --git a/readline/tab.go b/readline/tab.go index 7c86386..c3f9c37 100644 --- a/readline/tab.go +++ b/readline/tab.go @@ -93,19 +93,16 @@ func (rl *Instance) getTabSearchCompletion() { } rl.getCurrentGroup() - // Set the hint for this completion mode - rl.hintText = append([]rune("Completion search: "), rl.tfLine...) - - // Set the hint for this completion mode - rl.hintText = append([]rune("Completion search: "), rl.tfLine...) + // Set the info for this completion mode + rl.infoText = append([]rune("Completion search: "), rl.tfLine...) for _, g := range rl.tcGroups { g.updateTabFind(rl) } - // If total number of matches is zero, we directly change the hint, and return + // If total number of matches is zero, we directly change the info, and return if comps, _, _ := rl.getCompletionCount(); comps == 0 { - rl.hintText = append(rl.hintText, []rune(DIM+RED+" ! no matches (Ctrl-G/Esc to cancel)"+RESET)...) + rl.infoText = append(rl.infoText, []rune(DIM+RED+" ! no matches (Ctrl-G/Esc to cancel)"+RESET)...) } } @@ -120,25 +117,25 @@ func (rl *Instance) getHistorySearchCompletion() { rl.tcGroups = checkNilItems(rl.tcGroups) // Avoid nil maps in groups rl.getCurrentGroup() // Make sure there is a current group - // The history hint is already set, but overwrite it if we don't have completions + // The history info is already set, but overwrite it if we don't have completions if len(rl.tcGroups[0].Suggestions) == 0 { - rl.histHint = []rune(fmt.Sprintf("%s%s%s %s", DIM, RED, + rl.histInfo = []rune(fmt.Sprintf("%s%s%s %s", DIM, RED, "No command history source, or empty (Ctrl-G/Esc to cancel)", RESET)) - rl.hintText = rl.histHint + rl.infoText = rl.histInfo return } - // Set the hint line with everything - rl.histHint = append([]rune("\033[38;5;183m"+string(rl.histHint)+RESET), rl.tfLine...) - rl.histHint = append(rl.histHint, []rune(RESET)...) - rl.hintText = rl.histHint + // Set the info line with everything + rl.histInfo = append([]rune("\033[38;5;183m"+string(rl.histInfo)+RESET), rl.tfLine...) + rl.histInfo = append(rl.histInfo, []rune(RESET)...) + rl.infoText = rl.histInfo // Refresh filtered candidates rl.tcGroups[0].updateTabFind(rl) - // If no items matched history, add hint text that we failed to search + // If no items matched history, add info text that we failed to search if len(rl.tcGroups[0].Suggestions) == 0 { - rl.hintText = append(rl.histHint, []rune(DIM+RED+" ! no matches (Ctrl-G/Esc to cancel)"+RESET)...) + rl.infoText = append(rl.histInfo, []rune(DIM+RED+" ! no matches (Ctrl-G/Esc to cancel)"+RESET)...) return } } @@ -301,15 +298,15 @@ func (rl *Instance) cropCompletions(comps string) (cropped string, usedY int) { // Else we go on, but we have more comps than what allowed: // we will add a line to the end of the comps, giving the actualized // number of completions remaining and not printed - var moreComps = func(cropped string, offset int) (hinted string, noHint bool) { + var moreComps = func(cropped string, offset int) (infoed string, noInfo bool) { _, _, adjusted := rl.getCompletionCount() remain := adjusted - offset if remain == 0 { return cropped, true } - hint := fmt.Sprintf(DIM+YELLOW+" %d more completions... (scroll down to show)"+RESET+"\n", remain) - hinted = cropped + hint - return hinted, false + info := fmt.Sprintf(DIM+YELLOW+" %d more completions... (scroll down to show)"+RESET+"\n", remain) + infoed = cropped + info + return infoed, false } // Get the current absolute candidate position (prev groups x suggestions + curGroup.tcPosY) @@ -512,7 +509,7 @@ func (rl *Instance) hasOneCandidate() bool { // - The terminal lengh // we use this function to prompt for confirmation before printing comps. func (rl *Instance) promptCompletionConfirm(sentence string) { - rl.hintText = []rune(sentence) + rl.infoText = []rune(sentence) rl.compConfirmWait = true rl.viUndoSkipAppend = true diff --git a/readline/tabfind.go b/readline/tabfind.go index 44487bf..5abe307 100644 --- a/readline/tabfind.go +++ b/readline/tabfind.go @@ -33,7 +33,7 @@ func (rl *Instance) updateTabFind(r []rune) { var err error rl.regexSearch, err = regexp.Compile("(?i)" + string(rl.tfLine)) if err != nil { - rl.hintText = []rune(Red("Failed to match search regexp")) + rl.infoText = []rune(Red("Failed to match search regexp")) } // We update and print diff --git a/readline/update.go b/readline/update.go index 2f58cdc..508ae24 100644 --- a/readline/update.go +++ b/readline/update.go @@ -1,12 +1,15 @@ package readline +import "golang.org/x/text/width" + // updateHelpers is a key part of the whole refresh process: -// it should coordinate reprinting the input line, any hints and completions +// it should coordinate reprinting the input line, any Infos and completions // and manage to get back to the current (computed) cursor coordinates func (rl *Instance) updateHelpers() { - // Load all hints & completions before anything. - // Thus overwrites anything having been dirtily added/forced/modified, like rl.SetHintText() + // Load all Infos & completions before anything. + // Thus overwrites anything having been dirtily added/forced/modified, like rl.SetInfoText() + rl.getInfoText() rl.getHintText() if rl.modeTabCompletion { rl.getTabCompletion() @@ -20,6 +23,23 @@ func (rl *Instance) updateHelpers() { rl.renderHelpers() } +const tabWidth = 4 + +func getWidth(x []rune) int { + var w int + for _, j := range x { + k := width.LookupRune(j).Kind() + if j == '\t' { + w += tabWidth + } else if k == width.EastAsianWide || k == width.EastAsianFullwidth { + w += 2 + } else { + w++ + } + } + return w +} + // Update reference should be called only once in a "loop" (not Readline(), but key control loop) func (rl *Instance) updateReferences() { @@ -32,11 +52,11 @@ func (rl *Instance) updateReferences() { var fullLine, cPosLine int if len(rl.currentComp) > 0 { - fullLine = len(rl.lineComp) - cPosLine = len(rl.lineComp[:rl.pos]) + fullLine = getWidth(rl.lineComp) + cPosLine = getWidth(rl.lineComp[:rl.pos]) } else { - fullLine = len(rl.line) - cPosLine = len(rl.line[:rl.pos]) + fullLine = getWidth(rl.line) + cPosLine = getWidth(rl.line[:rl.pos]) } // We need the X offset of the whole line @@ -46,6 +66,10 @@ func (rl *Instance) updateReferences() { fullRest := toEndLine % GetTermWidth() rl.fullX = fullRest + if fullRest == 0 && fullOffset > 0 { + print("\n") + } + // Use rl.pos value to get the offset to go TO/FROM the CURRENT POSITION lineToCursorPos := rl.promptLen + cPosLine offsetToCursor := lineToCursorPos / GetTermWidth() @@ -75,11 +99,11 @@ func (rl *Instance) resetHelpers() { rl.modeAutoFind = false // Now reset all below-input helpers - rl.resetHintText() + rl.resetInfoText() rl.resetTabCompletion() } -// clearHelpers - Clears everything: prompt, input, hints & comps, +// clearHelpers - Clears everything: prompt, input, Infos & comps, // and comes back at the prompt. func (rl *Instance) clearHelpers() { @@ -97,25 +121,42 @@ func (rl *Instance) clearHelpers() { moveCursorForwards(rl.posX) } -// renderHelpers - pritns all components (prompt, line, hints & comps) +// renderHelpers - pritns all components (prompt, line, Infos & comps) // and replaces the cursor to its current position. This function never // computes or refreshes any value, except from inside the echo function. func (rl *Instance) renderHelpers() { - // Optional, because neutral on placement + // when the instance is in this state we want it to be "below" the user's + // input for it to be aligned properly + if !rl.compConfirmWait { + rl.writeHintText() + } rl.echo() + if rl.modeTabCompletion { + // in tab complete mode we want it to update + // when something has been selected + // (dynamic!!) + rl.getHintText() + rl.writeHintText() + } else if !rl.compConfirmWait { + // for the same reason above of wanting it below user input, do nothing here + } else { + rl.writeHintText() + } + + rl.echoRightPrompt() // Go at beginning of first line after input remainder moveCursorDown(rl.fullY - rl.posY) moveCursorBackwards(GetTermWidth()) - // Print hints, check for any confirmation hint current. - // (do not overwrite the confirmation question hint) + // Print Infos, check for any confirmation Info current. + // (do not overwrite the confirmation question Info) if !rl.compConfirmWait { - if len(rl.hintText) > 0 { + if len(rl.infoText) > 0 { print("\n") } - rl.writeHintText() + rl.writeInfoText() moveCursorBackwards(GetTermWidth()) // Print completions and go back to beginning of this line @@ -126,17 +167,17 @@ func (rl *Instance) renderHelpers() { } // If we are still waiting for the user to confirm too long completions - // Immediately refresh the hints + // Immediately refresh the Infos if rl.compConfirmWait { print("\n") - rl.writeHintText() - rl.getHintText() + rl.writeInfoText() + rl.getInfoText() moveCursorBackwards(GetTermWidth()) } - // Anyway, compensate for hint printout - if len(rl.hintText) > 0 { - moveCursorUp(rl.hintY) + // Anyway, compensate for Info printout + if len(rl.infoText) > 0 { + moveCursorUp(rl.infoY) } else if !rl.compConfirmWait { moveCursorUp(1) } else if rl.compConfirmWait { diff --git a/readline/vim.go b/readline/vim.go index f421d84..886927b 100644 --- a/readline/vim.go +++ b/readline/vim.go @@ -399,22 +399,22 @@ func (rl *Instance) refreshVimStatus() { rl.updateHelpers() } -// viHintMessage - lmorg's way of showing Vim status is to overwrite the hint. +// viInfoMessage - lmorg's way of showing Vim status is to overwrite the info. // Currently not used, as there is a possibility to show the current Vim mode in the prompt. -func (rl *Instance) viHintMessage() { +func (rl *Instance) viInfoMessage() { switch rl.modeViMode { case VimKeys: - rl.hintText = []rune("-- VIM KEYS -- (press `i` to return to normal editing mode)") + rl.infoText = []rune("-- VIM KEYS -- (press `i` to return to normal editing mode)") case VimInsert: - rl.hintText = []rune("-- INSERT --") + rl.infoText = []rune("-- INSERT --") case VimReplaceOnce: - rl.hintText = []rune("-- REPLACE CHARACTER --") + rl.infoText = []rune("-- REPLACE CHARACTER --") case VimReplaceMany: - rl.hintText = []rune("-- REPLACE --") + rl.infoText = []rune("-- REPLACE --") case VimDelete: - rl.hintText = []rune("-- DELETE --") + rl.infoText = []rune("-- DELETE --") default: - rl.getHintText() + rl.getInfoText() } rl.clearHelpers() diff --git a/readline/vimdelete.go b/readline/vimdelete.go index b836019..7a07259 100644 --- a/readline/vimdelete.go +++ b/readline/vimdelete.go @@ -33,7 +33,7 @@ func (rl *Instance) viDelete(r rune) { rl.saveBufToRegister(rl.line) rl.clearLine() rl.resetHelpers() - rl.getHintText() + rl.getInfoText() case 'e': vii := rl.getViIterations() diff --git a/rl.go b/rl.go index edbef28..8093273 100644 --- a/rl.go +++ b/rl.go @@ -5,23 +5,26 @@ import ( "io" "strings" + "hilbish/util" + "github.com/maxlandon/readline" - "github.com/yuin/gopher-lua" + rt "github.com/arnodel/golua/runtime" ) type lineReader struct { rl *readline.Instance } var fileHist *fileHistory +var hinter *rt.Closure +var highlighter *rt.Closure -// other gophers might hate this naming but this is local, shut up func newLineReader(prompt string, noHist bool) *lineReader { rl := readline.NewInstance() // we don't mind hilbish.read rl instances having completion, // but it cant have shared history if !noHist { fileHist = newFileHistory() - rl.SetHistoryCtrlR("file", fileHist) + rl.SetHistoryCtrlR("History", fileHist) rl.HistoryAutoWrite = false } rl.ShowVimMode = false @@ -44,9 +47,45 @@ func newLineReader(prompt string, noHist bool) *lineReader { } hooks.Em.Emit("hilbish.vimAction", actionStr, args) } + rl.HintText = func(line []rune, pos int) []rune { + if hinter == nil { + return []rune{} + } + + retVal, err := rt.Call1(l.MainThread(), rt.FunctionValue(highlighter), + rt.StringValue(string(line)), rt.IntValue(int64(pos))) + if err != nil { + fmt.Println(err) + return []rune{} + } + + hintText := "" + if luaStr, ok := retVal.TryString(); ok { + hintText = luaStr + } + + return []rune(hintText) + } + rl.SyntaxHighlighter = func(line []rune) string { + if highlighter == nil { + return string(line) + } + retVal, err := rt.Call1(l.MainThread(), rt.FunctionValue(highlighter), + rt.StringValue(string(line))) + if err != nil { + fmt.Println(err) + return string(line) + } + + highlighted := "" + if luaStr, ok := retVal.TryString(); ok { + highlighted = luaStr + } + + return highlighted + } rl.TabCompleter = func(line []rune, pos int, _ readline.DelayedTabContext) (string, []*readline.CompletionGroup) { ctx := string(line) - var completions []string var compGroup []*readline.CompletionGroup @@ -75,23 +114,20 @@ func newLineReader(prompt string, noHist bool) *lineReader { return prefix, compGroup } else { if completecb, ok := luaCompletions["command." + fields[0]]; ok { - luaFields := l.NewTable() - for _, f := range fields { - luaFields.Append(lua.LString(f)) + luaFields := rt.NewTable() + for i, f := range fields { + luaFields.Set(rt.IntValue(int64(i + 1)), rt.StringValue(f)) } - err := l.CallByParam(lua.P{ - Fn: completecb, - NRet: 1, - Protect: true, - }, lua.LString(query), lua.LString(ctx), luaFields) + + // 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 } - luacompleteTable := l.Get(-1) - l.Pop(1) - /* as an example with git, completion table should be structured like: @@ -116,60 +152,98 @@ func newLineReader(prompt string, noHist bool) *lineReader { 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.Type() == lua.LTNumber { - // 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()) - } - if compItems.Type() != lua.LTTable { - l.RaiseError("bad items for completion (expected table, got %v)", compItems.Type().String()) - } - 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 { - items = append(items, v.String()) - } - }) + if cmpTbl, ok := luacompleteTable.TryTable(); ok { + nextVal := rt.NilValue + for { + next, val, ok := cmpTbl.Next(nextVal) + if next == rt.NilValue { + break + } + nextVal = next - 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 + _, 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 } - compGroup = append(compGroup, &readline.CompletionGroup{ - DisplayType: dispType, - Descriptions: itemDescriptions, - Suggestions: items, - TrimSlash: false, - NoSpace: true, - }) + 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, + }) + } } } if len(compGroup) == 0 { - completions = fileComplete(query, ctx, fields) - compGroup = append(compGroup, &readline.CompletionGroup{ + completions, p := fileComplete(query, ctx, fields) + fcompGroup := []*readline.CompletionGroup{{ TrimSlash: false, NoSpace: true, Suggestions: completions, - }) + }} + + return p, fcompGroup } } return "", compGroup @@ -208,6 +282,13 @@ func (lr *lineReader) SetPrompt(p string) { } } +func (lr *lineReader) SetRightPrompt(p string) { + lr.rl.SetRightPrompt(p) + if initialized && !running { + lr.rl.RefreshPromptInPlace("") + } +} + func (lr *lineReader) AddHistory(cmd string) { fileHist.Write(cmd) } @@ -221,56 +302,65 @@ func (lr *lineReader) Resize() { } // lua module -func (lr *lineReader) Loader(L *lua.LState) *lua.LTable { - lrLua := map[string]lua.LGFunction{ - "add": lr.luaAddHistory, - "all": lr.luaAllHistory, - "clear": lr.luaClearHistory, - "get": lr.luaGetHistory, - "size": lr.luaSize, +func (lr *lineReader) Loader(rtm *rt.Runtime) *rt.Table { + lrLua := map[string]util.LuaExport{ + "add": {lr.luaAddHistory, 1, false}, + "all": {lr.luaAllHistory, 0, false}, + "clear": {lr.luaClearHistory, 0, false}, + "get": {lr.luaGetHistory, 1, false}, + "size": {lr.luaSize, 0, false}, } - mod := l.SetFuncs(l.NewTable(), lrLua) + mod := rt.NewTable() + util.SetExports(rtm, mod, lrLua) return mod } -func (lr *lineReader) luaAddHistory(l *lua.LState) int { - cmd := l.CheckString(1) +func (lr *lineReader) luaAddHistory(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + cmd, err := c.StringArg(0) + if err != nil { + return nil, err + } lr.AddHistory(cmd) - return 0 + return c.Next(), nil } -func (lr *lineReader) luaSize(L *lua.LState) int { - L.Push(lua.LNumber(fileHist.Len())) - - return 1 +func (lr *lineReader) luaSize(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + return c.PushingNext1(t.Runtime, rt.IntValue(int64(fileHist.Len()))), nil } -func (lr *lineReader) luaGetHistory(L *lua.LState) int { - idx := L.CheckInt(1) - cmd, _ := fileHist.GetLine(idx) - L.Push(lua.LString(cmd)) +func (lr *lineReader) luaGetHistory(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + idx, err := c.IntArg(0) + if err != nil { + return nil, err + } - return 0 + cmd, _ := fileHist.GetLine(int(idx)) + + return c.PushingNext1(t.Runtime, rt.StringValue(cmd)), nil } -func (lr *lineReader) luaAllHistory(L *lua.LState) int { - tbl := L.NewTable() +func (lr *lineReader) luaAllHistory(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + tbl := rt.NewTable() size := fileHist.Len() for i := 1; i < size; i++ { cmd, _ := fileHist.GetLine(i) - tbl.Append(lua.LString(cmd)) + tbl.Set(rt.IntValue(int64(i)), rt.StringValue(cmd)) } - L.Push(tbl) - - return 0 + return c.PushingNext1(t.Runtime, rt.TableValue(tbl)), nil } -func (lr *lineReader) luaClearHistory(l *lua.LState) int { - return 0 - +func (lr *lineReader) luaClearHistory(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + fileHist.clear() + return c.Next(), nil } diff --git a/runnermode.go b/runnermode.go new file mode 100644 index 0000000..4285142 --- /dev/null +++ b/runnermode.go @@ -0,0 +1,56 @@ +package main + +import ( + "hilbish/util" + + rt "github.com/arnodel/golua/runtime" +) + +func runnerModeLoader(rtm *rt.Runtime) *rt.Table { + exports := map[string]util.LuaExport{ + "sh": {shRunner, 1, false}, + "lua": {luaRunner, 1, false}, + "setMode": {hlrunnerMode, 1, false}, + } + + mod := rt.NewTable() + util.SetExports(rtm, mod, exports) + + return mod +} + +func shRunner(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + cmd, err := c.StringArg(0) + if err != nil { + return nil, err + } + + input, exitCode, err := handleSh(cmd) + var luaErr rt.Value = rt.NilValue + if err != nil { + luaErr = rt.StringValue(err.Error()) + } + + return c.PushingNext(t.Runtime, rt.StringValue(input), rt.IntValue(int64(exitCode)), luaErr), nil +} + +func luaRunner(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + cmd, err := c.StringArg(0) + if err != nil { + return nil, err + } + + input, exitCode, err := handleLua(cmd) + var luaErr rt.Value = rt.NilValue + if err != nil { + luaErr = rt.StringValue(err.Error()) + } + + return c.PushingNext(t.Runtime, rt.StringValue(input), rt.IntValue(int64(exitCode)), luaErr), nil +} diff --git a/timer.go b/timer.go new file mode 100644 index 0000000..481156d --- /dev/null +++ b/timer.go @@ -0,0 +1,106 @@ +package main + +import ( + "errors" + "fmt" + "os" + "time" + + "hilbish/util" + + rt "github.com/arnodel/golua/runtime" +) + +type timerType int64 +const ( + timerInterval timerType = iota + timerTimeout +) + +type timer struct{ + id int + typ timerType + running bool + dur time.Duration + fun *rt.Closure + th *timerHandler + ticker *time.Ticker + channel chan bool +} + +func (t *timer) start() error { + if t.running { + return errors.New("timer is already running") + } + + t.running = true + t.th.running++ + t.ticker = time.NewTicker(t.dur) + + go func() { + for { + select { + case <-t.ticker.C: + _, err := rt.Call1(l.MainThread(), rt.FunctionValue(t.fun)) + if err != nil { + fmt.Fprintln(os.Stderr, "Error in function:\n", err) + t.stop() + } + // only run one for timeout + if t.typ == timerTimeout { + t.stop() + } + case <-t.channel: + t.ticker.Stop() + return + } + } + }() + + return nil +} + +func (t *timer) stop() error { + if !t.running { + return errors.New("timer not running") + } + + t.channel <- true + t.running = false + t.th.running-- + + return nil +} + +func (t *timer) luaStart(thr *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + err := t.start() + if err != nil { + return nil, err + } + + return c.Next(), nil +} + +func (t *timer) luaStop(thr *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + err := t.stop() + if err != nil { + return nil, err + } + + return c.Next(), nil +} + +func (t *timer) lua() rt.Value { + tExports := map[string]util.LuaExport{ + "start": {t.luaStart, 0, false}, + "stop": {t.luaStop, 0, false}, + } + luaTimer := rt.NewTable() + util.SetExports(l, luaTimer, tExports) + + luaTimer.Set(rt.StringValue("type"), rt.IntValue(int64(t.typ))) + luaTimer.Set(rt.StringValue("running"), rt.BoolValue(t.running)) + luaTimer.Set(rt.StringValue("duration"), rt.IntValue(int64(t.dur / time.Millisecond))) + + return rt.TableValue(luaTimer) +} diff --git a/timerhandler.go b/timerhandler.go new file mode 100644 index 0000000..a85bb17 --- /dev/null +++ b/timerhandler.go @@ -0,0 +1,102 @@ +package main + +import ( + "sync" + "time" + + "hilbish/util" + + rt "github.com/arnodel/golua/runtime" +) + +var timers *timerHandler +type timerHandler struct { + mu *sync.RWMutex + timers map[int]*timer + latestID int + running int +} + +func newTimerHandler() *timerHandler { + return &timerHandler{ + timers: make(map[int]*timer), + latestID: 0, + mu: &sync.RWMutex{}, + } +} + +func (th *timerHandler) create(typ timerType, dur time.Duration, fun *rt.Closure) *timer { + th.mu.Lock() + defer th.mu.Unlock() + + th.latestID++ + t := &timer{ + typ: typ, + fun: fun, + dur: dur, + channel: make(chan bool, 1), + th: th, + id: th.latestID, + } + th.timers[th.latestID] = t + + return t +} + +func (th *timerHandler) get(id int) *timer { + th.mu.RLock() + defer th.mu.RUnlock() + + return th.timers[id] +} + +func (th *timerHandler) luaCreate(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.CheckNArgs(3); err != nil { + return nil, err + } + timerTypInt, err := c.IntArg(0) + if err != nil { + return nil, err + } + ms, err := c.IntArg(1) + if err != nil { + return nil, err + } + cb, err := c.ClosureArg(2) + if err != nil { + return nil, err + } + + timerTyp := timerType(timerTypInt) + tmr := th.create(timerTyp, time.Duration(ms) * time.Millisecond, cb) + return c.PushingNext1(t.Runtime, tmr.lua()), nil +} + +func (th *timerHandler) luaGet(thr *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + id, err := c.IntArg(0) + if err != nil { + return nil, err + } + + t := th.get(int(id)) + if t != nil { + return c.PushingNext1(thr.Runtime, t.lua()), nil + } + + return c.Next(), nil +} + +func (th *timerHandler) loader(rtm *rt.Runtime) *rt.Table { + thExports := map[string]util.LuaExport{ + "create": {th.luaCreate, 3, false}, + "get": {th.luaGet, 1, false}, + } + + luaTh := rt.NewTable() + util.SetExports(rtm, luaTh, thExports) + + return luaTh +} diff --git a/util/export.go b/util/export.go new file mode 100644 index 0000000..ee0b4a6 --- /dev/null +++ b/util/export.go @@ -0,0 +1,19 @@ +package util + +import ( + rt "github.com/arnodel/golua/runtime" +) + +// LuaExport represents a Go function which can be exported to Lua. +type LuaExport struct { + Function rt.GoFunctionFunc + ArgNum int + Variadic bool +} + +// SetExports puts the Lua function exports in the table. +func SetExports(rtm *rt.Runtime, tbl *rt.Table, exports map[string]LuaExport) { + for name, export := range exports { + rtm.SetEnvGoFunc(tbl, name, export.Function, export.ArgNum, export.Variadic) + } +} diff --git a/util/util.go b/util/util.go index 718c623..b8c267a 100644 --- a/util/util.go +++ b/util/util.go @@ -1,32 +1,120 @@ package util -import "github.com/yuin/gopher-lua" +import ( + "bufio" + "io" + "os" + + rt "github.com/arnodel/golua/runtime" +) // Document adds a documentation string to a module. // It is accessible via the __doc metatable. -func Document(L *lua.LState, module lua.LValue, doc string) { - mt := L.GetMetatable(module) - if mt == lua.LNil { - mt = L.NewTable() - L.SetMetatable(module, mt) +func Document(module *rt.Table, doc string) { + mt := module.Metatable() + + if mt == nil { + mt = rt.NewTable() + module.SetMetatable(mt) } - L.SetField(mt, "__doc", lua.LString(doc)) + + mt.Set(rt.StringValue("__doc"), rt.StringValue(doc)) } // SetField sets a field in a table, adding docs for it. // It is accessible via the __docProp metatable. It is a table of the names of the fields. -func SetField(L *lua.LState, module lua.LValue, field string, value lua.LValue, doc string) { - mt := L.GetMetatable(module) - if mt == lua.LNil { - mt = L.NewTable() - docProp := L.NewTable() - L.SetField(mt, "__docProp", docProp) +func SetField(rtm *rt.Runtime, module *rt.Table, field string, value rt.Value, doc string) { + // TODO: ^ rtm isnt needed, i should remove it + mt := module.Metatable() + + if mt == nil { + mt = rt.NewTable() + docProp := rt.NewTable() + mt.Set(rt.StringValue("__docProp"), rt.TableValue(docProp)) - L.SetMetatable(module, mt) + module.SetMetatable(mt) } - docProp := L.GetTable(mt, lua.LString("__docProp")) + docProp := mt.Get(rt.StringValue("__docProp")) - L.SetField(docProp, field, lua.LString(doc)) - L.SetField(module, field, value) + docProp.AsTable().Set(rt.StringValue(field), rt.StringValue(doc)) + module.Set(rt.StringValue(field), value) } +// DoString runs the code string in the Lua runtime. +func DoString(rtm *rt.Runtime, code string) error { + chunk, err := rtm.CompileAndLoadLuaChunk("", []byte(code), rt.TableValue(rtm.GlobalEnv())) + if chunk != nil { + _, err = rt.Call1(rtm.MainThread(), rt.FunctionValue(chunk)) + } + + return err +} + +// DoFile runs the contents of the file in the Lua runtime. +func DoFile(rtm *rt.Runtime, path string) error { + f, err := os.Open(path) + defer f.Close() + + if err != nil { + return err + } + + reader := bufio.NewReader(f) + c, err := reader.ReadByte() + if err != nil && err != io.EOF { + return err + } + + // unread so a char won't be missing + err = reader.UnreadByte() + if err != nil { + return err + } + + var buf []byte + if c == byte('#') { + // shebang - skip that line + _, err := reader.ReadBytes('\n') + if err != nil && err != io.EOF { + return err + } + buf = []byte{'\n'} + } + + for { + line, err := reader.ReadBytes('\n') + if err != nil { + if err == io.EOF { + break + } + return err + } + + buf = append(buf, line...) + } + + clos, err := rtm.LoadFromSourceOrCode(path, buf, "bt", rt.TableValue(rtm.GlobalEnv()), false) + if clos != nil { + _, err = rt.Call1(rtm.MainThread(), rt.FunctionValue(clos)) + } + + return err +} + +// HandleStrCallback handles function parameters for Go functions which take +// a string and a closure. +func HandleStrCallback(t *rt.Thread, c *rt.GoCont) (string, *rt.Closure, error) { + if err := c.CheckNArgs(2); err != nil { + return "", nil, err + } + name, err := c.StringArg(0) + if err != nil { + return "", nil, err + } + cb, err := c.ClosureArg(1) + if err != nil { + return "", nil, err + } + + return name, cb, err +} diff --git a/vars.go b/vars.go index 3c94a4a..9238ffb 100644 --- a/vars.go +++ b/vars.go @@ -2,8 +2,7 @@ package main // String vars that are free to be changed at compile time var ( - version = "v1.0.4" - defaultConfDir = "" // ~ will be substituted for home, path for user's default config + version = "v2.0.0" defaultHistDir = "" commonRequirePaths = "';./libs/?/init.lua;./?/init.lua;./?/?.lua'" diff --git a/vars_darwin.go b/vars_darwin.go index c969639..b780c23 100644 --- a/vars_darwin.go +++ b/vars_darwin.go @@ -17,4 +17,5 @@ var ( dataDir = "/usr/local/share/hilbish" preloadPath = dataDir + "/prelude/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 4bb23d3..5ea3ac5 100644 --- a/vars_linux.go +++ b/vars_linux.go @@ -17,4 +17,5 @@ var ( dataDir = "/usr/share/hilbish" preloadPath = dataDir + "/prelude/init.lua" sampleConfPath = dataDir + "/.hilbishrc.lua" // Path to default/sample config + defaultConfDir = "" ) diff --git a/vars_windows.go b/vars_windows.go index 0867480..5e9878c 100644 --- a/vars_windows.go +++ b/vars_windows.go @@ -11,4 +11,5 @@ var ( dataDir = "~\\Appdata\\Roaming\\Hilbish" // ~ and \ gonna cry? preloadPath = dataDir + "\\prelude\\init.lua" sampleConfPath = dataDir + "\\hilbishrc.lua" // Path to default/sample config + defaultConfDir = "" )