diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4aab838..f1fe1b1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,8 +1,12 @@ name: Build on: - - push - - pull_request + push: + branches: + - master + pull_request: + branches: + - master jobs: build: @@ -19,18 +23,18 @@ jobs: goos: windows steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: true - name: Setup Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: - go-version: '1.18.8' + go-version: '1.22.2' - name: Download Task run: 'sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d' - name: Build run: GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} ./bin/task - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: matrix.goos == 'windows' with: name: hilbish-${{ matrix.goos }}-${{ matrix.goarch }} @@ -44,7 +48,7 @@ jobs: libs docs emmyLuaDocs - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: matrix.goos != 'windows' with: name: hilbish-${{ matrix.goos }}-${{ matrix.goarch }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9d2728b..453430d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 6515d25..d524457 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -9,8 +9,8 @@ jobs: gen: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 - name: Run docgen run: go run cmd/docgen/docgen.go - name: Commit new docs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 29d2b83..f4606c3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ jobs: create-release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: taiki-e/create-gh-release-action@v1 with: title: Hilbish $tag @@ -30,7 +30,7 @@ jobs: - goarch: arm64 goos: windows steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: true fetch-depth: 0 diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 88a78ae..d5859b8 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -1,27 +1,34 @@ name: Build website on: - - push - - pull_request + push: + branches: + - master + tags: + - v[0-9]+.* + pull_request: + branches: + - master + workflow_dispatch: jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: true fetch-depth: 0 - name: Setup Hugo - uses: peaceiris/actions-hugo@v2 + uses: peaceiris/actions-hugo@v3 with: - hugo-version: 'latest' + hugo-version: '0.111.3' extended: true - name: Set branch name id: branch - run: echo "BRANCH_NAME=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> "$GITHUB_ENV" + run: echo "BRANCH_NAME=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/*/}}" >> "$GITHUB_ENV" - name: Fix base URL if: env.BRANCH_NAME != 'master' && github.repository_owner == 'Rosettea' @@ -32,14 +39,14 @@ jobs: - name: Deploy if: env.BRANCH_NAME == 'master' && github.repository_owner == 'Rosettea' - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./website/public keep_files: true - name: Deploy if: env.BRANCH_NAME != 'master' && github.repository_owner == 'Rosettea' - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./website/public diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c12ab1..87a7c47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,85 @@ # 🎀 Changelog ## Unreleased +### Added +- Forward/Right arrow key will fill in hint text (#327) + +### Fixed +- Skip over file and prevent panic if info cannot be retrieved during file completion (due to permission error or anything else) + +## [2.3.3] - 2024-11-04 +### Fixed +- Heredocs having issues + +### Added +- Adding `\` at the end of input will add a newline and prompt for more input. + +## [2.3.2] - 2024-07-30 +### Fixed +- Command path searching due to 2.3 changes to the shell interpreter + +## [2.3.1] - 2024-07-27 +[hehe when you see it release](https://youtu.be/AaAF51Gwbxo?si=rhj2iYuQRkqDa693&t=64) + +### Added +- `hilbish.opts.tips` was added to display random tips on start up. +Displayed tips can be modified via the `hilbish.tips` table. + +### Fixed +- Fix a minor regression related to the cd command not working with relative paths +- Updated the motd for 2.3 + +## [2.3.0] - 2024-07-20 +### Added +- `commander.registry` function to get all registered commanders. +- `fs.pipe` function to get a pair of connected files (a pipe). +- Added an alternative 2nd parameter to `hilbish.run`, which is `streams`. +`streams` is a table of input and output streams to run the command with. +It uses these 3 keys: + - `input` as standard input for the command + - `out` as standard output + - `err` as standard error + +Here is a minimal example of the new usage which allows users to now pipe commands +directly via Lua functions: + +```lua +local fs = require 'fs' +local pr, pw = fs.pipe() +hilbish.run('ls -l', { + stdout = pw, + stderr = pw, +}) + +pw:close() + +hilbish.run('wc -l', { + stdin = pr +}) +``` + +### Changed +- The `-S` flag will be set to Hilbish's absolute path +- Hilbish now builds on any Unix (if any dependencies also work, which should.) + +### Fixed +- Fix ansi attributes causing issues with text when cut off in greenhouse +- Fix greenhouse appearing on terminal resize +- Fix crashes when history goes out of bounds when using history navigation +- `exec` command should return if no arg presented +- Commanders can now be cancelled by Ctrl-C and wont hang the shell anymore. +See [issue 198](https://github.com/Rosettea/Hilbish/issues/198). +- Shell interpreter can now preserve its environment and set PWD properly. + +## [2.2.3] - 2024-04-27 +### Fixed +- Highligher and hinter work now, since it was regressed from the previous minor release. +- `cat` command no longer prints extra newline at end of each file + +### Added +- `cat` command now reads files in chunks, allowing for reading large files + +## [2.2.2] - 2024-04-16 ### Fixed - Line refresh fixes (less flicker) - Do more checks for a TTY @@ -708,6 +787,11 @@ This input for example will prompt for more input to complete: First "stable" release of Hilbish. +[2.3.3]: https://github.com/Rosettea/Hilbish/compare/v2.3.2...v2.3.3 +[2.3.2]: https://github.com/Rosettea/Hilbish/compare/v2.3.1...v2.3.2 +[2.3.1]: https://github.com/Rosettea/Hilbish/compare/v2.3.0...v2.3.1 +[2.3.0]: https://github.com/Rosettea/Hilbish/compare/v2.2.3...v2.3.0 +[2.2.3]: https://github.com/Rosettea/Hilbish/compare/v2.2.2...v2.2.3 [2.2.2]: https://github.com/Rosettea/Hilbish/compare/v2.2.1...v2.2.2 [2.2.1]: https://github.com/Rosettea/Hilbish/compare/v2.2.0...v2.2.1 [2.2.0]: https://github.com/Rosettea/Hilbish/compare/v2.1.0...v2.2.0 diff --git a/README.md b/README.md index 469630b..aed2dfc 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ +> [!TIP] +> Check out [Hilbish: Midnight Edition](https://github.com/Rosettea/Hilbish/tree/midnight-edition) if you want to use C Lua, LuaJIT or anything related! +
🌓 The Moon-powered shell! A comfy and extensible shell for Lua fans! 🌺 ✨ @@ -10,19 +13,23 @@
Hilbish is an extensible shell designed to be highly customizable. -It is configured in Lua and provides a good range of features. -It aims to be easy to use for anyone but powerful enough for -those who need it. + +It is configured in Lua, and provides a good range of features. +It aims to be easy to use for anyone, and powerful enough for +those who need more. 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! +than old shell scripts. It's fine for basic interactive shell uses, +and supports [both Lua and Sh interactively](https://rosettea.github.io/Hilbish/docs/features/runner-mode/). + +That's the only place Hilbish can use traditional shell syntax though; +everything else is Lua and aims to be infinitely configurable. + +If something isn't, open an issue! # Screenshots
-
# Getting Hilbish @@ -36,7 +43,7 @@ on the website for distributed binaries from GitHub or other package repositorie Otherwise, continue reading for steps on compiling. ## Prerequisites -- [Go 1.17+](https://go.dev) +- [Go 1.22+](https://go.dev) - [Task](https://taskfile.dev/installation/) (**Go on the hyperlink here to see Task's install method for your OS.**) ## Build diff --git a/api.go b/api.go index 9470709..43e361a 100644 --- a/api.go +++ b/api.go @@ -16,6 +16,7 @@ import ( "bytes" "errors" "fmt" + "io" "os" "os/exec" "runtime" @@ -27,6 +28,7 @@ import ( rt "github.com/arnodel/golua/runtime" "github.com/arnodel/golua/lib/packagelib" + "github.com/arnodel/golua/lib/iolib" "github.com/maxlandon/readline" "mvdan.cc/sh/v3/interp" ) @@ -152,12 +154,64 @@ func unsetVimMode() { util.SetField(l, hshMod, "vimMode", rt.NilValue) } -// run(cmd, returnOut) -> exitCode (number), stdout (string), stderr (string) +func handleStream(v rt.Value, strms *streams, errStream bool) error { + ud, ok := v.TryUserData() + if !ok { + return errors.New("expected metatable argument") + } + + val := ud.Value() + var varstrm io.Writer + if f, ok := val.(*iolib.File); ok { + varstrm = f.Handle() + } + + if f, ok := val.(*sink); ok { + varstrm = f.writer + } + + if varstrm == nil { + return errors.New("expected either a sink or file") + } + + if errStream { + strms.stderr = varstrm + } else { + strms.stdout = varstrm + } + + return nil +} + +// run(cmd, streams) -> exitCode (number), stdout (string), stderr (string) // Runs `cmd` in Hilbish's shell script interpreter. +// The `streams` parameter specifies the output and input streams the command should use. +// For example, to write command output to a sink. +// As a table, the caller can directly specify the standard output, error, and input +// streams of the command with the table keys `out`, `err`, and `input` respectively. +// As a boolean, it specifies whether the command should use standard output or return its output streams. // #param cmd string -// #param returnOut boolean If this is true, the function will return the standard output and error of the command instead of printing it. +// #param streams table|boolean // #returns number, string, string +// #example +/* +// This code is the same as `ls -l | wc -l` +local fs = require 'fs' +local pr, pw = fs.pipe() +hilbish.run('ls -l', { + stdout = pw, + stderr = pw, +}) + +pw:close() + +hilbish.run('wc -l', { + stdin = pr +}) +*/ +// #example func hlrun(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + // TODO: ON BREAKING RELEASE, DO NOT ACCEPT `streams` AS A BOOLEAN. if err := c.Check1Arg(); err != nil { return nil, err } @@ -166,20 +220,57 @@ func hlrun(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return nil, err } + strms := &streams{} var terminalOut bool if len(c.Etc()) != 0 { tout := c.Etc()[0] - termOut, ok := tout.TryBool() - terminalOut = termOut + + var ok bool + terminalOut, ok = tout.TryBool() if !ok { - return nil, errors.New("bad argument to run (expected boolean, got " + tout.TypeName() + ")") + luastreams, ok := tout.TryTable() + if !ok { + return nil, errors.New("bad argument to run (expected boolean or table, got " + tout.TypeName() + ")") + } + + handleStream(luastreams.Get(rt.StringValue("out")), strms, false) + handleStream(luastreams.Get(rt.StringValue("err")), strms, true) + + stdinstrm := luastreams.Get(rt.StringValue("input")) + if !stdinstrm.IsNil() { + ud, ok := stdinstrm.TryUserData() + if !ok { + return nil, errors.New("bad type as run stdin stream (expected userdata as either sink or file, got " + stdinstrm.TypeName() + ")") + } + + val := ud.Value() + var varstrm io.Reader + if f, ok := val.(*iolib.File); ok { + varstrm = f.Handle() + } + + if f, ok := val.(*sink); ok { + varstrm = f.reader + } + + if varstrm == nil { + return nil, errors.New("bad type as run stdin stream (expected userdata as either sink or file)") + } + + strms.stdin = varstrm + } + } else { + if !terminalOut { + strms = &streams{ + stdout: new(bytes.Buffer), + stderr: new(bytes.Buffer), + } + } } - } else { - terminalOut = true } var exitcode uint8 - stdout, stderr, err := execCommand(cmd, terminalOut) + stdout, stderr, err := execCommand(cmd, strms) if code, ok := interp.IsExitStatus(err); ok { exitcode = code @@ -187,11 +278,12 @@ func hlrun(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { exitcode = 1 } - stdoutStr := "" - stderrStr := "" - if !terminalOut { - stdoutStr = stdout.(*bytes.Buffer).String() - stderrStr = stderr.(*bytes.Buffer).String() + var stdoutStr, stderrStr string + if stdoutBuf, ok := stdout.(*bytes.Buffer); ok { + stdoutStr = stdoutBuf.String() + } + if stderrBuf, ok := stderr.(*bytes.Buffer); ok { + stderrStr = stderrBuf.String() } return c.PushingNext(t.Runtime, rt.IntValue(int64(exitcode)), rt.StringValue(stdoutStr), rt.StringValue(stderrStr)), nil @@ -609,7 +701,7 @@ func hlwhich(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { cmd := strings.Split(alias, " ")[0] // check for commander - if commands[cmd] != nil { + if cmds.Commands[cmd] != nil { // they dont resolve to a path, so just send the cmd return c.PushingNext1(t.Runtime, rt.StringValue(cmd)), nil } @@ -712,5 +804,14 @@ func hlhinter(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { // #example // #param line string func hlhighlighter(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { - return c.Next(), nil + if err := c.Check1Arg(); err != nil { + return nil, err + } + + line, err := c.StringArg(0) + if err != nil { + return nil, err + } + + return c.PushingNext1(t.Runtime, rt.StringValue(line)), nil } diff --git a/cmd/docgen/docgen.go b/cmd/docgen/docgen.go index 86a622a..bf8fd1b 100644 --- a/cmd/docgen/docgen.go +++ b/cmd/docgen/docgen.go @@ -488,7 +488,11 @@ func main() { } mdTable.SetContent(i - diff, 0, fmt.Sprintf(`%s`, dps.FuncName, dps.FuncSig)) - mdTable.SetContent(i - diff, 1, dps.Doc[0]) + if len(dps.Doc) == 0 { + fmt.Printf("WARNING! Function %s on module %s has no documentation!\n", dps.FuncName, modname) + } else { + mdTable.SetContent(i - diff, 1, dps.Doc[0]) + } } f.WriteString(mdTable.String()) f.WriteString("\n") diff --git a/complete.go b/complete.go index 71d92fb..86938cb 100644 --- a/complete.go +++ b/complete.go @@ -128,7 +128,7 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) { } // add lua registered commands to completions - for cmdName := range commands { + for cmdName := range cmds.Commands { if strings.HasPrefix(cmdName, query) { completions = append(completions, cmdName) } @@ -157,9 +157,12 @@ func matchPath(query string) ([]string, string) { files, _ := os.ReadDir(path) for _, entry := range files { - // should we handle errors here? file, err := entry.Info() - if err == nil && file.Mode() & os.ModeSymlink != 0 { + if err != nil { + continue + } + + if file.Mode() & os.ModeSymlink != 0 { path, err := filepath.EvalSymlinks(filepath.Join(path, file.Name())) if err == nil { file, err = os.Lstat(path) diff --git a/docs/api/commander.md b/docs/api/commander.md index 03ece54..b910706 100644 --- a/docs/api/commander.md +++ b/docs/api/commander.md @@ -26,8 +26,11 @@ In this example, a command with the name of `hello` is created that will print `Hello world!` to output. One question you may have is: What is the `sinks` parameter? -The `sinks` parameter is a table with 3 keys: `in`, `out`, -and `err`. All of them are a Sink. +The `sinks` parameter is a table with 3 keys: `input`, `out`, and `err`. +There is an `in` alias to `input`, but it requires using the string accessor syntax (`sinks['in']`) +as `in` is also a Lua keyword, so `input` is preferred for use. +All of them are a Sink. +In the future, `sinks.in` will be removed. - `in` is the standard input. You may use the read functions on this sink to get input from the user. @@ -41,6 +44,7 @@ This sink is for writing errors, as the name would suggest. |----|----| |deregister(name)|Removes the named command. Note that this will only remove Commander-registered commands.| |register(name, cb)|Adds a new command with the given `name`. When Hilbish has to run a command with a name,| +|registry() -> table|Returns all registered commanders. Returns a list of tables with the following keys:|
@@ -91,3 +95,19 @@ end) ```
+
+
+

+commander.registry() -> table + + + +

+ +Returns all registered commanders. Returns a list of tables with the following keys: +- `exec`: The function used to run the commander. Commanders require args and sinks to be passed. + +#### Parameters +This function has no parameters. +
+ diff --git a/docs/api/fs.md b/docs/api/fs.md index bc14055..7b733ef 100644 --- a/docs/api/fs.md +++ b/docs/api/fs.md @@ -23,6 +23,7 @@ library offers more functions and will work on any operating system Hilbish does |glob(pattern) -> matches (table)|Match all files based on the provided `pattern`.| |join(...path) -> string|Takes any list of paths and joins them based on the operating system's path separator.| |mkdir(name, recursive)|Creates a new directory with the provided `name`.| +|fpipe() -> File, File|Returns a pair of connected files, also known as a pipe.| |readdir(path) -> table[string]|Returns a list of all files and directories in the provided path.| |stat(path) -> {}|Returns the information about a given `path`.| @@ -183,6 +184,22 @@ fs.mkdir('./foo/bar', true) ``` +
+
+

+fs.fpipe() -> File, File + + + +

+ +Returns a pair of connected files, also known as a pipe. +The type returned is a Lua file, same as returned from `io` functions. + +#### Parameters +This function has no parameters. +
+

diff --git a/docs/api/hilbish/_index.md b/docs/api/hilbish/_index.md index b79dcde..5c7a0f0 100644 --- a/docs/api/hilbish/_index.md +++ b/docs/api/hilbish/_index.md @@ -28,7 +28,7 @@ interfaces and functions which directly relate to shell functionality. |prependPath(dir)|Prepends `dir` to $PATH.| |prompt(str, typ)|Changes the shell prompt to the provided string.| |read(prompt) -> input (string)|Read input from the user, using Hilbish's line editor/input reader.| -|run(cmd, returnOut) -> exitCode (number), stdout (string), stderr (string)|Runs `cmd` in Hilbish's shell script interpreter.| +|run(cmd, streams) -> exitCode (number), stdout (string), stderr (string)|Runs `cmd` in Hilbish's shell script interpreter.| |runnerMode(mode)|Sets the execution/runner mode for interactive Hilbish.| |timeout(cb, time) -> @Timer|Executed the `cb` function after a period of `time`.| |which(name) -> string|Checks if `name` is a valid command.| @@ -413,21 +413,44 @@ Text to print before input, can be empty.

-hilbish.run(cmd, returnOut) -> exitCode (number), stdout (string), stderr (string) +hilbish.run(cmd, streams) -> exitCode (number), stdout (string), stderr (string)

Runs `cmd` in Hilbish's shell script interpreter. +The `streams` parameter specifies the output and input streams the command should use. +For example, to write command output to a sink. +As a table, the caller can directly specify the standard output, error, and input +streams of the command with the table keys `out`, `err`, and `input` respectively. +As a boolean, it specifies whether the command should use standard output or return its output streams. #### Parameters `string` **`cmd`** -`boolean` **`returnOut`** -If this is true, the function will return the standard output and error of the command instead of printing it. +`table|boolean` **`streams`** + +#### Example +```lua + +// This code is the same as `ls -l | wc -l` +local fs = require 'fs' +local pr, pw = fs.pipe() +hilbish.run('ls -l', { + stdout = pw, + stderr = pw, +}) + +pw:close() + +hilbish.run('wc -l', { + stdin = pr +}) + +```

diff --git a/docs/api/hilbish/hilbish.runner.md b/docs/api/hilbish/hilbish.runner.md index b5cfde4..8c89a52 100644 --- a/docs/api/hilbish/hilbish.runner.md +++ b/docs/api/hilbish/hilbish.runner.md @@ -21,16 +21,18 @@ A runner is passed the input and has to return a table with these values. All are not required, only the useful ones the runner needs to return. (So if there isn't an error, just omit `err`.) -- `exitCode` (number): A numerical code to indicate the exit result. -- `input` (string): The user input. This will be used to add -to the history. -- `err` (string): A string to indicate an interal error for the runner. -It can be set to a few special values for Hilbish to throw the right hooks and have a better looking message: - -`[command]: not-found` will throw a command.not-found hook based on what `[command]` is. - -`[command]: not-executable` will throw a command.not-executable hook. -- `continue` (boolean): Whether to prompt the user for more input. +- `exitCode` (number): Exit code of the command +- `input` (string): The text input of the user. This is used by Hilbish to append extra input, in case +more is requested. +- `err` (string): A string that represents an error from the runner. +This should only be set when, for example, there is a syntax error. +It can be set to a few special values for Hilbish to throw the right +hooks and have a better looking message. + - `\: not-found` will throw a `command.not-found` hook + based on what `\` is. + - `\: not-executable` will throw a `command.not-executable` hook. +- `continue` (boolean): Whether Hilbish should prompt the user for no input +- `newline` (boolean): Whether a newline should be added at the end of `input`. Here is a simple example of a fennel runner. It falls back to shell script if fennel eval has an error. diff --git a/docs/features/runner-mode.md b/docs/features/runner-mode.md index 0f7a8dd..ec804c1 100644 --- a/docs/features/runner-mode.md +++ b/docs/features/runner-mode.md @@ -33,19 +33,6 @@ needs to run interactive input. For more detail, see the [API documentation](../ The `hilbish.runner` interface is an alternative to using `hilbish.runnerMode` and also provides the shell script and Lua runner functions that Hilbish itself uses. -A runner function is expected to return a table with the following values: -- `exitCode` (number): Exit code of the command -- `input` (string): The text input of the user. This is used by Hilbish to append extra input, in case -more is requested. -- `err` (string): A string that represents an error from the runner. -This should only be set when, for example, there is a syntax error. -It can be set to a few special values for Hilbish to throw the right -hooks and have a better looking message. - - `: not-found` will throw a `command.not-found` hook - based on what `` is. - - `: not-executable` will throw a `command.not-executable` hook. -- `continue` (boolean): Whether Hilbish should prompt the user for no input - ## Functions These are the "low level" functions for the `hilbish.runner` interface. diff --git a/emmyLuaDocs/commander.lua b/emmyLuaDocs/commander.lua index 285c4b5..bfa69e5 100644 --- a/emmyLuaDocs/commander.lua +++ b/emmyLuaDocs/commander.lua @@ -11,4 +11,8 @@ function commander.deregister(name) end --- function commander.register(name, cb) end +--- Returns all registered commanders. Returns a list of tables with the following keys: +--- - `exec`: The function used to run the commander. Commanders require args and sinks to be passed. +function commander.registry() end + return commander diff --git a/emmyLuaDocs/fs.lua b/emmyLuaDocs/fs.lua index 89a418b..ef80eba 100644 --- a/emmyLuaDocs/fs.lua +++ b/emmyLuaDocs/fs.lua @@ -34,6 +34,10 @@ function fs.join(...path) end --- function fs.mkdir(name, recursive) end +--- Returns a pair of connected files, also known as a pipe. +--- The type returned is a Lua file, same as returned from `io` functions. +function fs.fpipe() end + --- Returns a list of all files and directories in the provided path. function fs.readdir(path) end diff --git a/emmyLuaDocs/hilbish.lua b/emmyLuaDocs/hilbish.lua index 7cca355..b80a660 100644 --- a/emmyLuaDocs/hilbish.lua +++ b/emmyLuaDocs/hilbish.lua @@ -132,7 +132,13 @@ function hilbish.prompt(str, typ) end function hilbish.read(prompt) end --- Runs `cmd` in Hilbish's shell script interpreter. -function hilbish.run(cmd, returnOut) end +--- The `streams` parameter specifies the output and input streams the command should use. +--- For example, to write command output to a sink. +--- As a table, the caller can directly specify the standard output, error, and input +--- streams of the command with the table keys `out`, `err`, and `input` respectively. +--- As a boolean, it specifies whether the command should use standard output or return its output streams. +--- +function hilbish.run(cmd, streams) end --- Sets the execution/runner mode for interactive Hilbish. --- This determines whether Hilbish wll try to run input as Lua diff --git a/exec.go b/exec.go index cf84231..357c143 100644 --- a/exec.go +++ b/exec.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "os" + "os/signal" "path/filepath" "runtime" "strings" @@ -28,6 +29,12 @@ var errNotExec = errors.New("not executable") var errNotFound = errors.New("not found") var runnerMode rt.Value = rt.StringValue("hybrid") +type streams struct { + stdout io.Writer + stderr io.Writer + stdin io.Reader +} + type execError struct{ typ string cmd string @@ -91,6 +98,7 @@ func runInput(input string, priv bool) { var exitCode uint8 var err error var cont bool + var newline bool // save incase it changes while prompting (For some reason) currentRunner := runnerMode if currentRunner.Type() == rt.StringType { @@ -101,9 +109,9 @@ func runInput(input string, priv bool) { cmdFinish(0, input, priv) return } - input, exitCode, cont, err = handleSh(input) + input, exitCode, cont, newline, err = handleSh(input) case "hybridRev": - _, _, _, err = handleSh(input) + _, _, _, _, err = handleSh(input) if err == nil { cmdFinish(0, input, priv) return @@ -112,12 +120,12 @@ func runInput(input string, priv bool) { case "lua": input, exitCode, err = handleLua(input) case "sh": - input, exitCode, cont, err = handleSh(input) + input, exitCode, cont, newline, err = handleSh(input) } } else { // can only be a string or function so var runnerErr error - input, exitCode, cont, runnerErr, err = runLuaRunner(currentRunner, input) + input, exitCode, cont, newline, runnerErr, err = runLuaRunner(currentRunner, input) if err != nil { fmt.Fprintln(os.Stderr, err) cmdFinish(124, input, priv) @@ -130,15 +138,15 @@ func runInput(input string, priv bool) { } if cont { - input, err = reprompt(input) + input, err = continuePrompt(input, newline) if err == nil { goto rerun } else if err == io.EOF { - return + lr.SetPrompt(fmtPrompt(prompt)) } } - if err != nil { + if err != nil && err != io.EOF { if exErr, ok := isExecError(err); ok { hooks.Emit("command." + exErr.typ, exErr.cmd) } else { @@ -148,26 +156,28 @@ func runInput(input string, priv bool) { cmdFinish(exitCode, input, priv) } -func reprompt(input string) (string, error) { +func reprompt(input string, newline bool) (string, error) { for { - in, err := continuePrompt(strings.TrimSuffix(input, "\\")) + /* + if strings.HasSuffix(input, "\\") { + input = strings.TrimSuffix(input, "\\") + "\n" + } + */ + in, err := continuePrompt(input, newline) if err != nil { lr.SetPrompt(fmtPrompt(prompt)) return input, err } - if strings.HasSuffix(in, "\\") { - continue - } return in, nil } } -func runLuaRunner(runr rt.Value, userInput string) (input string, exitCode uint8, continued bool, runnerErr, err error) { +func runLuaRunner(runr rt.Value, userInput string) (input string, exitCode uint8, continued bool, newline bool, runnerErr, err error) { term := rt.NewTerminationWith(l.MainThread().CurrentCont(), 3, false) err = rt.Call(l.MainThread(), runr, []rt.Value{rt.StringValue(userInput)}, term) if err != nil { - return "", 124, false, nil, err + return "", 124, false, false, nil, err } var runner *rt.Table @@ -195,6 +205,10 @@ func runLuaRunner(runr rt.Value, userInput string) (input string, exitCode uint8 if c, ok := runner.Get(rt.StringValue("continue")).TryBool(); ok { continued = c } + + if nl, ok := runner.Get(rt.StringValue("newline")).TryBool(); ok { + newline = nl + } return } @@ -225,55 +239,68 @@ func handleLua(input string) (string, uint8, error) { return cmdString, 125, err } -func handleSh(cmdString string) (input string, exitCode uint8, cont bool, runErr error) { +func handleSh(cmdString string) (input string, exitCode uint8, cont bool, newline bool, runErr error) { shRunner := hshMod.Get(rt.StringValue("runner")).AsTable().Get(rt.StringValue("sh")) var err error - input, exitCode, cont, runErr, err = runLuaRunner(shRunner, cmdString) + input, exitCode, cont, newline, runErr, err = runLuaRunner(shRunner, cmdString) if err != nil { runErr = err } return } -func execSh(cmdString string) (string, uint8, bool, error) { - _, _, err := execCommand(cmdString, true) +func execSh(cmdString string) (input string, exitcode uint8, cont bool, newline bool, e error) { + _, _, err := execCommand(cmdString, nil) if err != nil { // If input is incomplete, start multiline prompting if syntax.IsIncomplete(err) { if !interactive { - return cmdString, 126, false, err + return cmdString, 126, false, false, err } - return cmdString, 126, true, err + + newline := false + if strings.Contains(err.Error(), "unclosed here-document") { + newline = true + } + return cmdString, 126, true, newline, err } else { if code, ok := interp.IsExitStatus(err); ok { - return cmdString, code, false, nil + return cmdString, code, false, false, nil } else { - return cmdString, 126, false, err + return cmdString, 126, false, false, err } } } - return cmdString, 0, false, nil + return cmdString, 0, false, false, nil } // Run command in sh interpreter -func execCommand(cmd string, terminalOut bool) (io.Writer, io.Writer, error) { +func execCommand(cmd string, strms *streams) (io.Writer, io.Writer, error) { file, err := syntax.NewParser().Parse(strings.NewReader(cmd), "") if err != nil { return nil, nil, err } - 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) + if strms == nil { + strms = &streams{} } + + if strms.stdout == nil { + strms.stdout = os.Stdout + } + + if strms.stderr == nil { + strms.stderr = os.Stderr + } + + if strms.stdin == nil { + strms.stdin = os.Stdin + } + + interp.StdIO(strms.stdin, strms.stdout, strms.stderr)(runner) + interp.Env(nil)(runner) + buf := new(bytes.Buffer) printer := syntax.NewPrinter() @@ -292,11 +319,11 @@ func execCommand(cmd string, terminalOut bool) (io.Writer, io.Writer, error) { interp.ExecHandler(execHandle(bg))(runner) err = runner.Run(context.TODO(), stmt) if err != nil { - return stdout, stderr, err + return strms.stdout, strms.stderr, err } } - return stdout, stderr, nil + return strms.stdout, strms.stderr, nil } func execHandle(bg bool) interp.ExecHandlerFunc { @@ -327,17 +354,45 @@ func execHandle(bg bool) interp.ExecHandlerFunc { } hc := interp.HandlerCtx(ctx) - if commands[args[0]] != nil { + if cmd := cmds.Commands[args[0]]; cmd != nil { stdin := newSinkInput(hc.Stdin) stdout := newSinkOutput(hc.Stdout) stderr := newSinkOutput(hc.Stderr) sinks := rt.NewTable() sinks.Set(rt.StringValue("in"), rt.UserDataValue(stdin.ud)) + sinks.Set(rt.StringValue("input"), rt.UserDataValue(stdin.ud)) sinks.Set(rt.StringValue("out"), rt.UserDataValue(stdout.ud)) sinks.Set(rt.StringValue("err"), rt.UserDataValue(stderr.ud)) - luaexitcode, err := rt.Call1(l.MainThread(), rt.FunctionValue(commands[args[0]]), rt.TableValue(luacmdArgs), rt.TableValue(sinks)) + t := rt.NewThread(l) + sig := make(chan os.Signal) + exit := make(chan bool) + + luaexitcode := rt.IntValue(63) + var err error + go func() { + defer func() { + if r := recover(); r != nil { + exit <- true + } + }() + + signal.Notify(sig, os.Interrupt) + select { + case <-sig: + t.KillContext() + return + } + + }() + + go func() { + luaexitcode, err = rt.Call1(t, rt.FunctionValue(cmd), rt.TableValue(luacmdArgs), rt.TableValue(sinks)) + exit <- true + }() + + <-exit if err != nil { fmt.Fprintln(os.Stderr, "Error in command:\n" + err.Error()) return interp.NewExitStatus(1) @@ -349,14 +404,14 @@ func execHandle(bg bool) interp.ExecHandlerFunc { exitcode = uint8(code) } else if luaexitcode != rt.NilValue { // deregister commander - delete(commands, args[0]) + delete(cmds.Commands, args[0]) fmt.Fprintf(os.Stderr, "Commander did not return number for exit code. %s, you're fired.\n", args[0]) } return interp.NewExitStatus(exitcode) } - err := lookpath(args[0]) + path, err := lookpath(args[0]) if err == errNotExec { return execError{ typ: "not-executable", @@ -377,15 +432,16 @@ func execHandle(bg bool) interp.ExecHandlerFunc { killTimeout := 2 * time.Second // from here is basically copy-paste of the default exec handler from // sh/interp but with our job handling - path, err := interp.LookPathDir(hc.Dir, hc.Env, args[0]) - if err != nil { - fmt.Fprintln(hc.Stderr, err) - return interp.NewExitStatus(127) - } env := hc.Env envList := make([]string, 0, 64) env.Each(func(name string, vr expand.Variable) bool { + if name == "PATH" { + pathEnv := os.Getenv("PATH") + envList = append(envList, "PATH="+pathEnv) + return true + } + 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 @@ -403,6 +459,7 @@ func execHandle(bg bool) interp.ExecHandlerFunc { } return true }) + cmd := exec.Cmd{ Path: path, Args: args, @@ -485,7 +542,7 @@ func handleExecErr(err error) (exit uint8) { return } -func lookpath(file string) error { // custom lookpath function so we know if a command is found *and* is executable +func lookpath(file string) (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:"} @@ -494,20 +551,20 @@ func lookpath(file string) error { // custom lookpath function so we know if a c } for _, s := range skip { if strings.HasPrefix(file, s) { - return findExecutable(file, false, false) + return file, findExecutable(file, false, false) } } for _, dir := range filepath.SplitList(os.Getenv("PATH")) { path := filepath.Join(dir, file) err := findExecutable(path, true, false) if err == errNotExec { - return err + return "", err } else if err == nil { - return nil + return path, nil } } - return os.ErrNotExist + return "", os.ErrNotExist } func splitInput(input string) ([]string, string) { diff --git a/execfile_unix.go b/execfile_unix.go index 44f924a..82c738b 100644 --- a/execfile_unix.go +++ b/execfile_unix.go @@ -1,4 +1,4 @@ -// +build linux darwin +//go:build unix package main diff --git a/execfile_windows.go b/execfile_windows.go index 4b3feef..3d6ef61 100644 --- a/execfile_windows.go +++ b/execfile_windows.go @@ -1,4 +1,4 @@ -// +build windows +//go:build windows package main diff --git a/go.mod b/go.mod index 65a463c..b9899cb 100644 --- a/go.mod +++ b/go.mod @@ -1,35 +1,38 @@ module hilbish -go 1.18 +go 1.21 + +toolchain go1.22.2 require ( - github.com/arnodel/golua v0.0.0-20220221163911-dfcf252b6f86 + github.com/arnodel/golua v0.0.0-20230215163904-e0b5347eaaa1 github.com/atsushinee/go-markdown-generator v0.0.0-20191121114853-83f9e1f68504 - github.com/blackfireio/osinfo v1.0.3 - github.com/maxlandon/readline v0.1.0-beta.0.20211027085530-2b76cabb8036 + github.com/blackfireio/osinfo v1.0.5 + github.com/maxlandon/readline v1.0.14 github.com/pborman/getopt v1.1.0 github.com/rjeczalik/notify v0.9.3 - github.com/sahilm/fuzzy v0.1.0 - golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a - golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 - mvdan.cc/sh/v3 v3.5.1 + github.com/sahilm/fuzzy v0.1.1 + golang.org/x/sys v0.22.0 + golang.org/x/term v0.22.0 + mvdan.cc/sh/v3 v3.0.0-00010101000000-000000000000 ) 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/evilsocket/islazy v1.11.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect + github.com/muesli/cancelreader v0.2.2 // 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-20220513210516-0976fa681c29 // indirect - golang.org/x/text v0.3.7 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/text v0.14.0 // indirect ) -replace mvdan.cc/sh/v3 => github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20220524215627-dfd9a4fa219b +replace mvdan.cc/sh/v3 => github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20240815163633-562273e09b73 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-20221213193027-cbf6d4e4d345 +replace github.com/arnodel/golua => github.com/Rosettea/golua v0.0.0-20241104031959-5551ea280f23 diff --git a/go.sum b/go.sum index 64bdf98..b06fa3f 100644 --- a/go.sum +++ b/go.sum @@ -1,72 +1,49 @@ -github.com/Rosettea/golua v0.0.0-20221213193027-cbf6d4e4d345 h1:QNYjYDogUSiNUkffbhFSrSCtpZhofeiVYGFN2FI4wSs= -github.com/Rosettea/golua v0.0.0-20221213193027-cbf6d4e4d345/go.mod h1:9jzpYPiU2is0HVGCiuIOBSXdergHUW44IEjmuN1UrIE= -github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20220524215627-dfd9a4fa219b h1:s5eDMhBk6H1BgipgLub/gv9qeyBaTuiHM0k3h2/9TSE= -github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20220524215627-dfd9a4fa219b/go.mod h1:R09vh/04ILvP2Gj8/Z9Jd0Dh0ZIvaucowMEs6abQpWs= +github.com/Rosettea/golua v0.0.0-20241104031959-5551ea280f23 h1:mUZnT0gmDEmTkqXsbnDbuJ3CNil7DCOMiCQYgjbKIdI= +github.com/Rosettea/golua v0.0.0-20241104031959-5551ea280f23/go.mod h1:9jzpYPiU2is0HVGCiuIOBSXdergHUW44IEjmuN1UrIE= +github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20240815163633-562273e09b73 h1:zTTUJqNnrF2qf4LgygN8Oae5Uxn6ewH0hA8jyTCHfXw= +github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20240815163633-562273e09b73/go.mod h1:YZalN5H7WNQw3DGij6IvHsEhn5YMW7M2FCwG6gnfKy4= 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/atsushinee/go-markdown-generator v0.0.0-20191121114853-83f9e1f68504 h1:R1/AOzdMbopSliUTTEHvHbyNmnZ3YxY5GvdhTkpPsSY= github.com/atsushinee/go-markdown-generator v0.0.0-20191121114853-83f9e1f68504/go.mod h1:kHBCvAXJIatTX1pw6tLiOspjGc3MhUDRlog9yrCUS+k= -github.com/blackfireio/osinfo v1.0.3 h1:Yk2t2GTPjBcESv6nDSWZKO87bGMQgO+Hi9OoXPpxX8c= -github.com/blackfireio/osinfo v1.0.3/go.mod h1:Pd987poVNmd5Wsx6PRPw4+w7kLlf9iJxoRKPtPAjOrA= -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= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= -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/blackfireio/osinfo v1.0.5 h1:6hlaWzfcpb87gRmznVf7wSdhysGqLRz9V/xuSdCEXrA= +github.com/blackfireio/osinfo v1.0.5/go.mod h1:Pd987poVNmd5Wsx6PRPw4+w7kLlf9iJxoRKPtPAjOrA= +github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= +github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/evilsocket/islazy v1.11.0 h1:B5w6uuS6ki6iDG+aH/RFeoMb8ijQh/pGabewqp2UeJ0= +github.com/evilsocket/islazy v1.11.0/go.mod h1:muYH4x5MB5YRdkxnrOtrXLIBX6LySj1uFIqys94LKdo= +github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= +github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -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/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 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/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY= github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc= -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/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= -github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4= -golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= +github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/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-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/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-20220411215600-e5f449aeb171 h1:EH1Deb8WZJ0xc0WK//leUHXcX9aLE5SymusoTmMZye8= -golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/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/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -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= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= diff --git a/golibs/commander/commander.go b/golibs/commander/commander.go index ea2da7a..840aaa1 100644 --- a/golibs/commander/commander.go +++ b/golibs/commander/commander.go @@ -17,8 +17,11 @@ In this example, a command with the name of `hello` is created that will print `Hello world!` to output. One question you may have is: What is the `sinks` parameter? -The `sinks` parameter is a table with 3 keys: `in`, `out`, -and `err`. All of them are a @Sink. +The `sinks` parameter is a table with 3 keys: `input`, `out`, and `err`. +There is an `in` alias to `input`, but it requires using the string accessor syntax (`sinks['in']`) +as `in` is also a Lua keyword, so `input` is preferred for use. +All of them are a @Sink. +In the future, `sinks.in` will be removed. - `in` is the standard input. You may use the read functions on this sink to get input from the user. @@ -40,11 +43,13 @@ import ( type Commander struct{ Events *bait.Bait Loader packagelib.Loader + Commands map[string]*rt.Closure } -func New(rtm *rt.Runtime) Commander { - c := Commander{ +func New(rtm *rt.Runtime) *Commander { + c := &Commander{ Events: bait.New(rtm), + Commands: make(map[string]*rt.Closure), } c.Loader = packagelib.Loader{ Load: c.loaderFunc, @@ -58,6 +63,7 @@ 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}, + "registry": util.LuaExport{c.cregistry, 0, false}, } mod := rt.NewTable() util.SetExports(rtm, mod, exports) @@ -88,7 +94,7 @@ func (c *Commander) cregister(t *rt.Thread, ct *rt.GoCont) (rt.Cont, error) { return nil, err } - c.Events.Emit("commandRegister", cmdName, cmd) + c.Commands[cmdName] = cmd return ct.Next(), err } @@ -105,7 +111,23 @@ func (c *Commander) cderegister(t *rt.Thread, ct *rt.GoCont) (rt.Cont, error) { return nil, err } - c.Events.Emit("commandDeregister", cmdName) + delete(c.Commands, cmdName) return ct.Next(), err } + +// registry() -> table +// Returns all registered commanders. Returns a list of tables with the following keys: +// - `exec`: The function used to run the commander. Commanders require args and sinks to be passed. +// #returns table +func (c *Commander) cregistry(t *rt.Thread, ct *rt.GoCont) (rt.Cont, error) { + registryLua := rt.NewTable() + for cmdName, cmd := range c.Commands { + cmdTbl := rt.NewTable() + cmdTbl.Set(rt.StringValue("exec"), rt.FunctionValue(cmd)) + + registryLua.Set(rt.StringValue(cmdName), rt.TableValue(cmdTbl)) + } + + return ct.PushingNext1(t.Runtime, rt.TableValue(registryLua)), nil +} diff --git a/golibs/fs/fs.go b/golibs/fs/fs.go index 6c11e31..2933797 100644 --- a/golibs/fs/fs.go +++ b/golibs/fs/fs.go @@ -18,17 +18,29 @@ import ( rt "github.com/arnodel/golua/runtime" "github.com/arnodel/golua/lib/packagelib" + "github.com/arnodel/golua/lib/iolib" + "mvdan.cc/sh/v3/interp" ) -var rtmm *rt.Runtime var watcherMetaKey = rt.StringValue("hshwatcher") -var Loader = packagelib.Loader{ - Load: loaderFunc, - Name: "fs", +type fs struct{ + runner *interp.Runner + Loader packagelib.Loader } -func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) { - rtmm = rtm +func New(runner *interp.Runner) *fs { + f := &fs{ + runner: runner, + } + f.Loader = packagelib.Loader{ + Load: f.loaderFunc, + Name: "fs", + } + + return f +} + +func (f *fs) loaderFunc(rtm *rt.Runtime) (rt.Value, func()) { watcherMethods := rt.NewTable() watcherFuncs := map[string]util.LuaExport{ "start": {watcherStart, 1, false}, @@ -54,15 +66,16 @@ func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) { rtm.SetRegistry(watcherMetaKey, rt.TableValue(watcherMeta)) 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}, - "abs": util.LuaExport{fabs, 1, false}, - "basename": util.LuaExport{fbasename, 1, false}, - "dir": util.LuaExport{fdir, 1, false}, - "glob": util.LuaExport{fglob, 1, false}, - "join": util.LuaExport{fjoin, 0, true}, + "cd": util.LuaExport{f.fcd, 1, false}, + "mkdir": util.LuaExport{f.fmkdir, 2, false}, + "stat": util.LuaExport{f.fstat, 1, false}, + "readdir": util.LuaExport{f.freaddir, 1, false}, + "abs": util.LuaExport{f.fabs, 1, false}, + "basename": util.LuaExport{f.fbasename, 1, false}, + "dir": util.LuaExport{f.fdir, 1, false}, + "glob": util.LuaExport{f.fglob, 1, false}, + "join": util.LuaExport{f.fjoin, 0, true}, + "pipe": util.LuaExport{f.fpipe, 0, false}, "watch": util.LuaExport{fwatch, 2, false}, } mod := rt.NewTable() @@ -78,7 +91,7 @@ func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) { // This can be used to resolve short paths like `..` to `/home/user`. // #param path string // #returns string -func fabs(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { +func (f *fs) fabs(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { path, err := c.StringArg(0) if err != nil { return nil, err @@ -98,7 +111,7 @@ func fabs(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { // `.` will be returned. // #param path string Path to get the base name of. // #returns string -func fbasename(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { +func (f *fs) fbasename(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err } @@ -113,7 +126,7 @@ func fbasename(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { // cd(dir) // Changes Hilbish's directory to `dir`. // #param dir string Path to change directory to. -func fcd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { +func (f *fs) fcd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err } @@ -123,10 +136,12 @@ func fcd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { } path = util.ExpandHome(strings.TrimSpace(path)) + abspath, _ := filepath.Abs(path) err = os.Chdir(path) if err != nil { return nil, err } + interp.Dir(abspath)(f.runner) return c.Next(), err } @@ -136,7 +151,7 @@ func fcd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { // `~/Documents/doc.txt` then this function will return `~/Documents`. // #param path string Path to get the directory for. // #returns string -func fdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { +func (f *fs) fdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err } @@ -167,7 +182,7 @@ print(matches) -- -> {'init.lua', 'code.lua'} #example */ -func fglob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { +func (f *fs) fglob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err } @@ -201,7 +216,7 @@ print(fs.join(hilbish.userDir.config, 'hilbish')) -- -> '/home/user/.config/hilbish' on Linux #example */ -func fjoin(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { +func (f *fs) fjoin(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { strs := make([]string, len(c.Etc())) for i, v := range c.Etc() { if v.Type() != rt.StringType { @@ -228,7 +243,7 @@ func fjoin(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { fs.mkdir('./foo/bar', true) #example */ -func fmkdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { +func (f *fs) fmkdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.CheckNArgs(2); err != nil { return nil, err } @@ -254,11 +269,27 @@ func fmkdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.Next(), err } +// fpipe() -> File, File +// Returns a pair of connected files, also known as a pipe. +// The type returned is a Lua file, same as returned from `io` functions. +// #returns File +// #returns File +func (f *fs) fpipe(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + rf, wf, err := os.Pipe() + if err != nil { + return nil, err + } + + rfLua := iolib.NewFile(rf, 0) + wfLua := iolib.NewFile(wf, 0) + + return c.PushingNext(t.Runtime, rfLua.Value(t.Runtime), wfLua.Value(t.Runtime)), nil +} // readdir(path) -> table[string] // Returns a list of all files and directories in the provided path. // #param dir string // #returns table -func freaddir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { +func (f *fs) freaddir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err } @@ -306,7 +337,7 @@ Would print the following: ]]-- #example */ -func fstat(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { +func (f *fs) fstat(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err } @@ -350,7 +381,7 @@ func fwatch(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return nil, err } - dw := newWatcher(dir, watcher) + dw := newWatcher(dir, watcher, t.Runtime) return c.PushingNext1(t.Runtime, rt.UserDataValue(dw.ud)), nil } diff --git a/golibs/fs/watcher.go b/golibs/fs/watcher.go index f4f34ab..b92ba6d 100644 --- a/golibs/fs/watcher.go +++ b/golibs/fs/watcher.go @@ -16,6 +16,7 @@ type watcher struct{ started bool ud *rt.UserData notifyChan chan notify.EventInfo + rtm *rt.Runtime } func (w *watcher) start() { @@ -32,7 +33,7 @@ func (w *watcher) start() { ev := notif.Event().String() path := notif.Path() - _, err := rt.Call1(rtmm.MainThread(), rt.FunctionValue(w.callback), rt.StringValue(ev), rt.StringValue(path)) + _, err := rt.Call1(w.rtm.MainThread(), rt.FunctionValue(w.callback), rt.StringValue(ev), rt.StringValue(path)) if err != nil { // TODO: throw error } @@ -73,9 +74,10 @@ func watcherStop(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.Next(), nil } -func newWatcher(path string, callback *rt.Closure) *watcher { +func newWatcher(path string, callback *rt.Closure, rtm *rt.Runtime) *watcher { pw := &watcher{ path: path, + rtm: rtm, callback: callback, } pw.ud = watcherUserData(pw) @@ -104,6 +106,6 @@ func valueToWatcher(val rt.Value) (*watcher, bool) { } func watcherUserData(j *watcher) *rt.UserData { - watcherMeta := rtmm.Registry(watcherMetaKey) + watcherMeta := j.rtm.Registry(watcherMetaKey) return rt.NewUserData(j, watcherMeta.AsTable()) } diff --git a/init_windows.go b/init_windows.go index 825069d..e76629b 100644 --- a/init_windows.go +++ b/init_windows.go @@ -1,4 +1,4 @@ -// +build windows +//go:build windows package main diff --git a/job_unix.go b/job_unix.go index 5029012..0a038b1 100644 --- a/job_unix.go +++ b/job_unix.go @@ -1,4 +1,4 @@ -// +build darwin linux +//go:build unix package main diff --git a/job_windows.go b/job_windows.go index 140a5d1..26818b5 100644 --- a/job_windows.go +++ b/job_windows.go @@ -1,4 +1,4 @@ -// +build windows +//go:build windows package main diff --git a/lua.go b/lua.go index e46d27b..88fedf8 100644 --- a/lua.go +++ b/lua.go @@ -30,22 +30,11 @@ func luaInit() { util.DoString(l, "hilbish = require 'hilbish'") // Add fs and terminal module module to Lua - lib.LoadLibs(l, fs.Loader) + f := fs.New(runner) + lib.LoadLibs(l, f.Loader) lib.LoadLibs(l, terminal.Loader) - cmds := commander.New(l) - // When a command from Lua is added, register it for use - cmds.Events.On("commandRegister", func(args ...interface{}) { - cmdName := args[0].(string) - cmd := args[1].(*rt.Closure) - - commands[cmdName] = cmd - }) - cmds.Events.On("commandDeregister", func(args ...interface{}) { - cmdName := args[0].(string) - - delete(commands, cmdName) - }) + cmds = commander.New(l) lib.LoadLibs(l, cmds.Loader) hooks = bait.New(l) diff --git a/main.go b/main.go index 4b756c0..1bddfc4 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "os" + "os/exec" "os/user" "path/filepath" "runtime" @@ -14,18 +15,19 @@ import ( "hilbish/util" "hilbish/golibs/bait" + "hilbish/golibs/commander" rt "github.com/arnodel/golua/runtime" "github.com/pborman/getopt" "github.com/maxlandon/readline" "golang.org/x/term" + "mvdan.cc/sh/v3/interp" ) var ( l *rt.Runtime lr *lineReader - commands = map[string]*rt.Closure{} luaCompletions = map[string]*rt.Closure{} confDir string @@ -33,11 +35,14 @@ var ( curuser *user.User hooks *bait.Bait + cmds *commander.Commander defaultConfPath string defaultHistPath string + runner *interp.Runner ) func main() { + runner, _ = interp.New() curuser, _ = user.Current() homedir := curuser.HomeDir confDir, _ = os.UserConfigDir() @@ -114,7 +119,13 @@ func main() { // Set $SHELL if the user wants to if *setshflag { - os.Setenv("SHELL", os.Args[0]) + os.Setenv("SHELL", "hilbish") + + path, err := exec.LookPath("hilbish") + if err == nil { + os.Setenv("SHELL", path) + } + } lr = newLineReader("", false) @@ -212,8 +223,9 @@ input: } if strings.HasSuffix(input, "\\") { + print("\n") for { - input, err = continuePrompt(input) + input, err = continuePrompt(strings.TrimSuffix(input, "\\") + "\n", false) if err != nil { running = true lr.SetPrompt(fmtPrompt(prompt)) @@ -237,16 +249,24 @@ input: exit(0) } -func continuePrompt(prev string) (string, error) { +func continuePrompt(prev string, newline bool) (string, error) { hooks.Emit("multiline", nil) lr.SetPrompt(multilinePrompt) + cont, err := lr.Read() if err != nil { return "", err } - cont = strings.TrimSpace(cont) - return prev + strings.TrimSuffix(cont, "\n"), nil + if newline { + cont = "\n" + cont + } + + if strings.HasSuffix(cont, "\\") { + cont = strings.TrimSuffix(cont, "\\") + "\n" + } + + return prev + cont, nil } // This semi cursed function formats our prompt (obviously) diff --git a/nature/commands/cat.lua b/nature/commands/cat.lua index 06df507..a2375e9 100644 --- a/nature/commands/cat.lua +++ b/nature/commands/cat.lua @@ -9,6 +9,8 @@ commander.register('cat', function(args, sinks) usage: cat [file]...]] end + local chunkSize = 2^13 -- 8K buffer size + for _, fName in ipairs(args) do local f = io.open(fName) if f == nil then @@ -17,7 +19,11 @@ usage: cat [file]...]] goto continue end - sinks.out:writeln(f:read '*a') + while true do + local block = f:read(chunkSize) + if not block then break end + sinks.out:write(block) + end ::continue:: end io.flush() diff --git a/nature/commands/exec.lua b/nature/commands/exec.lua index d279e31..61ef923 100644 --- a/nature/commands/exec.lua +++ b/nature/commands/exec.lua @@ -1,5 +1,8 @@ local commander = require 'commander' commander.register('exec', function(args) + if #args == 0 then + return + end hilbish.exec(args[1]) end) diff --git a/nature/doc.lua b/nature/doc.lua index 657af51..f0b7e11 100644 --- a/nature/doc.lua +++ b/nature/doc.lua @@ -17,8 +17,8 @@ function M.renderCodeBlock(text) end for i, line in ipairs(lines) do - lines[i] = ' ' .. M.highlight(line:sub(0, longest)) - .. string.rep(' ', longest - line:len()) .. ' ' + lines[i] = lunacolors.format('{greyBg}' .. ' ' .. M.highlight(line:sub(0, longest)) + .. string.rep(' ', longest - line:len()) .. ' ') end return '\n' .. lunacolors.format('{greyBg}' .. table.concat(lines, '\n')) .. '\n' diff --git a/nature/greenhouse/init.lua b/nature/greenhouse/init.lua index 50d5fad..fe4c31c 100644 --- a/nature/greenhouse/init.lua +++ b/nature/greenhouse/init.lua @@ -61,17 +61,24 @@ function Greenhouse:updateCurrentPage(text) page:setText(text) end +local ansiPatters = { + '\x1b%[%d+;%d+;%d+;%d+;%d+%w', + '\x1b%[%d+;%d+;%d+;%d+%w', + '\x1b%[%d+;%d+;%d+%w', + '\x1b%[%d+;%d+%w', + '\x1b%[%d+%w' +} + function Greenhouse:sub(str, offset, limit) local overhead = 0 local function addOverhead(s) overhead = overhead + string.len(s) end - local s = str:gsub('\x1b%[%d+;%d+;%d+;%d+;%d+%w', addOverhead) - :gsub('\x1b%[%d+;%d+;%d+;%d+%w', addOverhead) - :gsub('\x1b%[%d+;%d+;%d+%w',addOverhead) - :gsub('\x1b%[%d+;%d+%w', addOverhead) - :gsub('\x1b%[%d+%w', addOverhead) + local s = str + for _, pat in ipairs(ansiPatters) do + s = s:gsub(pat, addOverhead) + end return s:sub(offset, utf8.offset(str, limit + overhead) or limit + overhead) --return s:sub(offset, limit + overhead) @@ -94,14 +101,40 @@ function Greenhouse:draw() self.sink:write(ansikit.getCSI(2, 'J')) local writer = self.sink.writeln + self.attributes = {} for i = offset, offset + self.region.height - 1 do + local resetEnd = false if i > #lines then break end if i == offset + self.region.height - 1 then writer = self.sink.write end self.sink:write(ansikit.getCSI(self.start + i - offset .. ';1', 'H')) local line = lines[i]:gsub('{separator}', function() return self.separator:rep(self.region.width - 1) end) - writer(self.sink, self:sub(line:gsub('\t', ' '), self.horizOffset, self.region.width)) + for _, pat in ipairs(ansiPatters) do + line:gsub(pat, function(s) + if s == lunacolors.formatColors.reset then + self.attributes = {} + resetEnd = true + else + --resetEnd = false + --table.insert(self.attributes, s) + end + end) + end + +--[[ + if #self.attributes ~= 0 then + for _, attr in ipairs(self.attributes) do + --writer(self.sink, attr) + end + end +]]-- + + self.sink:write(lunacolors.formatColors.reset) + writer(self.sink, self:sub(line:gsub('\t', ' '), self.horizOffset, self.region.width + self.horizOffset)) + if resetEnd then + self.sink:write(lunacolors.formatColors.reset) + end end writer(self.sink, '\27[0m') self:render() @@ -271,6 +304,15 @@ end function Greenhouse:input(char) end +local function read() + terminal.saveState() + terminal.setRaw() + local c = hilbish.editor.readChar() + + terminal.restoreState() + return c +end + function Greenhouse:initUi() local ansikit = require 'ansikit' local bait = require 'bait' @@ -280,14 +322,17 @@ function Greenhouse:initUi() local Page = require 'nature.greenhouse.page' local done = false - bait.catch('signal.sigint', function() + local function sigint() ansikit.clear() done = true - end) + end - bait.catch('signal.resize', function() + local function resize() self:update() - end) + end + bait.catch('signal.sigint', sigint) + + bait.catch('signal.resize', resize) ansikit.screenAlt() ansikit.clear(true) @@ -311,15 +356,10 @@ function Greenhouse:initUi() ansikit.showCursor() ansikit.screenMain() -end -function read() - terminal.saveState() - terminal.setRaw() - local c = hilbish.editor.readChar() - - terminal.restoreState() - return c + self = nil + bait.release('signal.sigint', sigint) + bait.release('signal.resize', resize) end return Greenhouse diff --git a/nature/opts/init.lua b/nature/opts/init.lua index 474ea3b..d55864f 100644 --- a/nature/opts/init.lua +++ b/nature/opts/init.lua @@ -14,7 +14,8 @@ The nice lil shell for {blue}Lua{reset} fanatics! motd = true, fuzzy = false, notifyJobFinish = true, - crimmas = true + crimmas = true, + tips = true } for optsName, default in pairs(defaultOpts) do diff --git a/nature/opts/motd.lua b/nature/opts/motd.lua index c1f31b4..ca08142 100644 --- a/nature/opts/motd.lua +++ b/nature/opts/motd.lua @@ -2,8 +2,9 @@ local bait = require 'bait' local lunacolors = require 'lunacolors' hilbish.motd = [[ -Finally at {red}v2.2!{reset} So much {green}documentation improvements{reset} -and 1 single fix for Windows! {blue}.. and a feature they can't use.{reset} +Wait ... {magenta}2.3{reset} is basically the same as {red}2.2?{reset} +Erm.. {blue}Ctrl-C works for Commanders,{reset} {cyan}and the sh runner has some fixes.{reset} +Just trust me bro, this is an important bug fix release. {red}- 🌺 sammyette{reset} ]] bait.catch('hilbish.init', function() diff --git a/nature/opts/tips.lua b/nature/opts/tips.lua new file mode 100644 index 0000000..c951b2e --- /dev/null +++ b/nature/opts/tips.lua @@ -0,0 +1,35 @@ +local bait = require 'bait' +local lunacolors = require 'lunacolors' + +local postamble = [[ +{yellow}These tips can be disabled with {reset}{invert} hilbish.opts.tips = false {reset} +]] + +hilbish.tips = { + 'Join the discord and say hi! {blue}https://discord.gg/3PDdcQz{reset}', + '{green}hilbish.alias{reset} interface manages shell aliases. See more detail by running {blue}doc api hilbish.alias.', + '{green}hilbish.appendPath(\'path\'){reset} -> Appends the provided dir to the command path ($PATH)', + '{green}hilbish.completions{reset} -> Used to control suggestions when tab completing.', + '{green}hilbish.message{reset} -> Simple notification system which can be used by other plugins and parts of the shell to notify the user of various actions.', + [[ +{green}hilbish.opts{reset} -> Simple toggle or value options a user can set. +You may disable the startup greeting by {invert}hilbish.opts.greeting = false{reset} +]], +[[ +{green}hilbish.runner{reset} -> The runner interface contains functions to +manage how Hilbish interprets interactive input. The default runners can run +shell script and Lua code! +]], +[[ +Add Lua-written commands with the commander module! +Check the command {blue}doc api commander{reset} or the web docs: +https://rosettea.github.io/Hilbish/docs/api/commander/ +]] +} + +bait.catch('hilbish.init', function() + if hilbish.interactive and hilbish.opts.tips then + local idx = math.random(1, #hilbish.tips) + print(lunacolors.format('{yellow}🛈 Tip:{reset} ' .. hilbish.tips[idx] .. '\n' .. postamble)) + end +end) diff --git a/pprof.go b/pprof.go index 977eeb0..ac4ed55 100644 --- a/pprof.go +++ b/pprof.go @@ -1,4 +1,4 @@ -// +build pprof +//go:build pprof package main diff --git a/readline/hint.go b/readline/hint.go index 6c6a67c..d0c54fe 100644 --- a/readline/hint.go +++ b/readline/hint.go @@ -56,3 +56,10 @@ func (rl *Instance) resetHintText() { //rl.hintY = 0 rl.hintText = []rune{} } + +func (rl *Instance) insertHintText() { + if len(rl.hintText) != 0 { + // fill in hint text + rl.insert(rl.hintText) + } +} diff --git a/readline/history.go b/readline/history.go index e226b4d..0c87a62 100644 --- a/readline/history.go +++ b/readline/history.go @@ -128,15 +128,19 @@ func (rl *Instance) walkHistory(i int) { } rl.histOffset += i + historyLen := history.Len() if rl.histOffset == 0 { rl.line = []rune(rl.lineBuf) rl.pos = len(rl.lineBuf) } else if rl.histOffset <= -1 { rl.histOffset = 0 + } else if rl.histOffset > historyLen { + // TODO: should this wrap around?s + rl.histOffset = 0 } else { dedup = true old = string(rl.line) - new, err = history.GetLine(history.Len() - rl.histOffset) + new, err = history.GetLine(historyLen - rl.histOffset) if err != nil { rl.resetHelpers() print("\r\n" + err.Error() + "\r\n") diff --git a/readline/readline.go b/readline/readline.go index 627bff4..7282071 100644 --- a/readline/readline.go +++ b/readline/readline.go @@ -707,6 +707,9 @@ func (rl *Instance) escapeSeq(r []rune) { rl.renderHelpers() return } + + rl.insertHintText() + if (rl.modeViMode == VimInsert && rl.pos < len(rl.line)) || (rl.modeViMode != VimInsert && rl.pos < len(rl.line)-1) { rl.moveCursorByAdjust(1) diff --git a/readline/tabfind.go b/readline/tabfind.go index 830dad3..3e46312 100644 --- a/readline/tabfind.go +++ b/readline/tabfind.go @@ -29,7 +29,7 @@ func (rl *Instance) updateTabFind(r []rune) { rl.search = string(rl.tfLine) // We update and print - rl.clearHelpers() + //rl.clearHelpers() rl.getTabCompletion() rl.renderHelpers() } diff --git a/readline/update.go b/readline/update.go index 66b3ba0..0538aad 100644 --- a/readline/update.go +++ b/readline/update.go @@ -121,7 +121,7 @@ func (rl *Instance) clearHelpers() { moveCursorForwards(rl.fullX) // Clear everything below - //print(seqClearScreenBelow) + print(seqClearScreenBelow) // Go back to current cursor position moveCursorBackwards(GetTermWidth()) diff --git a/rl.go b/rl.go index 7d5ed89..231d04b 100644 --- a/rl.go +++ b/rl.go @@ -70,11 +70,8 @@ func newLineReader(prompt string, noHist bool) *lineReader { hooks.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(hinter), + hinter := hshMod.Get(rt.StringValue("hinter")) + retVal, err := rt.Call1(l.MainThread(), hinter, rt.StringValue(string(line)), rt.IntValue(int64(pos))) if err != nil { fmt.Println(err) @@ -89,10 +86,8 @@ func newLineReader(prompt string, noHist bool) *lineReader { return []rune(hintText) } rl.SyntaxHighlighter = func(line []rune) string { - if highlighter == nil { - return string(line) - } - retVal, err := rt.Call1(l.MainThread(), rt.FunctionValue(highlighter), + highlighter := hshMod.Get(rt.StringValue("highlighter")) + retVal, err := rt.Call1(l.MainThread(), highlighter, rt.StringValue(string(line))) if err != nil { fmt.Println(err) diff --git a/runnermode.go b/runnermode.go index 55adfdc..fb8bcf4 100644 --- a/runnermode.go +++ b/runnermode.go @@ -21,16 +21,18 @@ A runner is passed the input and has to return a table with these values. All are not required, only the useful ones the runner needs to return. (So if there isn't an error, just omit `err`.) -- `exitCode` (number): A numerical code to indicate the exit result. -- `input` (string): The user input. This will be used to add -to the history. -- `err` (string): A string to indicate an interal error for the runner. -It can be set to a few special values for Hilbish to throw the right hooks and have a better looking message: - -`[command]: not-found` will throw a command.not-found hook based on what `[command]` is. - -`[command]: not-executable` will throw a command.not-executable hook. -- `continue` (boolean): Whether to prompt the user for more input. +- `exitCode` (number): Exit code of the command +- `input` (string): The text input of the user. This is used by Hilbish to append extra input, in case +more is requested. +- `err` (string): A string that represents an error from the runner. +This should only be set when, for example, there is a syntax error. +It can be set to a few special values for Hilbish to throw the right +hooks and have a better looking message. + - `: not-found` will throw a `command.not-found` hook + based on what `` is. + - `: not-executable` will throw a `command.not-executable` hook. +- `continue` (boolean): Whether Hilbish should prompt the user for no input +- `newline` (boolean): Whether a newline should be added at the end of `input`. Here is a simple example of a fennel runner. It falls back to shell script if fennel eval has an error. @@ -85,7 +87,7 @@ func shRunner(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return nil, err } - _, exitCode, cont, err := execSh(aliases.Resolve(cmd)) + _, exitCode, cont, newline, err := execSh(aliases.Resolve(cmd)) var luaErr rt.Value = rt.NilValue if err != nil { luaErr = rt.StringValue(err.Error()) @@ -94,6 +96,7 @@ func shRunner(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { runnerRet.Set(rt.StringValue("input"), rt.StringValue(cmd)) runnerRet.Set(rt.StringValue("exitCode"), rt.IntValue(int64(exitCode))) runnerRet.Set(rt.StringValue("continue"), rt.BoolValue(cont)) + runnerRet.Set(rt.StringValue("newline"), rt.BoolValue(newline)) runnerRet.Set(rt.StringValue("err"), luaErr) return c.PushingNext(t.Runtime, rt.TableValue(runnerRet)), nil diff --git a/signal_unix.go b/signal_unix.go index 2e6c885..1564d93 100644 --- a/signal_unix.go +++ b/signal_unix.go @@ -1,4 +1,4 @@ -// +build darwin linux +//go:build unix package main diff --git a/signal_windows.go b/signal_windows.go index 42a9fff..2ed3370 100644 --- a/signal_windows.go +++ b/signal_windows.go @@ -1,4 +1,4 @@ -// +build windows +//go:build windows package main diff --git a/vars.go b/vars.go index 9093a88..55d71a7 100644 --- a/vars.go +++ b/vars.go @@ -11,8 +11,8 @@ var ( // Version info var ( - ver = "v2.2.1" - releaseName = "Poppy" + ver = "v2.3.3" + releaseName = "Alyssum" gitCommit string gitBranch string diff --git a/vars_darwin.go b/vars_darwin.go index 8ec83ba..43215d5 100644 --- a/vars_darwin.go +++ b/vars_darwin.go @@ -1,4 +1,4 @@ -// +build darwin +//go:build darwin package main diff --git a/vars_linux.go b/vars_unix.go similarity index 96% rename from vars_linux.go rename to vars_unix.go index e1160ba..f90fa55 100644 --- a/vars_linux.go +++ b/vars_unix.go @@ -1,4 +1,4 @@ -// +build linux +//go:build unix && !darwin package main diff --git a/vars_windows.go b/vars_windows.go index d1bd7b6..f724fc2 100644 --- a/vars_windows.go +++ b/vars_windows.go @@ -1,4 +1,4 @@ -// +build windows +//go:build windows package main diff --git a/website/content/blog/v2.3-release.md b/website/content/blog/v2.3-release.md new file mode 100644 index 0000000..3185148 --- /dev/null +++ b/website/content/blog/v2.3-release.md @@ -0,0 +1,48 @@ +--- +title: "v2.3 Release" +date: 2024-07-20T10:05:17-04:00 +draft: false +--- + + +> The release with full changelogs and prebuilt binaries can be +seen at the [v2.3.0](https://github.com/Rosettea/Hilbish/releases/tag/v2.3.0) +tag. + +Hilbish v2.3 has now been released! This is small feature and bug fix release +which took a while to cme ut since I took a long break from programming in general. +The next release will be great, so stay tuned for that. + +# Features +## Pipes (via Lua) +Commands can now be piped to each other via the Lua API with the `hilbish.run` +function and an `fs.pipe`. + +Here is a minimal example of the new usage which allows users to now pipe commands +directly via Lua functions: + +```lua +local fs = require 'fs' +local pr, pw = fs.pipe() +hilbish.run('ls -l', { + stdout = pw, + stderr = pw, +}) + +pw:close() + +hilbish.run('wc -l', { + stdin = pr +}) +``` + +This also means it's easier to make commands output to any stream output, +including in commanders. + +# Bug Fixes +- Commanders can now be cancelled with Ctrl-C, which means if they froze for some reason +they can now be exited. +- The shell script interpreter now keeps its environment, and this also fixes the +current working directory being wrong with some commands. +- Some greenhouse bugs have been fixed, like randomly appearing when resizing the terminal +and some text attributes like color appearing where they weren't supposed to.