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..2171249 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -9,10 +9,19 @@ jobs:
gen:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-go@v2
- - name: Run docgen
+ - uses: actions/checkout@v4
+ with:
+ submodules: true
+ fetch-depth: 0
+ - uses: actions/setup-go@v5
+ - name: Download Task
+ run: 'sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d'
+ - name: Build
+ run: ./bin/task
+ - name: Run docgen (go-written)
run: go run cmd/docgen/docgen.go
+ - name: Run docgen (lua-written)
+ run: ./hilbish cmd/docgen/docgen.lua
- name: Commit new docs
uses: stefanzweifel/git-auto-commit-action@v4
with:
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 021edf7..c07d540 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,39 @@
## Unreleased
### Added
+- Forward/Right arrow key will fill in hint text (#327)
+
+## [2.3.4] - 2024-12-28
+### Fixed
+- Skip over file and prevent panic if info cannot be retrieved during file completion (due to permission error or anything else)
+- Apply environment variables properly after 2.3 shell interpreter changes
+- hilbish.sink.readAll() function now reads data that doesn't end in a newline
+
+## [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.
@@ -28,6 +61,19 @@ hilbish.run('wc -l', {
})
```
+### 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.
@@ -744,6 +790,11 @@ This input for example will prompt for more input to complete:
First "stable" release of Hilbish.
+[2.3.4]: https://github.com/Rosettea/Hilbish/compare/v2.3.3...v2.3.4
+[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
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 43e361a..315884c 100644 --- a/api.go +++ b/api.go @@ -13,10 +13,9 @@ package main import ( - "bytes" + //"bytes" "errors" "fmt" - "io" "os" "os/exec" "runtime" @@ -28,9 +27,9 @@ import ( rt "github.com/arnodel/golua/runtime" "github.com/arnodel/golua/lib/packagelib" - "github.com/arnodel/golua/lib/iolib" + //"github.com/arnodel/golua/lib/iolib" "github.com/maxlandon/readline" - "mvdan.cc/sh/v3/interp" + //"mvdan.cc/sh/v3/interp" ) var exports = map[string]util.LuaExport{ @@ -39,7 +38,6 @@ var exports = map[string]util.LuaExport{ "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}, @@ -49,7 +47,6 @@ var exports = map[string]util.LuaExport{ "inputMode": {hlinputMode, 1, false}, "interval": {hlinterval, 2, false}, "read": {hlread, 1, false}, - "run": {hlrun, 1, true}, "timeout": {hltimeout, 2, false}, "which": {hlwhich, 1, false}, } @@ -134,6 +131,9 @@ func hilbishLoad(rtm *rt.Runtime) (rt.Value, func()) { pluginModule := moduleLoader(rtm) mod.Set(rt.StringValue("module"), rt.TableValue(pluginModule)) + sinkModule := util.SinkLoader(l) + mod.Set(rt.StringValue("sink"), rt.TableValue(sinkModule)) + return rt.TableValue(mod), nil } @@ -154,6 +154,7 @@ func unsetVimMode() { util.SetField(l, hshMod, "vimMode", rt.NilValue) } +/* func handleStream(v rt.Value, strms *streams, errStream bool) error { ud, ok := v.TryUserData() if !ok { @@ -182,112 +183,7 @@ func handleStream(v rt.Value, strms *streams, errStream bool) error { 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 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 - } - cmd, err := c.StringArg(0) - if err != nil { - return nil, err - } - - strms := &streams{} - var terminalOut bool - if len(c.Etc()) != 0 { - tout := c.Etc()[0] - - var ok bool - terminalOut, ok = tout.TryBool() - if !ok { - 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), - } - } - } - } - - var exitcode uint8 - stdout, stderr, err := execCommand(cmd, strms) - - if code, ok := interp.IsExitStatus(err); ok { - exitcode = code - } else if err != nil { - exitcode = 1 - } - - 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 -} // cwd() -> string // Returns the current directory of the shell. @@ -404,7 +300,7 @@ hilbish.multiprompt '-->' */ func hlmultiprompt(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { - return nil, err + return c.PushingNext1(t.Runtime, rt.StringValue(multilinePrompt)), nil } prompt, err := c.StringArg(0) if err != nil { @@ -508,7 +404,7 @@ func hlexec(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { } cmdArgs, _ := splitInput(cmd) if runtime.GOOS != "windows" { - cmdPath, err := exec.LookPath(cmdArgs[0]) + cmdPath, err := util.LookPath(cmdArgs[0]) if err != nil { fmt.Println(err) // if we get here, cmdPath will be nothing @@ -706,7 +602,7 @@ func hlwhich(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.PushingNext1(t.Runtime, rt.StringValue(cmd)), nil } - path, err := exec.LookPath(cmd) + path, err := util.LookPath(cmd) if err != nil { return c.Next(), nil } @@ -742,34 +638,6 @@ func hlinputMode(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { 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. -// Read [about runner mode](../features/runner-mode) for more information. -// #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() { - 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(line, pos) // The command line hint handler. It gets called on every key insert to // determine what text to use as an inline hint. It is passed the current diff --git a/cmd/docgen/docgen.go b/cmd/docgen/docgen.go index bf8fd1b..1521e0e 100644 --- a/cmd/docgen/docgen.go +++ b/cmd/docgen/docgen.go @@ -84,6 +84,7 @@ var prefix = map[string]string{ "commander": "c", "bait": "b", "terminal": "term", + "snail": "snail", } func getTagsAndDocs(docs string) (map[string][]tag, []string) { @@ -208,6 +209,10 @@ func setupDocType(mod string, typ *doc.Type) *docPiece { } func setupDoc(mod string, fun *doc.Func) *docPiece { + if fun.Doc == "" { + return nil + } + docs := strings.TrimSpace(fun.Doc) tags, parts := getTagsAndDocs(docs) @@ -299,10 +304,28 @@ start: func main() { fset := token.NewFileSet() os.Mkdir("docs", 0777) + os.RemoveAll("docs/api") os.Mkdir("docs/api", 0777) + + f, err := os.Create("docs/api/_index.md") + if err != nil { + panic(err) + } + f.WriteString(`--- +title: API +layout: doc +weight: -100 +menu: docs +--- + +Welcome to the API documentation for Hilbish. This documents Lua functions +provided by Hilbish. +`) + f.Close() + os.Mkdir("emmyLuaDocs", 0777) - dirs := []string{"./"} + dirs := []string{"./", "./util"} filepath.Walk("golibs/", func (path string, info os.FileInfo, err error) error { if !info.IsDir() { return nil @@ -329,7 +352,7 @@ func main() { pieces := []docPiece{} typePieces := []docPiece{} mod := l - if mod == "main" { + if mod == "main" || mod == "util" { mod = "hilbish" } var hasInterfaces bool @@ -413,14 +436,23 @@ func main() { interfaceModules[modname].Types = append(interfaceModules[modname].Types, piece) } - docs[mod] = module{ - Types: filteredTypePieces, - Docs: filteredPieces, - ShortDescription: shortDesc, - Description: strings.Join(desc, "\n"), - HasInterfaces: hasInterfaces, - Properties: docPieceTag("property", tags), - Fields: docPieceTag("field", tags), + fmt.Println(filteredTypePieces) + if newDoc, ok := docs[mod]; ok { + oldMod := docs[mod] + newDoc.Types = append(filteredTypePieces, oldMod.Types...) + newDoc.Docs = append(filteredPieces, oldMod.Docs...) + + docs[mod] = newDoc + } else { + docs[mod] = module{ + Types: filteredTypePieces, + Docs: filteredPieces, + ShortDescription: shortDesc, + Description: strings.Join(desc, "\n"), + HasInterfaces: hasInterfaces, + Properties: docPieceTag("property", tags), + Fields: docPieceTag("field", tags), + } } } diff --git a/cmd/docgen/docgen.lua b/cmd/docgen/docgen.lua index 207357a..073456b 100644 --- a/cmd/docgen/docgen.lua +++ b/cmd/docgen/docgen.lua @@ -1,7 +1,9 @@ local fs = require 'fs' local emmyPattern = '^%-%-%- (.+)' -local modpattern = '^%-+ @module (%w+)' +local emmyPattern2 = '^%-%- (.+)' +local modpattern = '^%-+ @module (.+)' local pieces = {} +local descriptions = {} local files = fs.readdir 'nature' for _, fname in ipairs(files) do @@ -13,18 +15,25 @@ for _, fname in ipairs(files) do local mod = header:match(modpattern) if not mod then goto continue end - print(fname, mod) pieces[mod] = {} + descriptions[mod] = {} local docPiece = {} local lines = {} local lineno = 0 + local doingDescription = true + for line in f:lines() do lineno = lineno + 1 lines[lineno] = line if line == header then goto continue2 end - if not line:match(emmyPattern) then + if line:match(emmyPattern) or line:match(emmyPattern2) then + if doingDescription then + table.insert(descriptions[mod], line:match(emmyPattern) or line:match(emmyPattern2)) + end + else + doingDescription = false if line:match '^function' then local pattern = (string.format('^function %s%%.', mod) .. '(%w+)') local funcName = line:match(pattern) @@ -32,10 +41,12 @@ for _, fname in ipairs(files) do local dps = { description = {}, + example = {}, params = {} } local offset = 1 + local doingExample = false while true do local prev = lines[lineno - offset] @@ -51,11 +62,23 @@ for _, fname in ipairs(files) do if emmy == 'param' then table.insert(dps.params, 1, { name = emmythings[1], - type = emmythings[2] + type = emmythings[2], + -- the +1 accounts for space. + description = table.concat(emmythings, ' '):sub(emmythings[1]:len() + 1 + emmythings[2]:len() + 1) }) end else - table.insert(dps.description, 1, docline) + if docline:match '#example' then + doingExample = not doingExample + end + + if not docline:match '#example' then + if doingExample then + table.insert(dps.example, 1, docline) + else + table.insert(dps.description, 1, docline) + end + end end offset = offset + 1 else @@ -63,7 +86,7 @@ for _, fname in ipairs(files) do end end - pieces[mod][funcName] = dps + table.insert(pieces[mod], {funcName, dps}) end docPiece = {} goto continue2 @@ -81,30 +104,83 @@ description: %s layout: doc menu: docs: - parent: "Nature" + parent: "%s" --- ]] for iface, dps in pairs(pieces) do local mod = iface:match '(%w+)%.' or 'nature' - local path = string.format('docs/%s/%s.md', mod, iface) + local docParent = 'Nature' + + path = string.format('docs/%s/%s.md', mod, iface) + if mod ~= 'nature' then + docParent = "API" + path = string.format('docs/api/%s/%s.md', mod, iface) + end + if iface == 'hilbish' then + docParent = "API" + path = string.format('docs/api/hilbish/_index.md', mod, iface) + end + fs.mkdir(fs.dir(path), true) - local f-
![]()
= io.open(path, 'w') - f:write(string.format(header, 'Module', iface, 'No description.')) - print(f) - print(mod, path) + local exists = pcall(fs.stat, path) + local newOrNotNature = (exists and mod ~= 'nature') or iface == 'hilbish' - for func, docs in pairs(dps) do - f:write(string.format('
\n', func)) + local f= io.open(path, newOrNotNature and 'r+' or 'w+') + local tocPos + if not newOrNotNature then + f:write(string.format(header, 'Module', iface, (descriptions[iface] and #descriptions[iface] > 0) and descriptions[iface][1] or 'No description.', docParent)) + if descriptions[iface] and #descriptions[iface] > 0 then + table.remove(descriptions[iface], 1) + f:write(string.format('\n## Introduction\n%s\n\n', table.concat(descriptions[iface], '\n'))) + f:write('## Functions\n') + f:write([[||| +|----|----| +]]) + tocPos = f:seek() + end + end + + local tocSearch = false + for line in f:lines() do + if line:match '^## Functions' then + tocSearch = true + end + if tocSearch and line == '' then + tocSearch = false + tocPos = f:seek() - 1 + end + end + + table.sort(dps, function(a, b) return a[1] < b[1] end) + for _, piece in pairs(dps) do + local func = piece[1] + local docs = piece[2] local sig = string.format('%s.%s(', iface, func) + local params = '' for idx, param in ipairs(docs.params) do - sig = sig .. ((param.name:gsub('%?$', ''))) - if idx ~= #docs.params then sig = sig .. ', ' end + sig = sig .. param.name:gsub('%?$', '') + params = params .. param.name:gsub('%?$', '') + if idx ~= #docs.params then + sig = sig .. ', ' + params = params .. ', ' + end end sig = sig .. ')' - f:write(string.format([[ + + if tocPos then + f:seek('set', tocPos) + local contents = f:read '*a' + f:seek('set', tocPos) + local tocLine = string.format('|%s|%s|\n', func, string.format('%s(%s)', func, params), docs.description[1]) + f:write(tocLine .. contents) + f:seek 'end' + end + + f:write(string.format('
\n\n', func)) + f:write(string.format([[- -%s @@ -120,7 +196,12 @@ for iface, dps in pairs(pieces) do f:write 'This function has no parameters. \n' end for _, param in ipairs(docs.params) do - f:write(string.format('`%s` **`%s`**\n', param.name:gsub('%?$', ''), param.type)) + f:write(string.format('`%s` **`%s`** \n', param.name:gsub('%?$', ''), param.type)) + f:write(string.format('%s\n\n', param.description)) + end + if #docs.example ~= 0 then + f:write '#### Example\n' + f:write(string.format('```lua\n%s\n```\n', table.concat(docs.example, '\n'))) end --[[ local params = table.filter(docs, function(t) diff --git a/complete.go b/complete.go index 1c40b20..e2f0812 100644 --- a/complete.go +++ b/complete.go @@ -98,7 +98,7 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) { if len(fileCompletions) != 0 { for _, f := range fileCompletions { fullPath, _ := filepath.Abs(util.ExpandHome(query + strings.TrimPrefix(f, filePref))) - if err := findExecutable(escapeInvertReplaer.Replace(fullPath), false, true); err != nil { + if err := util.FindExecutable(escapeInvertReplaer.Replace(fullPath), false, true); err != nil { continue } completions = append(completions, f) @@ -115,7 +115,7 @@ 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 - err := findExecutable(match, true, false) + err := util.FindExecutable(match, true, false) if err != nil { continue } @@ -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/hilbish/_index.md b/docs/api/hilbish/_index.md index 5c7a0f0..5aa7045 100644 --- a/docs/api/hilbish/_index.md +++ b/docs/api/hilbish/_index.md @@ -28,10 +28,10 @@ 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, 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.| +|runnerMode(mode)|Sets the execution/runner mode for interactive Hilbish.| +|run(cmd, streams)|Runs `cmd` in Hilbish's shell script interpreter.| ## Static module fields ||| @@ -408,72 +408,6 @@ Returns `input`, will be nil if Ctrl-D is pressed, or an error occurs. `string` **`prompt?`** Text to print before input, can be empty. -
--- --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`** - - -`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 -}) - -``` -
---hilbish.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. -Read [about runner mode](../features/runner-mode) for more information. - -#### Parameters -`string|function` **`mode`** - -
@@ -519,8 +453,7 @@ Will return the path of the binary, or a basename if it's a commander.
## Sink -A sink is a structure that has input and/or output to/from -a desination. +A sink is a structure that has input and/or output to/from a desination. ### Methods #### autoFlush(auto) @@ -542,3 +475,65 @@ Writes data to a sink. #### writeln(str) Writes data to a sink with a newline at the end. +
+++ ++hilbish.run(cmd, streams) + + + +
+ +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 +`cmd` **`string`** + + +`streams` **`table|boolean`** + + +#### 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.abbr.md b/docs/api/hilbish/hilbish.abbr.md new file mode 100644 index 0000000..8e88c19 --- /dev/null +++ b/docs/api/hilbish/hilbish.abbr.md @@ -0,0 +1,67 @@ +--- +title: Module hilbish.abbr +description: command line abbreviations +layout: doc +menu: + docs: + parent: "API" +--- + + +## Introduction +The abbr module manages Hilbish abbreviations. These are words that can be replaced +with longer command line strings when entered. +As an example, `git push` can be abbreviated to `gp`. When the user types +`gp` into the command line, after hitting space or enter, it will expand to `git push`. +Abbreviations can be used as an alternative to aliases. They are saved entirely in the history +Instead of the aliased form of the same command. + +## Functions +||| +|----|----| +|remove(abbr)|Removes the named `abbr`.| +|add(abbr, expanded|function, opts)|Adds an abbreviation. The `abbr` is the abbreviation itself,| ++hilbish.runnerMode(mode) + + + +
+ +Sets the execution/runner mode for interactive Hilbish. +**NOTE: This function is deprecated and will be removed in 3.0** +Use `hilbish.runner.setCurrent` instead. +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. +Read [about runner mode](../features/runner-mode) for more information. +#### Parameters +`mode` **`string|function`** + + +
+++ ++hilbish.abbr.add(abbr, expanded|function, opts) + + + +
+ +Adds an abbreviation. The `abbr` is the abbreviation itself, +while `expanded` is what the abbreviation should expand to. +It can be either a function or a string. If it is a function, it will expand to what +the function returns. +`opts` is a table that accepts 1 key: `anywhere`. +`opts.anywhere` defines whether the abbr expands anywhere in the command line or not, +whereas the default behavior is only at the beginning of the line +#### Parameters +`abbr` **`string`** + + +`expanded|function` **`string`** + + +`opts` **`table`** + + +
+++ diff --git a/docs/api/hilbish/hilbish.editor.md b/docs/api/hilbish/hilbish.editor.md index c70b605..6dac64b 100644 --- a/docs/api/hilbish/hilbish.editor.md +++ b/docs/api/hilbish/hilbish.editor.md @@ -14,12 +14,30 @@ directly interact with the line editor in use. ## Functions ||| |----|----| +|deleteByAmount(amount)|Deletes characters in the line by the given amount.| |getLine() -> string|Returns the current input line.| |getVimRegister(register) -> string|Returns the text that is at the register.| |insert(text)|Inserts text into the Hilbish command line.| |getChar() -> string|Reads a keystroke from the user. This is in a format of something like Ctrl-L.| |setVimRegister(register, text)|Sets the vim register at `register` to hold the passed text.| ++hilbish.abbr.remove(abbr) + + + +
+ +Removes the named `abbr`. +#### Parameters +`abbr` **`string`** + + +
++++hilbish.editor.deleteByAmount(amount) + + + +
+ +Deletes characters in the line by the given amount. + +#### Parameters +`number` **`amount`** + + +
@@ -96,6 +114,9 @@ hilbish.editor.setVimRegister(register, text) Sets the vim register at `register` to hold the passed text. #### Parameters +`string` **`register`** + + `string` **`text`** diff --git a/docs/api/hilbish/hilbish.messages.md b/docs/api/hilbish/hilbish.messages.md new file mode 100644 index 0000000..705cfa2 --- /dev/null +++ b/docs/api/hilbish/hilbish.messages.md @@ -0,0 +1,135 @@ +--- +title: Module hilbish.messages +description: simplistic message passing +layout: doc +menu: + docs: + parent: "API" +--- + + +## Introduction +The messages interface defines a way for Hilbish-integrated commands, +user config and other tasks to send notifications to alert the user.z +The `hilbish.message` type is a table with the following keys: +`title` (string): A title for the message notification. +`text` (string): The contents of the message. +`channel` (string): States the origin of the message, `hilbish.*` is reserved for Hilbish tasks. +`summary` (string): A short summary of the `text`. +`icon` (string): Unicode (preferably standard emoji) icon for the message notification +`read` (boolean): Whether the full message has been read or not. + +## Functions +||| +|----|----| +|unreadCount()|Returns the amount of unread messages.| +|send(message)|Sends a message.| +|readAll()|Marks all messages as read.| +|read(idx)|Marks a message at `idx` as read.| +|delete(idx)|Deletes the message at `idx`.| +|clear()|Deletes all messages.| +|all()|Returns all messages.| +
+++ ++hilbish.messages.all() + + + +
+ +Returns all messages. +#### Parameters +This function has no parameters. +
+++ ++hilbish.messages.clear() + + + +
+ +Deletes all messages. +#### Parameters +This function has no parameters. +
+++ ++hilbish.messages.delete(idx) + + + +
+ +Deletes the message at `idx`. +#### Parameters +`idx` **`number`** + + +
+++ ++hilbish.messages.read(idx) + + + +
+ +Marks a message at `idx` as read. +#### Parameters +`idx` **`number`** + + +
+++ ++hilbish.messages.readAll() + + + +
+ +Marks all messages as read. +#### Parameters +This function has no parameters. +
+++ ++hilbish.messages.send(message) + + + +
+ +Sends a message. +#### Parameters +`message` **`hilbish.message`** + + +
+++ diff --git a/docs/api/hilbish/hilbish.runner.md b/docs/api/hilbish/hilbish.runner.md index b5cfde4..4ba4999 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. + - `\+hilbish.messages.unreadCount() + + + +
+ +Returns the amount of unread messages. +#### Parameters +This function has no parameters. +: 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. @@ -52,29 +54,16 @@ end) ## Functions ||| |----|----| -|setMode(cb)|This is the same as the `hilbish.runnerMode` function.| |lua(cmd)|Evaluates `cmd` as Lua input. This is the same as using `dofile`| -|sh(cmd)|Runs a command in Hilbish's shell script interpreter.| - -
--+|sh()|nil| +|setMode(mode)|**NOTE: This function is deprecated and will be removed in 3.0**| +|setCurrent(name)|Sets Hilbish's runner mode by name.| +|set(name, runner)|*Sets* a runner by name. The difference between this function and| +|run(input, priv)|Runs `input` with the currently set Hilbish runner.| +|getCurrent()|Returns the current runner by name.| +|get(name)|Get a runner by name.| +|exec(cmd, runnerName)|Executes `cmd` with a runner.| +|add(name, runner)|Adds a runner to the table of available runners.|-hilbish.runner.setMode(cb) - - - -
- -This is the same as the `hilbish.runnerMode` function. -It takes a callback, which will be used to execute all interactive input. -In normal cases, neither callbacks should be overrided by the user, -as the higher level functions listed below this will handle it. - -#### Parameters -`function` **`cb`** - - -
@@ -95,20 +84,164 @@ or `load`, but is appropriated for the runner interface.
-++ +-hilbish.runner.sh(cmd) - +hilbish.runner.add(name, runner) +
-Runs a command in Hilbish's shell script interpreter. -This is the equivalent of using `source`. - +Adds a runner to the table of available runners. +If runner is a table, it must have the run function in it. #### Parameters -`string` **`cmd`** +`name` **`string`** + Name of the runner + +`runner` **`function|table`** + + +
++++hilbish.runner.exec(cmd, runnerName) + + + +
+ +Executes `cmd` with a runner. +If `runnerName` is not specified, it uses the default Hilbish runner. +#### Parameters +`cmd` **`string`** + + +`runnerName` **`string?`**
+++ ++hilbish.runner.get(name) + + + +
+ +Get a runner by name. +#### Parameters +`name` **`string`** + Name of the runner to retrieve. + +
+++ ++hilbish.runner.getCurrent() + + + +
+ +Returns the current runner by name. +#### Parameters +This function has no parameters. +
+++ ++hilbish.runner.run(input, priv) + + + +
+ +Runs `input` with the currently set Hilbish runner. +This method is how Hilbish executes commands. +`priv` is an optional boolean used to state if the input should be saved to history. +#### Parameters +`input` **`string`** + + +`priv` **`bool`** + + +
+++ ++hilbish.runner.set(name, runner) + + + +
+ +*Sets* a runner by name. The difference between this function and +add, is set will *not* check if the named runner exists. +The runner table must have the run function in it. +#### Parameters +`name` **`string`** + + +`runner` **`table`** + + +
+++ ++hilbish.runner.setCurrent(name) + + + +
+ +Sets Hilbish's runner mode by name. +#### Parameters +`name` **`string`** + + +
+++ ++hilbish.runner.setMode(mode) + + + +
+ +**NOTE: This function is deprecated and will be removed in 3.0** +Use `hilbish.runner.setCurrent` instead. +This is the same as the `hilbish.runnerMode` function. +It takes a callback, which will be used to execute all interactive input. +Or a string which names the runner mode to use. +#### Parameters +`mode` **`string|function`** + + +
+++ diff --git a/docs/api/snail.md b/docs/api/snail.md new file mode 100644 index 0000000..f183306 --- /dev/null +++ b/docs/api/snail.md @@ -0,0 +1,50 @@ +--- +title: Module snail +description: shell script interpreter library +layout: doc +menu: + docs: + parent: "API" +--- + +## Introduction + +The snail library houses Hilbish's Lua wrapper of its shell script interpreter. +It's not very useful other than running shell scripts, which can be done with other +Hilbish functions. + +## Functions +||| +|----|----| +|new() -> @Snail|Creates a new Snail instance.| + ++hilbish.runner.sh() + + + +
+ + +#### Parameters +This function has no parameters. +
+++ +## Types ++snail.new() -> Snail + + + +
+ +Creates a new Snail instance. + +#### Parameters +This function has no parameters. +
+ +## Snail +A Snail is a shell script interpreter instance. + +### Methods +#### dir(path) +Changes the directory of the snail instance. +The interpreter keeps its set directory even when the Hilbish process changes +directory, so this should be called on the `hilbish.cd` hook. + +#### run(command, streams) +Runs a shell command. Works the same as `hilbish.run`, but only accepts a table of streams. + 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/docs/getting-started.md b/docs/getting-started.md index de7607e..b5c3148 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -53,8 +53,39 @@ which follows XDG on Linux and MacOS, and is located in %APPDATA% on Windows. As the directory is usually `~/.config` on Linux, you can run this command to copy it: `cp /usr/share/hilbish/.hilbishrc.lua ~/.config/hilbish/init.lua` -Now you can get to editing it. Since it's just a Lua file, having basic -knowledge of Lua would help. All of Lua's standard libraries and functions -from Lua 5.4 are available. Hilbish has some custom and modules that are -available. To see them, you can run the `doc` command. This also works as -general documentation for other things. +Now we can get to customization! + +If we closely examine a small snippet of the default config: +```lua +-- Default Hilbish config +-- .. with some omitted code .. -- + +local function doPrompt(fail) + hilbish.prompt(lunacolors.format( + '{blue}%u {cyan}%d ' .. (fail and '{red}' or '{green}') .. '∆ ' + )) +end + +doPrompt() + +bait.catch('command.exit', function(code) + doPrompt(code ~= 0) +end) +``` + +We see a whopping **three** Hilbish libraries being used in this part of code. +First is of course, named after the shell itself, [`hilbish`](../api/hilbish). This is kind of a +"catch-all" namespace for functions that directly related to shell functionality/settings. + +And as we can see, the [hilbish.prompt](../api/hilbish/#prompt) function is used +to change our prompt. Change our prompt to what, exactly? + +The doc for the function states that the verbs `%u` and `%d`are used for username and current directory +of the shell, respectively. + +We wrap this in the [`lunacolors.format`](../lunacolors) function, to give +our prompt some nice color. + +But you might have also noticed that this is in the `doPrompt` function, which is called once, +and then used again in a [bait](../api/bait) hook. Specifically, the `command.exit` hook, +which is called after a command exits, so when it finishes running. diff --git a/docs/hooks/hilbish.md b/docs/hooks/hilbish.md index d5d8a48..038b721 100644 --- a/docs/hooks/hilbish.md +++ b/docs/hooks/hilbish.md @@ -43,5 +43,29 @@ The notification. The properties are defined in the link above.
-+ `hilbish.vimAction` -> actionName, args > Sent when the user does a "vim action," being something -like yanking or pasting text. See `doc vim-mode actions` for more info. +## hilbish.cd +Sent when the current directory of the shell is changed (via interactive means.) +If you are implementing a custom command that changes the directory of the shell, +you must throw this hook manually for correctness. + +#### Variables +`string` **`path`** +Absolute path of the directory that was changed to. + +`string` **`oldPath`** +Absolute path of the directory Hilbish *was* in. + +
+ +## hilbish.vimAction +Sent when the user does a "vim action," being something like yanking or pasting text. +See `doc vim-mode actions` for more info. + +#### Variables +`string` **`actionName`** +Absolute path of the directory that was changed to. + +`table` **`args`** +Table of args relating to the Vim action. + +
diff --git a/docs/nature/dirs.md b/docs/nature/dirs.md index 3c707e6..7f25706 100644 --- a/docs/nature/dirs.md +++ b/docs/nature/dirs.md @@ -1,42 +1,27 @@ --- title: Module dirs -description: No description. +description: internal directory management layout: doc menu: docs: parent: "Nature" --- + +## Introduction +The dirs module defines a small set of functions to store and manage +directories. + +## Functions +||| +|----|----| +|setOld(d)|Sets the old directory string.| +|recent(idx)|Get entry from recent directories list based on index.| +|push(dir)|Add `dir` to the recent directories list.| +|pop(num)|Remove the specified amount of dirs from the recent directories list.| +|peak(num)|Look at `num` amount of recent directories, starting from the latest.|
--- --dirs.setOld(d) - - - -
- -Sets the old directory string. -#### Parameters -`d` **`string`** -
--- --dirs.push() - - - -
- -Add `d` to the recent directories list. -#### Parameters -This function has no parameters. -
-+dirs.peak(num) @@ -45,12 +30,15 @@ dirs.peak(num)
Look at `num` amount of recent directories, starting from the latest. +This returns a table of recent directories, up to the `num` amount. #### Parameters -`num` **`number`** +`num` **`number`** + +
-+
-+++ ++dirs.push(dir) + + + +
+ +Add `dir` to the recent directories list. +#### Parameters +`dir` **`string`** + + +
++ +dirs.recent(idx) @@ -74,6 +80,24 @@ dirs.recent(idx) Get entry from recent directories list based on index. #### Parameters -`idx` **`number`** +`idx` **`number`** + + +
++diff --git a/docs/nature/doc.md b/docs/nature/doc.md new file mode 100644 index 0000000..f940c0d --- /dev/null +++ b/docs/nature/doc.md @@ -0,0 +1,76 @@ +--- +title: Module doc +description: command-line doc rendering +layout: doc +menu: + docs: + parent: "Nature" +--- + + +## Introduction +The doc module contains a small set of functions +used by the Greenhouse pager to render parts of the documentation pages. +This is only documented for the sake of it. It's only intended use +is by the Greenhouse pager. + +## Functions +||| +|----|----| +|renderInfoBlock(type, text)|Renders an info block. An info block is a block of text with| +|renderCodeBlock(text)|Assembles and renders a code block. This returns| +|highlight(text)|Performs basic Lua code highlighting.| ++dirs.setOld(d) + + + +
+ +Sets the old directory string. +#### Parameters +`d` **`string`** + +
+++ ++doc.highlight(text) + + + +
+ +Performs basic Lua code highlighting. +#### Parameters +`text` **`string`** + Code/text to do highlighting on. + +
+++ ++doc.renderCodeBlock(text) + + + +
+ +Assembles and renders a code block. This returns +the supplied text based on the number of command line columns, +and styles it to resemble a code block. +#### Parameters +`text` **`string`** + + +
+++ diff --git a/editor.go b/editor.go index 9c49440..2c04f25 100644 --- a/editor.go +++ b/editor.go @@ -17,6 +17,7 @@ func editorLoader(rtm *rt.Runtime) *rt.Table { "getVimRegister": {editorGetRegister, 2, false}, "getLine": {editorGetLine, 0, false}, "readChar": {editorReadChar, 0, false}, + "deleteByAmount": {editorDeleteByAmount, 1, false}, } mod := rt.NewTable() @@ -47,7 +48,7 @@ func editorInsert(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { // #interface editor // setVimRegister(register, text) // Sets the vim register at `register` to hold the passed text. -// #aram register string +// #param register string // #param text string func editorSetRegister(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { @@ -106,3 +107,22 @@ func editorReadChar(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.PushingNext1(t.Runtime, rt.StringValue(string(buf))), nil } + +// #interface editor +// deleteByAmount(amount) +// Deletes characters in the line by the given amount. +// #param amount number +func editorDeleteByAmount(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + + amount, err := c.IntArg(0) + if err != nil { + return nil, err + } + + lr.rl.DeleteByAmount(int(amount)) + + return c.Next(), nil +} diff --git a/emmyLuaDocs/hilbish.lua b/emmyLuaDocs/hilbish.lua index b80a660..a2935bb 100644 --- a/emmyLuaDocs/hilbish.lua +++ b/emmyLuaDocs/hilbish.lua @@ -7,11 +7,8 @@ local hilbish = {} --- @param cmd string function hilbish.aliases.add(alias, cmd) end ---- This is the same as the `hilbish.runnerMode` function. ---- It takes a callback, which will be used to execute all interactive input. ---- In normal cases, neither callbacks should be overrided by the user, ---- as the higher level functions listed below this will handle it. -function hilbish.runner.setMode(cb) end +--- Deletes characters in the line by the given amount. +function hilbish.editor.deleteByAmount(amount) end --- Returns the current input line. function hilbish.editor.getLine() end @@ -131,24 +128,6 @@ function hilbish.prompt(str, typ) end --- Returns `input`, will be nil if Ctrl-D is pressed, or an error occurs. function hilbish.read(prompt) end ---- 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. ---- -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 ---- 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. ---- Read [about runner mode](../features/runner-mode) for more information. -function hilbish.runnerMode(mode) end - --- Executed the `cb` function after a period of `time`. --- This creates a Timer that starts ticking immediately. function hilbish.timeout(cb, time) end @@ -168,28 +147,6 @@ function hilbish.jobs:foreground() end --- or `load`, but is appropriated for the runner interface. function hilbish.runner.lua(cmd) end ---- Sets/toggles the option of automatically flushing output. ---- A call with no argument will toggle the value. ---- @param auto boolean|nil -function hilbish:autoFlush(auto) end - ---- Flush writes all buffered input to the sink. -function hilbish:flush() end - ---- Reads a liine of input from the sink. ---- @returns string -function hilbish:read() end - ---- Reads all input from the sink. ---- @returns string -function hilbish:readAll() end - ---- Writes data to a sink. -function hilbish:write(str) end - ---- Writes data to a sink with a newline at the end. -function hilbish:writeln(str) end - --- Starts running the job. function hilbish.jobs:start() end @@ -200,10 +157,6 @@ function hilbish.jobs:stop() end --- It will throw if any error occurs. function hilbish.module.load(path) end ---- Runs a command in Hilbish's shell script interpreter. ---- This is the equivalent of using `source`. -function hilbish.runner.sh(cmd) end - --- Starts a timer. function hilbish.timers:start() end @@ -262,4 +215,26 @@ function hilbish.timers.create(type, time, callback) end --- Retrieves a timer via its ID. function hilbish.timers.get(id) end +--- Sets/toggles the option of automatically flushing output. +--- A call with no argument will toggle the value. +--- @param auto boolean|nil +function hilbish:autoFlush(auto) end + +--- Flush writes all buffered input to the sink. +function hilbish:flush() end + +--- Reads a liine of input from the sink. +--- @returns string +function hilbish:read() end + +--- Reads all input from the sink. +--- @returns string +function hilbish:readAll() end + +--- Writes data to a sink. +function hilbish:write(str) end + +--- Writes data to a sink with a newline at the end. +function hilbish:writeln(str) end + return hilbish diff --git a/emmyLuaDocs/snail.lua b/emmyLuaDocs/snail.lua new file mode 100644 index 0000000..94c84df --- /dev/null +++ b/emmyLuaDocs/snail.lua @@ -0,0 +1,16 @@ +--- @meta + +local snail = {} + +--- Changes the directory of the snail instance. +--- The interpreter keeps its set directory even when the Hilbish process changes +--- directory, so this should be called on the `hilbish.cd` hook. +function snail:dir(path) end + +--- Creates a new Snail instance. +function snail.new() end + +--- Runs a shell command. Works the same as `hilbish.run`, but only accepts a table of streams. +function snail:run(command, streams) end + +return snail diff --git a/emmyLuaDocs/util.lua b/emmyLuaDocs/util.lua new file mode 100644 index 0000000..9f8d634 --- /dev/null +++ b/emmyLuaDocs/util.lua @@ -0,0 +1,83 @@ +--- @meta + +local util = {} + +--- +function util.AbbrevHome changes the user's home directory in the path string to ~ (tilde) end + +--- +function util. end + +--- +function util.DoFile runs the contents of the file in the Lua runtime. end + +--- +function util.DoString runs the code string in the Lua runtime. end + +--- directory. +function util.ExpandHome expands ~ (tilde) in the path, changing it to the user home end + +--- +function util. end + +--- +function util.ForEach loops through a Lua table. end + +--- +function util. end + +--- a string and a closure. +function util.HandleStrCallback handles function parameters for Go functions which take end + +--- +function util. end + +--- +function util. end + +--- +function util.SetExports puts the Lua function exports in the table. end + +--- It is accessible via the __docProp metatable. It is a table of the names of the fields. +function util.SetField sets a field in a table, adding docs for it. end + +--- is one which has a metatable proxy to ensure no overrides happen to it. +--- It sets the field in the table and sets the __docProp metatable on the +--- user facing table. +function util.SetFieldProtected sets a field in a protected table. A protected table end + +--- Sets/toggles the option of automatically flushing output. +--- A call with no argument will toggle the value. +--- @param auto boolean|nil +function util:autoFlush(auto) end + +--- Flush writes all buffered input to the sink. +function util:flush() end + +--- +function util. end + +--- Reads a liine of input from the sink. +--- @returns string +function util:read() end + +--- Reads all input from the sink. +--- @returns string +function util:readAll() end + +--- Writes data to a sink. +function util:write(str) end + +--- Writes data to a sink with a newline at the end. +function util:writeln(str) end + +--- +function util. end + +--- +function util. end + +--- +function util. end + +return util diff --git a/exec.go b/exec.go index cf1b299..4ed53a0 100644 --- a/exec.go +++ b/exec.go @@ -1,207 +1,26 @@ package main import ( - "bytes" - "context" "errors" - "os/exec" "fmt" - "io" "os" - "path/filepath" - "runtime" "strings" - "syscall" - "time" - - "hilbish/util" 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" ) 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 - code int - colon bool - err error -} - -func (e execError) Error() string { - return fmt.Sprintf("%s: %s", e.cmd, e.typ) -} - -func (e execError) sprint() error { - sep := " " - if e.colon { - sep = ": " - } - - return fmt.Errorf("hilbish: %s%s%s", e.cmd, sep, e.err.Error()) -} - -func isExecError(err error) (execError, bool) { - if exErr, ok := err.(execError); ok { - return exErr, true - } - - fields := strings.Split(err.Error(), ": ") - knownTypes := []string{ - "not-found", - "not-executable", - } - - if len(fields) > 1 && contains(knownTypes, fields[1]) { - var colon bool - var e error - switch fields[1] { - case "not-found": - e = errNotFound - case "not-executable": - colon = true - e = errNotExec - } - - return execError{ - cmd: fields[0], - typ: fields[1], - colon: colon, - err: e, - }, true - } - - return execError{}, false -} +var runnerMode rt.Value = rt.NilValue func runInput(input string, priv bool) { running = true - cmdString := aliases.Resolve(input) - hooks.Emit("command.preexec", input, cmdString) - - rerun: - var exitCode uint8 - var err error - var cont bool - // save incase it changes while prompting (For some reason) - currentRunner := runnerMode - if currentRunner.Type() == rt.StringType { - switch currentRunner.AsString() { - case "hybrid": - _, _, err = handleLua(input) - if err == nil { - cmdFinish(0, input, priv) - return - } - input, exitCode, cont, err = handleSh(input) - case "hybridRev": - _, _, _, err = handleSh(input) - if err == nil { - cmdFinish(0, input, priv) - return - } - input, exitCode, err = handleLua(input) - case "lua": - input, exitCode, err = handleLua(input) - case "sh": - input, exitCode, cont, err = handleSh(input) - } - } else { - // can only be a string or function so - var runnerErr error - input, exitCode, cont, runnerErr, err = runLuaRunner(currentRunner, input) - if err != nil { - fmt.Fprintln(os.Stderr, err) - cmdFinish(124, input, priv) - return - } - // yep, we only use `err` to check for lua eval error - // our actual error should only be a runner provided error at this point - // command not found type, etc - err = runnerErr - } - - if cont { - input, err = reprompt(input) - if err == nil { - goto rerun - } else if err == io.EOF { - return - } - } - + runnerRun := hshMod.Get(rt.StringValue("runner")).AsTable().Get(rt.StringValue("run")) + _, err := rt.Call1(l.MainThread(), runnerRun, rt.StringValue(input), rt.BoolValue(priv)) if err != nil { - if exErr, ok := isExecError(err); ok { - hooks.Emit("command." + exErr.typ, exErr.cmd) - } else { - fmt.Fprintln(os.Stderr, err) - } + fmt.Fprintln(os.Stderr, err) } - cmdFinish(exitCode, input, priv) -} - -func reprompt(input string) (string, error) { - for { - in, err := continuePrompt(strings.TrimSuffix(input, "\\")) - 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) { - 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 - } - - var runner *rt.Table - var ok bool - runnerRet := term.Get(0) - if runner, ok = runnerRet.TryTable(); !ok { - fmt.Fprintln(os.Stderr, "runner did not return a table") - exitCode = 125 - input = userInput - return - } - - if code, ok := runner.Get(rt.StringValue("exitCode")).TryInt(); ok { - exitCode = uint8(code) - } - - if inp, ok := runner.Get(rt.StringValue("input")).TryString(); ok { - input = inp - } - - if errStr, ok := runner.Get(rt.StringValue("err")).TryString(); ok { - runnerErr = fmt.Errorf("%s", errStr) - } - - if c, ok := runner.Get(rt.StringValue("continue")).TryBool(); ok { - continued = c - } - return } func handleLua(input string) (string, uint8, error) { @@ -231,311 +50,13 @@ func handleLua(input string) (string, uint8, error) { return cmdString, 125, err } -func handleSh(cmdString string) (input string, exitCode uint8, cont 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) - if err != nil { - runErr = err - } - return -} - -func execSh(cmdString string) (string, uint8, bool, 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, true, err - } else { - if code, ok := interp.IsExitStatus(err); ok { - return cmdString, code, false, nil - } else { - return cmdString, 126, false, err - } - } - } - - return cmdString, 0, false, nil -} - -// Run command in sh interpreter -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() - - 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) - - 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, []string{}, "") - } - - interp.ExecHandler(execHandle(bg))(runner) - err = runner.Run(context.TODO(), stmt) - if err != nil { - return strms.stdout, strms.stderr, err - } - } - - return strms.stdout, strms.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]] != "" { - for i, arg := range args { - if strings.Contains(arg, " ") { - args[i] = fmt.Sprintf("\"%s\"", arg) - } - } - _, argstring = splitInput(strings.Join(args, " ")) - - // If alias was found, use command alias - argstring = aliases.Resolve(argstring) - var err error - args, err = shell.Fields(argstring, nil) - if err != nil { - return err - } - } - - // If command is defined in Lua then run it - luacmdArgs := rt.NewTable() - for i, str := range args[1:] { - luacmdArgs.Set(rt.IntValue(int64(i + 1)), rt.StringValue(str)) - } - - hc := interp.HandlerCtx(ctx) - 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(cmd), rt.TableValue(luacmdArgs), rt.TableValue(sinks)) - if err != nil { - fmt.Fprintln(os.Stderr, "Error in command:\n" + err.Error()) - return interp.NewExitStatus(1) - } - - var exitcode uint8 - - if code, ok := luaexitcode.TryInt(); ok { - exitcode = uint8(code) - } else if luaexitcode != rt.NilValue { - // deregister commander - 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]) - if err == errNotExec { - return execError{ - typ: "not-executable", - cmd: args[0], - code: 126, - colon: true, - err: errNotExec, - } - } else if err != nil { - return execError{ - typ: "not-found", - cmd: args[0], - code: 127, - err: errNotFound, - } - } - - 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 !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, - } - - var j *job - if bg { - j = jobs.getLatest() - j.setHandle(&cmd) - err = j.start() - } else { - err = cmd.Start() - } - - if err == nil { - 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() - } - - exit := handleExecErr(err) - - if bg { - j.exitCode = int(exit) - j.finish() - } - return interp.NewExitStatus(exit) - } -} - -func handleExecErr(err error) (exit uint8) { - ctx := context.TODO() - - 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 - } - exit = uint8(128 + status.Signal()) - return - } - exit = uint8(status.ExitStatus()) - return - } - exit = 1 - return - case *exec.Error: - // did not start - //fmt.Fprintf(hc.Stderr, "%v\n", err) - exit = 127 - default: return - } - - return -} -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) { - return 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 - } else if err == nil { - return nil - } - } - - return os.ErrNotExist -} - func splitInput(input string) ([]string, string) { // end my suffering // TODO: refactor this garbage quoted := false - startlastcmd := false - lastcmddone := false cmdArgs := []string{} sb := &strings.Builder{} cmdstr := &strings.Builder{} - lastcmd := "" //readline.GetHistory(readline.HistorySize() - 1) for _, r := range input { if r == '"' { @@ -551,22 +72,6 @@ func splitInput(input string) ([]string, string) { // if not quoted and there's a space then add to cmdargs cmdArgs = append(cmdArgs, sb.String()) sb.Reset() - } else if !quoted && r == '^' && startlastcmd && !lastcmddone { - // if ^ is found, isnt in quotes and is - // the second occurence of the character and is - // the first time "^^" has been used - cmdstr.WriteString(lastcmd) - sb.WriteString(lastcmd) - - startlastcmd = !startlastcmd - lastcmddone = !lastcmddone - - continue - } else if !quoted && r == '^' && !lastcmddone { - // if ^ is found, isnt in quotes and is the - // first time of starting "^^" - startlastcmd = !startlastcmd - continue } else { sb.WriteRune(r) } @@ -578,11 +83,3 @@ func splitInput(input string) ([]string, string) { return cmdArgs, cmdstr.String() } - -func cmdFinish(code uint8, cmdstr string, private bool) { - util.SetField(l, hshMod, "exitCode", rt.IntValue(int64(code))) - // 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.Emit("command.exit", rt.IntValue(int64(code)), cmdstr, private) -} diff --git a/go.mod b/go.mod index a7975b7..cc88c8e 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module hilbish -go 1.18 +go 1.21 + +toolchain go1.22.2 require ( github.com/arnodel/golua v0.0.0-20230215163904-e0b5347eaaa1 @@ -9,8 +11,8 @@ require ( github.com/maxlandon/readline v1.0.14 github.com/pborman/getopt v1.1.0 github.com/sahilm/fuzzy v0.1.1 - golang.org/x/sys v0.19.0 - golang.org/x/term v0.19.0 + golang.org/x/sys v0.22.0 + golang.org/x/term v0.22.0 mvdan.cc/sh/v3 v3.8.0 ) @@ -19,16 +21,17 @@ require ( github.com/arnodel/strftime v0.1.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.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-20240427174124-d239074c1749 +replace github.com/arnodel/golua => github.com/Rosettea/golua v0.0.0-20241104031959-5551ea280f23 diff --git a/go.sum b/go.sum index 193f17e..b4b7a91 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ -github.com/Rosettea/golua v0.0.0-20240427174124-d239074c1749 h1:jIFnWBTsYw8s7RX7H2AOXjDVhWP3ol7OzUVaPN2KnGI= -github.com/Rosettea/golua v0.0.0-20240427174124-d239074c1749/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/strftime v0.1.6 h1:0hc0pUvk8KhEMXE+htyaOUV42zNcf/csIbjzEFCJqsw= @@ -10,45 +10,37 @@ github.com/atsushinee/go-markdown-generator v0.0.0-20191121114853-83f9e1f68504 h github.com/atsushinee/go-markdown-generator v0.0.0-20191121114853-83f9e1f68504/go.mod h1:kHBCvAXJIatTX1pw6tLiOspjGc3MhUDRlog9yrCUS+k= 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.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/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/google/renameio v1.0.1/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk= -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/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/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.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -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/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.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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-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.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20210916214954-140adaaadfaf/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= -golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +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= -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/snail/lua.go b/golibs/snail/lua.go new file mode 100644 index 0000000..5850f37 --- /dev/null +++ b/golibs/snail/lua.go @@ -0,0 +1,221 @@ +// shell script interpreter library +/* +The snail library houses Hilbish's Lua wrapper of its shell script interpreter. +It's not very useful other than running shell scripts, which can be done with other +Hilbish functions. +*/ +package snail + +import ( + "errors" + "fmt" + "io" + "strings" + + "hilbish/util" + + rt "github.com/arnodel/golua/runtime" + "github.com/arnodel/golua/lib/packagelib" + "github.com/arnodel/golua/lib/iolib" + "mvdan.cc/sh/v3/interp" + "mvdan.cc/sh/v3/syntax" +) + +var snailMetaKey = rt.StringValue("hshsnail") +var Loader = packagelib.Loader{ + Load: loaderFunc, + Name: "snail", +} + +func loaderFunc(rtm *rt.Runtime) (rt.Value, func()) { + snailMeta := rt.NewTable() + snailMethods := rt.NewTable() + snailFuncs := map[string]util.LuaExport{ + "run": {snailrun, 3, false}, + "dir": {snaildir, 2, false}, + } + util.SetExports(rtm, snailMethods, snailFuncs) + + snailIndex := func(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + arg := c.Arg(1) + val := snailMethods.Get(arg) + + return c.PushingNext1(t.Runtime, val), nil + } + snailMeta.Set(rt.StringValue("__index"), rt.FunctionValue(rt.NewGoFunction(snailIndex, "__index", 2, false))) + rtm.SetRegistry(snailMetaKey, rt.TableValue(snailMeta)) + + exports := map[string]util.LuaExport{ + "new": util.LuaExport{snailnew, 0, false}, + } + + mod := rt.NewTable() + util.SetExports(rtm, mod, exports) + + return rt.TableValue(mod), nil +} + +// new() -> @Snail +// Creates a new Snail instance. +func snailnew(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + s := New(t.Runtime) + return c.PushingNext1(t.Runtime, rt.UserDataValue(snailUserData(s))), nil +} + +// #member +// run(command, streams) +// Runs a shell command. Works the same as `hilbish.run`, but only accepts a table of streams. +// #param command string +// #param streams table +func snailrun(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.CheckNArgs(2); err != nil { + return nil, err + } + + s, err := snailArg(c, 0) + if err != nil { + return nil, err + } + + cmd, err := c.StringArg(1) + if err != nil { + return nil, err + } + + streams := &util.Streams{} + thirdArg := c.Arg(2) + switch thirdArg.Type() { + case rt.TableType: + args := thirdArg.AsTable() + + if luastreams, ok := args.Get(rt.StringValue("sinks")).TryTable(); ok { + handleStream(luastreams.Get(rt.StringValue("out")), streams, false, false) + handleStream(luastreams.Get(rt.StringValue("err")), streams, true, false) + handleStream(luastreams.Get(rt.StringValue("input")), streams, false, true) + } + case rt.NilType: // noop + default: + return nil, errors.New("expected 3rd arg to be a table") + } + + var newline bool + var cont bool + var luaErr rt.Value = rt.NilValue + exitCode := 0 + bg, _, _, err := s.Run(cmd, streams) + if err != nil { + if syntax.IsIncomplete(err) { + /* + if !interactive { + return cmdString, 126, false, false, err + } + */ + if strings.Contains(err.Error(), "unclosed here-document") { + newline = true + } + cont = true + } else { + if code, ok := interp.IsExitStatus(err); ok { + exitCode = int(code) + } else { + if exErr, ok := util.IsExecError(err); ok { + exitCode = exErr.Code + } + luaErr = rt.StringValue(err.Error()) + } + } + } + runnerRet := rt.NewTable() + 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) + + runnerRet.Set(rt.StringValue("bg"), rt.BoolValue(bg)) + return c.PushingNext1(t.Runtime, rt.TableValue(runnerRet)), nil +} + +// #member +// dir(path) +// Changes the directory of the snail instance. +// The interpreter keeps its set directory even when the Hilbish process changes +// directory, so this should be called on the `hilbish.cd` hook. +// #param path string Has to be an absolute path. +func snaildir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.CheckNArgs(2); err != nil { + return nil, err + } + + s, err := snailArg(c, 0) + if err != nil { + return nil, err + } + + dir, err := c.StringArg(1) + if err != nil { + return nil, err + } + + interp.Dir(dir)(s.runner) + return c.Next(), nil +} + +func handleStream(v rt.Value, strms *util.Streams, errStream, inStream bool) error { + if v == rt.NilValue { + return nil + } + + ud, ok := v.TryUserData() + if !ok { + return errors.New("expected metatable argument") + } + + val := ud.Value() + var varstrm io.ReadWriter + if f, ok := val.(*iolib.File); ok { + varstrm = f.Handle() + } + + if f, ok := val.(*util.Sink); ok { + varstrm = f.Rw + } + + if varstrm == nil { + return errors.New("expected either a sink or file") + } + + if errStream { + strms.Stderr = varstrm + } else if inStream { + strms.Stdin = varstrm + } else { + strms.Stdout = varstrm + } + + return nil +} + +func snailArg(c *rt.GoCont, arg int) (*Snail, error) { + s, ok := valueToSnail(c.Arg(arg)) + if !ok { + return nil, fmt.Errorf("#%d must be a snail", arg + 1) + } + + return s, nil +} + +func valueToSnail(val rt.Value) (*Snail, bool) { + u, ok := val.TryUserData() + if !ok { + return nil, false + } + + s, ok := u.Value().(*Snail) + return s, ok +} + +func snailUserData(s *Snail) *rt.UserData { + snailMeta := s.runtime.Registry(snailMetaKey) + return rt.NewUserData(s, snailMeta.AsTable()) +} diff --git a/golibs/snail/snail.go b/golibs/snail/snail.go new file mode 100644 index 0000000..3ca1d12 --- /dev/null +++ b/golibs/snail/snail.go @@ -0,0 +1,302 @@ +package snail + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "os/exec" + "os/signal" + "runtime" + "strings" + "time" + + "hilbish/util" + + 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" +) + +// #type +// A Snail is a shell script interpreter instance. +type Snail struct{ + runner *interp.Runner + runtime *rt.Runtime +} + +func New(rtm *rt.Runtime) *Snail { + runner, _ := interp.New() + + return &Snail{ + runner: runner, + runtime: rtm, + } +} + +func (s *Snail) Run(cmd string, strms *util.Streams) (bool, io.Writer, io.Writer, error){ + file, err := syntax.NewParser().Parse(strings.NewReader(cmd), "") + if err != nil { + return false, nil, nil, err + } + + if strms == nil { + strms = &util.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)(s.runner) + interp.Env(nil)(s.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, []string{}, "") + } + + interp.ExecHandler(func(ctx context.Context, args []string) error { + _, argstring := splitInput(strings.Join(args, " ")) + // i dont really like this but it works + aliases := make(map[string]string) + aliasesLua, _ := util.DoString(s.runtime, "return hilbish.aliases.list()") + util.ForEach(aliasesLua.AsTable(), func(k, v rt.Value) { + aliases[k.AsString()] = v.AsString() + }) + if aliases[args[0]] != "" { + for i, arg := range args { + if strings.Contains(arg, " ") { + args[i] = fmt.Sprintf("\"%s\"", arg) + } + } + _, argstring = splitInput(strings.Join(args, " ")) + + // If alias was found, use command alias + argstring = util.MustDoString(s.runtime, fmt.Sprintf(`return hilbish.aliases.resolve("%s")`, argstring)).AsString() + + var err error + args, err = shell.Fields(argstring, nil) + if err != nil { + return err + } + } + + // If command is defined in Lua then run it + luacmdArgs := rt.NewTable() + for i, str := range args[1:] { + luacmdArgs.Set(rt.IntValue(int64(i + 1)), rt.StringValue(str)) + } + + hc := interp.HandlerCtx(ctx) + + cmds := make(map[string]*rt.Closure) + luaCmds := util.MustDoString(s.runtime, "local commander = require 'commander'; return commander.registry()").AsTable() + util.ForEach(luaCmds, func(k, v rt.Value) { + cmds[k.AsString()] = v.AsTable().Get(rt.StringValue("exec")).AsClosure() + }) + if cmd := cmds[args[0]]; cmd != nil { + stdin := util.NewSinkInput(s.runtime, hc.Stdin) + stdout := util.NewSinkOutput(s.runtime, hc.Stdout) + stderr := util.NewSinkOutput(s.runtime, hc.Stderr) + + sinks := rt.NewTable() + sinks.Set(rt.StringValue("in"), rt.UserDataValue(stdin.UserData)) + sinks.Set(rt.StringValue("input"), rt.UserDataValue(stdin.UserData)) + sinks.Set(rt.StringValue("out"), rt.UserDataValue(stdout.UserData)) + sinks.Set(rt.StringValue("err"), rt.UserDataValue(stderr.UserData)) + + t := rt.NewThread(s.runtime) + 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) + } + + var exitcode uint8 + + if code, ok := luaexitcode.TryInt(); ok { + exitcode = uint8(code) + } else if luaexitcode != rt.NilValue { + // deregister commander + delete(cmds, 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) + } + + path, err := util.LookPath(args[0]) + if err == util.ErrNotExec { + return util.ExecError{ + Typ: "not-executable", + Cmd: args[0], + Code: 126, + Colon: true, + Err: util.ErrNotExec, + } + } else if err != nil { + return util.ExecError{ + Typ: "not-found", + Cmd: args[0], + Code: 127, + Err: util.ErrNotFound, + } + } + + killTimeout := 2 * time.Second + // from here is basically copy-paste of the default exec handler from + // sh/interp but with our job handling + + env := hc.Env + envList := os.Environ() + env.Each(func(name string, vr expand.Variable) bool { + 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, + } + + //var j *job + if bg { + /* + j = jobs.getLatest() + j.setHandle(&cmd) + err = j.start() + */ + } else { + err = cmd.Start() + } + + if err == nil { + 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() + } + + exit := util.HandleExecErr(err) + + if bg { + //j.exitCode = int(exit) + //j.finish() + } + return interp.NewExitStatus(exit) + })(s.runner) + err = s.runner.Run(context.TODO(), stmt) + if err != nil { + return bg, strms.Stdout, strms.Stderr, err + } + } + + return bg, strms.Stdout, strms.Stderr, nil +} + +func splitInput(input string) ([]string, string) { + // end my suffering + // TODO: refactor this garbage + quoted := false + cmdArgs := []string{} + sb := &strings.Builder{} + cmdstr := &strings.Builder{} + + for _, r := range input { + if r == '"' { + // start quoted input + // this determines if other runes are replaced + quoted = !quoted + // dont add back quotes + //sb.WriteRune(r) + } else if !quoted && r == '~' { + // if not in quotes and ~ is found then make it $HOME + sb.WriteString(os.Getenv("HOME")) + } else if !quoted && r == ' ' { + // if not quoted and there's a space then add to cmdargs + cmdArgs = append(cmdArgs, sb.String()) + sb.Reset() + } else { + sb.WriteRune(r) + } + cmdstr.WriteRune(r) + } + if sb.Len() > 0 { + cmdArgs = append(cmdArgs, sb.String()) + } + + return cmdArgs, cmdstr.String() +} 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.go b/job.go index f5bd6f2..fcb1c2c 100644 --- a/job.go +++ b/job.go @@ -56,8 +56,8 @@ func (j *job) start() error { } j.setHandle(&cmd) } - // bgProcAttr is defined in execfile_+doc.renderInfoBlock(type, text) + + + +
+ +Renders an info block. An info block is a block of text with +an icon and styled text block. +#### Parameters +`type` **`string`** + Type of info block. The only one specially styled is the `warning`. + +`text` **`string`** + + +.go, it holds a procattr struct - // in a simple explanation, it makes signals from hilbish (sigint) + // bgProcAttr is defined in job_ .go, it holds a procattr struct + // in a simple explanation, it makes signals from hilbish (like sigint) // not go to it (child process) j.handle.SysProcAttr = bgProcAttr // reset output buffers @@ -136,7 +136,7 @@ func luaStartJob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if !j.running { err := j.start() - exit := handleExecErr(err) + exit := util.HandleExecErr(err) j.exitCode = int(exit) j.finish() } diff --git a/job_unix.go b/job_unix.go index 5029012..2caa4ae 100644 --- a/job_unix.go +++ b/job_unix.go @@ -1,4 +1,4 @@ -// +build darwin linux +//go:build unix package main @@ -10,6 +10,10 @@ import ( "golang.org/x/sys/unix" ) +var bgProcAttr *syscall.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, +} + func (j *job) foreground() error { if jobs.foreground { return errors.New("(another) job already foregrounded") diff --git a/job_windows.go b/job_windows.go index 140a5d1..1ac4646 100644 --- a/job_windows.go +++ b/job_windows.go @@ -1,11 +1,16 @@ -// +build windows +//go:build windows package main import ( "errors" + "syscall" ) +var bgProcAttr *syscall.SysProcAttr = &syscall.SysProcAttr{ + CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP, +} + func (j *job) foreground() error { return errors.New("not supported on windows") } diff --git a/lua.go b/lua.go index 94b7910..859a39d 100644 --- a/lua.go +++ b/lua.go @@ -3,11 +3,13 @@ package main import ( "fmt" "os" + "path/filepath" "hilbish/util" "hilbish/golibs/bait" "hilbish/golibs/commander" "hilbish/golibs/fs" + "hilbish/golibs/snail" "hilbish/golibs/terminal" rt "github.com/arnodel/golua/runtime" @@ -23,15 +25,14 @@ func luaInit() { MessageHandler: debuglib.Traceback, }) lib.LoadAll(l) - setupSinkType(l) lib.LoadLibs(l, hilbishLoader) // yes this is stupid, i know util.DoString(l, "hilbish = require 'hilbish'") - // Add fs and terminal module module to Lua lib.LoadLibs(l, fs.Loader) lib.LoadLibs(l, terminal.Loader) + lib.LoadLibs(l, snail.Loader) cmds = commander.New(l) lib.LoadLibs(l, cmds.Loader) @@ -63,7 +64,7 @@ func luaInit() { err1 := util.DoFile(l, "nature/init.lua") if err1 != nil { - err2 := util.DoFile(l, preloadPath) + err2 := util.DoFile(l, filepath.Join(dataDir, "nature", "init.lua")) if err2 != nil { fmt.Fprintln(os.Stderr, "Missing nature module, some functionality and builtins will be missing.") fmt.Fprintln(os.Stderr, "local error:", err1) diff --git a/main.go b/main.go index 4bdfdac..77b1847 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "os" + "os/exec" "os/user" "path/filepath" "runtime" @@ -39,11 +40,24 @@ var ( ) func main() { + if runtime.GOOS == "linux" { + // dataDir should only be empty on linux to allow XDG_DATA_DIRS searching. + // but since it might be set on some distros (nixos) we should still check if its really is empty. + if dataDir == "" { + searchableDirs := getenv("XDG_DATA_DIRS", "/usr/local/share/:/usr/share/") + dataDir = "." + for _, path := range strings.Split(searchableDirs, ":") { + _, err := os.Stat(filepath.Join(path, "hilbish", ".hilbishrc.lua")) + if err == nil { + dataDir = filepath.Join(path, "hilbish") + break + } + } + } + } + curuser, _ = user.Current() - homedir := curuser.HomeDir confDir, _ = os.UserConfigDir() - preloadPath = strings.Replace(preloadPath, "~", homedir, 1) - sampleConfPath = strings.Replace(sampleConfPath, "~", homedir, 1) // i honestly dont know what directories to use for this switch runtime.GOOS { @@ -115,7 +129,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) @@ -131,10 +151,11 @@ func main() { confpath := ".hilbishrc.lua" if err != nil { // If it wasnt found, go to the real sample conf - _, err = os.ReadFile(sampleConfPath) - confpath = sampleConfPath + sampleConfigPath := filepath.Join(dataDir, ".hilbishrc.lua") + _, err = os.ReadFile(sampleConfigPath) + confpath = sampleConfigPath if err != nil { - fmt.Println("could not find .hilbishrc.lua or", sampleConfPath) + fmt.Println("could not find .hilbishrc.lua or", sampleConfigPath) return } } @@ -213,8 +234,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)) @@ -238,16 +260,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) @@ -294,15 +324,6 @@ func removeDupes(slice []string) []string { return newSlice } -func contains(s []string, e string) bool { - for _, a := range s { - if strings.ToLower(a) == strings.ToLower(e) { - return true - } - } - return false -} - func exit(code int) { jobs.stopAll() diff --git a/nature/abbr.lua b/nature/abbr.lua new file mode 100644 index 0000000..cbe89ff --- /dev/null +++ b/nature/abbr.lua @@ -0,0 +1,61 @@ +-- @module hilbish.abbr +-- command line abbreviations +-- The abbr module manages Hilbish abbreviations. These are words that can be replaced +-- with longer command line strings when entered. +-- As an example, `git push` can be abbreviated to `gp`. When the user types +-- `gp` into the command line, after hitting space or enter, it will expand to `git push`. +-- Abbreviations can be used as an alternative to aliases. They are saved entirely in the history +-- Instead of the aliased form of the same command. +local bait = require 'bait' +local hilbish = require 'hilbish' +hilbish.abbr = { + all = {} +} + +--- Adds an abbreviation. The `abbr` is the abbreviation itself, +--- while `expanded` is what the abbreviation should expand to. +--- It can be either a function or a string. If it is a function, it will expand to what +--- the function returns. +--- `opts` is a table that accepts 1 key: `anywhere`. +--- `opts.anywhere` defines whether the abbr expands anywhere in the command line or not, +--- whereas the default behavior is only at the beginning of the line +-- @param abbr string +-- @param expanded|function string +-- @param opts table +function hilbish.abbr.add(abbr, expanded, opts) + print(abbr, expanded, opts) + opts = opts or {} + opts.abbr = abbr + opts.expand = expanded + hilbish.abbr.all[abbr] = opts +end + +--- Removes the named `abbr`. +-- @param abbr string +function hilbish.abbr.remove(abbr) + hilbish.abbr.all[abbr] = nil +end + +bait.catch('hilbish.rawInput', function(c) + -- 0x0d == enter + if c == ' ' or c == string.char(0x0d) then + -- check if the last "word" was a valid abbreviation + local line = hilbish.editor.getLine() + local lineSplits = string.split(line, ' ') + local thisAbbr = hilbish.abbr.all[lineSplits[#lineSplits]] + + if thisAbbr and (#lineSplits == 1 or thisAbbr.anywhere == true) then + hilbish.editor.deleteByAmount(-lineSplits[#lineSplits]:len()) + if type(thisAbbr.expand) == 'string' then + hilbish.editor.insert(thisAbbr.expand) + elseif type(thisAbbr.expand) == 'function' then + local expandRet = thisAbbr.expand() + if type(expandRet) ~= 'string' then + print(string.format('abbr %s has an expand function that did not return a string. instead it returned: %s', thisAbbr.abbr, expandRet)) + return + end + hilbish.editor.insert(expandRet) + end + end + end +end) diff --git a/nature/commands/cd.lua b/nature/commands/cd.lua index 7cfe4a2..9f532ca 100644 --- a/nature/commands/cd.lua +++ b/nature/commands/cd.lua @@ -3,8 +3,9 @@ local commander = require 'commander' local fs = require 'fs' local dirs = require 'nature.dirs' -dirs.old = hilbish.cwd() commander.register('cd', function (args, sinks) + local oldPath = hilbish.cwd() + if #args > 1 then sinks.out:writeln("cd: too many arguments") return 1 @@ -16,13 +17,13 @@ commander.register('cd', function (args, sinks) sinks.out:writeln(path) end - dirs.setOld(hilbish.cwd()) - dirs.push(path) - + local absPath = fs.abs(path) local ok, err = pcall(function() fs.cd(path) end) if not ok then sinks.out:writeln(err) return 1 end - bait.throw('cd', path) + + bait.throw('cd', path, oldPath) + bait.throw('hilbish.cd', absPath, oldPath) end) 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/dirs.lua b/nature/dirs.lua index 328b4b7..db55954 100644 --- a/nature/dirs.lua +++ b/nature/dirs.lua @@ -1,10 +1,13 @@ -- @module dirs +-- internal directory management +-- The dirs module defines a small set of functions to store and manage +-- directories. +local bait = require 'bait' local fs = require 'fs' local dirs = {} ---- Last (current working) directory. Separate from recentDirs mainly for ---- easier use. +--- Last (current working) directory. Separate from recentDirs mainly for easier use. dirs.old = '' --- Table of recent directories. For use, look at public functions. dirs.recentDirs = {} @@ -35,19 +38,21 @@ function dirRecents(num, remove) end --- Look at `num` amount of recent directories, starting from the latest. +--- This returns a table of recent directories, up to the `num` amount. -- @param num? number function dirs.peak(num) return dirRecents(num) end ---- Add `d` to the recent directories list. -function dirs.push(d) +--- Add `dir` to the recent directories list. +--- @param dir string +function dirs.push(dir) dirs.recentDirs[dirs.recentSize + 1] = nil - if dirs.recentDirs[#dirs.recentDirs - 1] ~= d then - ok, d = pcall(fs.abs, d) - assert(ok, 'could not turn "' .. d .. '"into an absolute path') + if dirs.recentDirs[#dirs.recentDirs - 1] ~= dir then + local ok, dir = pcall(fs.abs, dir) + assert(ok, 'could not turn "' .. dir .. '"into an absolute path') - table.insert(dirs.recentDirs, 1, d) + table.insert(dirs.recentDirs, 1, dir) end end @@ -73,4 +78,9 @@ function dirs.setOld(d) dirs.old = d end +bait.catch('hilbish.cd', function(path, oldPath) + dirs.setOld(oldPath) + dirs.push(path) +end) + return dirs diff --git a/nature/doc.lua b/nature/doc.lua index 657af51..a21312a 100644 --- a/nature/doc.lua +++ b/nature/doc.lua @@ -1,13 +1,25 @@ +-- @module doc +-- command-line doc rendering +-- The doc module contains a small set of functions +-- used by the Greenhouse pager to render parts of the documentation pages. +-- This is only documented for the sake of it. It's only intended use +-- is by the Greenhouse pager. local lunacolors = require 'lunacolors' -local M = {} +local doc = {} -function M.highlight(text) +--- Performs basic Lua code highlighting. +--- @param text string Code/text to do highlighting on. +function doc.highlight(text) return text:gsub('\'.-\'', lunacolors.yellow) --:gsub('%-%- .-', lunacolors.black) end -function M.renderCodeBlock(text) +--- Assembles and renders a code block. This returns +--- the supplied text based on the number of command line columns, +--- and styles it to resemble a code block. +--- @param text string +function doc.renderCodeBlock(text) local longest = 0 local lines = string.split(text:gsub('\t', ' '), '\n') @@ -17,14 +29,18 @@ 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}' .. ' ' .. doc.highlight(line:sub(0, longest)) + .. string.rep(' ', longest - line:len()) .. ' ') end return '\n' .. lunacolors.format('{greyBg}' .. table.concat(lines, '\n')) .. '\n' end -function M.renderInfoBlock(type, text) +--- Renders an info block. An info block is a block of text with +--- an icon and styled text block. +--- @param type string Type of info block. The only one specially styled is the `warning`. +--- @param text string +function doc.renderInfoBlock(type, text) local longest = 0 local lines = string.split(text:gsub('\t', ' '), '\n') @@ -34,7 +50,7 @@ function M.renderInfoBlock(type, text) end for i, line in ipairs(lines) do - lines[i] = ' ' .. M.highlight(line:sub(0, longest)) + lines[i] = ' ' .. doc.highlight(line:sub(0, longest)) .. string.rep(' ', longest - line:len()) .. ' ' end @@ -44,4 +60,4 @@ function M.renderInfoBlock(type, text) end return '\n' .. heading .. '\n' .. lunacolors.format('{greyBg}' .. table.concat(lines, '\n')) .. '\n' end -return M +return doc diff --git a/nature/greenhouse/init.lua b/nature/greenhouse/init.lua index e58faf7..2badfae 100644 --- a/nature/greenhouse/init.lua +++ b/nature/greenhouse/init.lua @@ -1,4 +1,5 @@ --- Greenhouse is a simple text scrolling handler for terminal programs. +-- @module greenhouse +-- Greenhouse is a simple text scrolling handler (pager) for terminal programs. -- The idea is that it can be set a region to do its scrolling and paging -- job and then the user can draw whatever outside it. -- This reduces code duplication for the message viewer @@ -61,17 +62,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 +102,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() diff --git a/nature/greenhouse/page.lua b/nature/greenhouse/page.lua index 51d1440..185ef61 100644 --- a/nature/greenhouse/page.lua +++ b/nature/greenhouse/page.lua @@ -1,3 +1,4 @@ +-- @module greenhouse.page local Object = require 'nature.object' local Page = Object:extend() @@ -10,6 +11,7 @@ function Page:new(title, text) self.children = {} end + function Page:setText(text) self.lines = string.split(text, '\n') end diff --git a/nature/hilbish.lua b/nature/hilbish.lua new file mode 100644 index 0000000..3d852a8 --- /dev/null +++ b/nature/hilbish.lua @@ -0,0 +1,78 @@ +-- @module hilbish +local bait = require 'bait' +local snail = require 'snail' + +hilbish.snail = snail.new() +bait.catch('hilbish.cd', function(path) + hilbish.snail:dir(path) +end) +--- 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. +--- #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 +-- @param cmd string +-- @param streams table|boolean +-- @returns number, string, string +function hilbish.run(cmd, streams) + local sinks = {} + + if type(streams) == 'boolean' then + if not streams then + sinks = { + out = hilbish.sink.new(), + err = hilbish.sink.new(), + input = io.stdin + } + end + elseif type(streams) == 'table' then + sinks = streams + end + + local out = hilbish.snail:run(cmd, {sinks = sinks}) + local returns = {out.exitCode} + + if type(streams) == 'boolean' and not streams then + table.insert(returns, sinks.out:readAll()) + table.insert(returns, sinks.err:readAll()) + end + + return table.unpack(returns) +end + +--- Sets the execution/runner mode for interactive Hilbish. +--- **NOTE: This function is deprecated and will be removed in 3.0** +--- Use `hilbish.runner.setCurrent` instead. +--- 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. +--- Read [about runner mode](../features/runner-mode) for more information. +-- @param mode string|function +function hilbish.runnerMode(mode) + if type(mode) == 'string' then + hilbish.runner.setCurrent(mode) + elseif type(mode) == 'function' then + hilbish.runner.set('_', { + run = mode + }) + hilbish.runner.setCurrent '_' + else + error('expected runner mode type to be either string or function, got', type(mode)) + end +end diff --git a/nature/hummingbird.lua b/nature/hummingbird.lua index 581e92c..88cb88f 100644 --- a/nature/hummingbird.lua +++ b/nature/hummingbird.lua @@ -1,3 +1,14 @@ +-- @module hilbish.messages +-- simplistic message passing +-- The messages interface defines a way for Hilbish-integrated commands, +-- user config and other tasks to send notifications to alert the user.z +-- The `hilbish.message` type is a table with the following keys: +-- `title` (string): A title for the message notification. +-- `text` (string): The contents of the message. +-- `channel` (string): States the origin of the message, `hilbish.*` is reserved for Hilbish tasks. +-- `summary` (string): A short summary of the `text`. +-- `icon` (string): Unicode (preferably standard emoji) icon for the message notification +-- `read` (boolean): Whether the full message has been read or not. local bait = require 'bait' local commander = require 'commander' local lunacolors = require 'lunacolors' @@ -44,24 +55,30 @@ function hilbish.messages.send(message) bait.throw('hilbish.notification', message) end +--- Marks a message at `idx` as read. +--- @param idx number function hilbish.messages.read(idx) local msg = M._messages[idx] - if msg then + if msg then M._messages[idx].read = true unread = unread - 1 end end -function hilbish.messages.readAll(idx) +--- Marks all messages as read. +function hilbish.messages.readAll() for _, msg in ipairs(hilbish.messages.all()) do hilbish.messages.read(msg.index) end end +--- Returns the amount of unread messages. function hilbish.messages.unreadCount() return unread end +--- Deletes the message at `idx`. +--- @param idx number function hilbish.messages.delete(idx) local msg = M._messages[idx] if not msg then @@ -71,12 +88,14 @@ function hilbish.messages.delete(idx) M._messages[idx] = nil end +--- Deletes all messages. function hilbish.messages.clear() for _, msg in ipairs(hilbish.messages.all()) do hilbish.messages.delete(msg.index) end end +--- Returns all messages. function hilbish.messages.all() return M._messages end diff --git a/nature/init.lua b/nature/init.lua index f87b274..d8b2aa3 100644 --- a/nature/init.lua +++ b/nature/init.lua @@ -18,6 +18,8 @@ table.insert(package.searchers, function(module) return function() return hilbish.module.load(path) end, path end) +require 'nature.hilbish' + require 'nature.commands' require 'nature.completions' require 'nature.opts' @@ -25,6 +27,7 @@ require 'nature.vim' require 'nature.runner' require 'nature.hummingbird' require 'nature.env' +require 'nature.abbr' local shlvl = tonumber(os.getenv 'SHLVL') if shlvl ~= nil then 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..064ef93 100644 --- a/nature/opts/motd.lua +++ b/nature/opts/motd.lua @@ -2,8 +2,7 @@ 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} +{magenta}Hilbish{reset} blooms in the {blue}midnight.{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/nature/runner.lua b/nature/runner.lua index 235ab77..427fb7e 100644 --- a/nature/runner.lua +++ b/nature/runner.lua @@ -1,4 +1,5 @@ ---- hilbish.runner +-- @module hilbish.runner +local snail = require 'snail' local currentRunner = 'hybrid' local runners = {} @@ -6,7 +7,7 @@ local runners = {} hilbish = hilbish --- Get a runner by name. ---- @param name string +--- @param name string Name of the runner to retrieve. --- @return table function hilbish.runner.get(name) local r = runners[name] @@ -18,10 +19,10 @@ function hilbish.runner.get(name) return r end ---- Adds a runner to the table of available runners. If runner is a table, ---- it must have the run function in it. ---- @param name string ---- @param runner function | table +--- Adds a runner to the table of available runners. +--- If runner is a table, it must have the run function in it. +--- @param name string Name of the runner +--- @param runner function|table function hilbish.runner.add(name, runner) if type(name) ~= 'string' then error 'expected runner name to be a table' @@ -42,7 +43,9 @@ function hilbish.runner.add(name, runner) hilbish.runner.set(name, runner) end ---- Sets a runner by name. The runner table must have the run function in it. +--- *Sets* a runner by name. The difference between this function and +--- add, is set will *not* check if the named runner exists. +--- The runner table must have the run function in it. --- @param name string --- @param runner table function hilbish.runner.set(name, runner) @@ -53,11 +56,11 @@ function hilbish.runner.set(name, runner) runners[name] = runner end ---- Executes cmd with a runner. If runnerName isn't passed, it uses ---- the user's current runner. +--- Executes `cmd` with a runner. +--- If `runnerName` is not specified, it uses the default Hilbish runner. --- @param cmd string --- @param runnerName string? ---- @return string, number, string +--- @return table function hilbish.runner.exec(cmd, runnerName) if not runnerName then runnerName = currentRunner end @@ -66,13 +69,11 @@ function hilbish.runner.exec(cmd, runnerName) return r.run(cmd) end ---- Sets the current interactive/command line runner mode. +--- Sets Hilbish's runner mode by name. --- @param name string function hilbish.runner.setCurrent(name) - local r = hilbish.runner.get(name) + hilbish.runner.get(name) -- throws if it doesnt exist. currentRunner = name - - hilbish.runner.setMode(r.run) end --- Returns the current runner by name. @@ -81,6 +82,81 @@ function hilbish.runner.getCurrent() return currentRunner end +--- **NOTE: This function is deprecated and will be removed in 3.0** +--- Use `hilbish.runner.setCurrent` instead. +--- This is the same as the `hilbish.runnerMode` function. +--- It takes a callback, which will be used to execute all interactive input. +--- Or a string which names the runner mode to use. +-- @param mode string|function +function hilbish.runner.setMode(mode) + hilbish.runnerMode(mode) +end + +local function finishExec(exitCode, input, priv) + hilbish.exitCode = exitCode + bait.throw('command.exit', exitCode, input, priv) +end + +local function continuePrompt(prev, newline) + local multilinePrompt = hilbish.multiprompt() + -- the return of hilbish.read is nil when error or ctrl-d + local cont = hilbish.read(multilinePrompt) + if not cont then + return + end + + if newline then + cont = '\n' .. cont + end + + if cont:match '\\$' then + cont = cont:gsub('\\$', '') .. '\n' + end + + return prev .. cont +end + +--- Runs `input` with the currently set Hilbish runner. +--- This method is how Hilbish executes commands. +--- `priv` is an optional boolean used to state if the input should be saved to history. +-- @param input string +-- @param priv bool +function hilbish.runner.run(input, priv) + local command = hilbish.aliases.resolve(input) + bait.throw('command.preexec', input, command) + + ::rerun:: + local runner = hilbish.runner.get(currentRunner) + local ok, out = pcall(runner.run, input) + if not ok then + io.stderr:write(out .. '\n') + finishExec(124, out.input, priv) + return + end + + if out.continue then + local contInput = continuePrompt(input, out.newline) + if contInput then + input = contInput + goto rerun + end + end + + if out.err then + local fields = string.split(out.err, ': ') + if fields[2] == 'not-found' or fields[2] == 'not-executable' then + bait.throw('command.' .. fields[2], fields[1]) + else + io.stderr:write(out.err .. '\n') + end + end + finishExec(out.exitCode, out.input, priv) +end + +function hilbish.runner.sh(input) + return hilbish.snail:run(input) +end + hilbish.runner.add('hybrid', function(input) local cmdStr = hilbish.aliases.resolve(input) @@ -107,7 +183,5 @@ hilbish.runner.add('lua', function(input) return hilbish.runner.lua(cmdStr) end) -hilbish.runner.add('sh', function(input) - return hilbish.runner.sh(input) -end) - +hilbish.runner.add('sh', hilbish.runner.sh) +hilbish.runner.setCurrent 'hybrid' 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/completers/command-arguments.go b/readline/completers/command-arguments.go deleted file mode 100644 index 912ac7e..0000000 --- a/readline/completers/command-arguments.go +++ /dev/null @@ -1,23 +0,0 @@ -package completers - -import ( - "github.com/jessevdk/go-flags" - - "github.com/maxlandon/readline" -) - -// CompleteCommandArguments - Completes all values for arguments to a command. -// Arguments here are different from command options (--option). -// Many categories, from multiple sources in multiple contexts -func completeCommandArguments(cmd *flags.Command, arg string, lastWord string) (prefix string, completions []*readline.CompletionGroup) { - - // the prefix is the last word, by default - prefix = lastWord - - // SEE completeOptionArguments FOR A WAY TO ADD COMPLETIONS TO SPECIFIC ARGUMENTS ------------------------------ - - // found := argumentByName(cmd, arg) - // var comp *readline.CompletionGroup // This group is used as a buffer, to add groups to final completions - - return -} diff --git a/readline/completers/env.go b/readline/completers/env.go deleted file mode 100644 index ae77aab..0000000 --- a/readline/completers/env.go +++ /dev/null @@ -1,124 +0,0 @@ -package completers - -import ( - "os" - "strings" - - "github.com/maxlandon/readline" -) - -// completeEnvironmentVariables - Returns all environment variables as suggestions -func completeEnvironmentVariables(lastWord string) (last string, completions []*readline.CompletionGroup) { - - // Check if last input is made of several different variables - allVars := strings.Split(lastWord, "/") - lastVar := allVars[len(allVars)-1] - - var evaluated = map[string]string{} - - grp := &readline.CompletionGroup{ - Name: "console OS environment", - MaxLength: 5, // Should be plenty enough - DisplayType: readline.TabDisplayGrid, - TrimSlash: true, // Some variables can be paths - } - - for k, v := range clientEnv { - if strings.HasPrefix("$"+k, lastVar) { - grp.Suggestions = append(grp.Suggestions, "$"+k+"/") - evaluated[k] = v - } - } - - completions = append(completions, grp) - - return lastVar, completions -} - -// clientEnv - Contains all OS environment variables, client-side. -// This is used for things like downloading/uploading files from localhost, etc., -// therefore we need completion and parsing stuff, sometimes. -var clientEnv = map[string]string{} - -// ParseEnvironmentVariables - Parses a line of input and replace detected environment variables with their values. -func ParseEnvironmentVariables(args []string) (processed []string, err error) { - - for _, arg := range args { - - // Anywhere a $ is assigned means there is an env variable - if strings.Contains(arg, "$") || strings.Contains(arg, "~") { - - //Split in case env is embedded in path - envArgs := strings.Split(arg, "/") - - // If its not a path - if len(envArgs) == 1 { - processed = append(processed, handleCuratedVar(arg)) - } - - // If len of the env var split is > 1, its a path - if len(envArgs) > 1 { - processed = append(processed, handleEmbeddedVar(arg)) - } - } else if arg != "" && arg != " " { - // Else, if arg is not an environment variable, return it as is - processed = append(processed, arg) - } - - } - return -} - -// handleCuratedVar - Replace an environment variable alone and without any undesired characters attached -func handleCuratedVar(arg string) (value string) { - if strings.HasPrefix(arg, "$") && arg != "" && arg != "$" { - envVar := strings.TrimPrefix(arg, "$") - val, ok := clientEnv[envVar] - if !ok { - return envVar - } - return val - } - if arg != "" && arg == "~" { - return clientEnv["HOME"] - } - - return arg -} - -// handleEmbeddedVar - Replace an environment variable that is in the middle of a path, or other one-string combination -func handleEmbeddedVar(arg string) (value string) { - - envArgs := strings.Split(arg, "/") - var path []string - - for _, arg := range envArgs { - if strings.HasPrefix(arg, "$") && arg != "" && arg != "$" { - envVar := strings.TrimPrefix(arg, "$") - val, ok := clientEnv[envVar] - if !ok { - // Err will be caught when command is ran anyway, or completion will stop... - path = append(path, arg) - } - path = append(path, val) - } else if arg != "" && arg == "~" { - path = append(path, clientEnv["HOME"]) - } else if arg != " " && arg != "" { - path = append(path, arg) - } - } - - return strings.Join(path, "/") -} - -// loadClientEnv - Loads all user environment variables -func loadClientEnv() error { - env := os.Environ() - - for _, kv := range env { - key := strings.Split(kv, "=")[0] - value := strings.Split(kv, "=")[1] - clientEnv[key] = value - } - return nil -} diff --git a/readline/completers/hint-completer.go b/readline/completers/hint-completer.go deleted file mode 100644 index e838961..0000000 --- a/readline/completers/hint-completer.go +++ /dev/null @@ -1,180 +0,0 @@ -package completers - -import ( - "strings" - - "github.com/jessevdk/go-flags" - - "github.com/maxlandon/readline" -) - -// HintCompleter - Entrypoint to all hints in the Wiregost console -func (c *CommandCompleter) HintCompleter(line []rune, pos int) (hint []rune) { - - // Format and sanitize input - // @args => All items of the input line - // @last => The last word detected in input line as []rune - // @lastWord => The last word detected in input as string - args, last, lastWord := formatInput(line) - - // Detect base command automatically - var command = c.detectedCommand(args) - - // Menu hints (command line is empty, or nothing recognized) - if noCommandOrEmpty(args, last, command) { - hint = MenuHint(args, last) - } - - // Check environment variables - if envVarAsked(args, lastWord) { - return envVarHint(args, last) - } - - // Command Hint - if commandFound(command) { - - // Command hint by default (no space between cursor and last command character) - hint = CommandHint(command) - - // Check environment variables - if envVarAsked(args, lastWord) { - return envVarHint(args, last) - } - - // If options are asked for root command, return commpletions. - if len(command.Groups()) > 0 { - for _, grp := range command.Groups() { - if opt, yes := optionArgRequired(args, last, grp); yes { - hint = OptionArgumentHint(args, last, opt) - } - } - } - - // If command has args, hint for args - if arg, yes := commandArgumentRequired(lastWord, args, command); yes { - hint = []rune(CommandArgumentHints(args, last, command, arg)) - } - - // Brief subcommand hint - if lastIsSubCommand(lastWord, command) { - hint = []rune(commandHint + command.Find(string(last)).ShortDescription) - } - - // Handle subcommand if found - if sub, ok := subCommandFound(lastWord, args, command); ok { - return HandleSubcommandHints(args, last, sub) - } - - } - - // Handle system binaries, shell commands, etc... - if commandFoundInPath(args[0]) { - // hint = []rune(exeHint + util.ParseSummary(util.GetManPages(args[0]))) - } - - return -} - -// CommandHint - Yields the hint of a Wiregost command -func CommandHint(command *flags.Command) (hint []rune) { - return []rune(commandHint + command.ShortDescription) -} - -// HandleSubcommandHints - Handles hints for a subcommand and its arguments, options, etc. -func HandleSubcommandHints(args []string, last []rune, command *flags.Command) (hint []rune) { - - // If command has args, hint for args - if arg, yes := commandArgumentRequired(string(last), args, command); yes { - hint = []rune(CommandArgumentHints(args, last, command, arg)) - return - } - - // Environment variables - if envVarAsked(args, string(last)) { - hint = envVarHint(args, last) - } - - // If the last word in input is an option --name, yield argument hint if needed - if len(command.Groups()) > 0 { - for _, grp := range command.Groups() { - if opt, yes := optionArgRequired(args, last, grp); yes { - hint = OptionArgumentHint(args, last, opt) - } - } - } - - // If user asks for completions with "-" or "--". - // (Note: This takes precedence on any argument hints, as it is evaluated after them) - if commandOptionsAsked(args, string(last), command) { - return OptionHints(args, last, command) - } - - return -} - -// CommandArgumentHints - Yields hints for arguments to commands if they have some -func CommandArgumentHints(args []string, last []rune, command *flags.Command, arg string) (hint []rune) { - - found := argumentByName(command, arg) - // Base Hint is just a description of the command argument - hint = []rune(argHint + found.Description) - - return -} - -// ModuleOptionHints - If the option being set has a description, show it -func ModuleOptionHints(opt string) (hint []rune) { - return -} - -// OptionHints - Yields hints for proposed options lists/groups -func OptionHints(args []string, last []rune, command *flags.Command) (hint []rune) { - return -} - -// OptionArgumentHint - Yields hints for arguments to an option (generally the last word in input) -func OptionArgumentHint(args []string, last []rune, opt *flags.Option) (hint []rune) { - return []rune(valueHint + opt.Description) -} - -// MenuHint - Returns the Hint for a given menu context -func MenuHint(args []string, current []rune) (hint []rune) { - return -} - -// SpecialCommandHint - Shows hints for Wiregost special commands -func SpecialCommandHint(args []string, current []rune) (hint []rune) { - return current -} - -// envVarHint - Yields hints for environment variables -func envVarHint(args []string, last []rune) (hint []rune) { - // Trim last in case its a path with multiple vars - allVars := strings.Split(string(last), "/") - lastVar := allVars[len(allVars)-1] - - // Base hint - hint = []rune(envHint + lastVar) - - envVar := strings.TrimPrefix(lastVar, "$") - - if v, ok := clientEnv[envVar]; ok { - if v != "" { - hintStr := string(hint) + " => " + clientEnv[envVar] - hint = []rune(hintStr) - } - } - return -} - -var ( - // Hint signs - menuHint = readline.RESET + readline.DIM + readline.BOLD + " menu " + readline.RESET // Dim - envHint = readline.RESET + readline.GREEN + readline.BOLD + " env " + readline.RESET + readline.DIM + readline.GREEN // Green - commandHint = readline.RESET + readline.DIM + readline.BOLD + " command " + readline.RESET + readline.DIM + "\033[38;5;244m" // Cream - exeHint = readline.RESET + readline.DIM + readline.BOLD + " shell " + readline.RESET + readline.DIM // Dim - optionHint = "\033[38;5;222m" + readline.BOLD + " options " + readline.RESET + readline.DIM + "\033[38;5;222m" // Cream-Yellow - valueHint = readline.RESET + readline.DIM + readline.BOLD + " value " + readline.RESET + readline.DIM + "\033[38;5;244m" // Pink-Cream - // valueHint = "\033[38;5;217m" + readline.BOLD + " Value " + readline.RESET + readline.DIM + "\033[38;5;244m" // Pink-Cream - argHint = readline.DIM + "\033[38;5;217m" + readline.BOLD + " arg " + readline.RESET + readline.DIM + "\033[38;5;244m" // Pink-Cream -) diff --git a/readline/completers/local-filesystem.go b/readline/completers/local-filesystem.go deleted file mode 100644 index fcec4c5..0000000 --- a/readline/completers/local-filesystem.go +++ /dev/null @@ -1,205 +0,0 @@ -package completers - -import ( - "io/ioutil" - "os" - "os/user" - "path/filepath" - "strings" - - "github.com/maxlandon/readline" -) - -func completeLocalPath(last string) (string, *readline.CompletionGroup) { - - // Completions - completion := &readline.CompletionGroup{ - Name: "(console) local path", - MaxLength: 10, // The grid system is not yet able to roll on comps if > MaxLength - DisplayType: readline.TabDisplayGrid, - TrimSlash: true, - } - var suggestions []string - - // Any parsing error is silently ignored, for not messing the prompt - processedPath, _ := ParseEnvironmentVariables([]string{last}) - - // Check if processed input is empty - var inputPath string - if len(processedPath) == 1 { - inputPath = processedPath[0] - } - - // Add a slash if the raw input has one but not the processed input - if len(last) > 0 && last[len(last)-1] == '/' { - inputPath += "/" - } - - var linePath string // curated version of the inputPath - var absPath string // absolute path (excluding suffix) of the inputPath - var lastPath string // last directory in the input path - - if strings.HasSuffix(string(inputPath), "/") { - linePath = filepath.Dir(string(inputPath)) - absPath, _ = expand(string(linePath)) // Get absolute path - - } else if string(inputPath) == "" { - linePath = "." - absPath, _ = expand(string(linePath)) - } else { - linePath = filepath.Dir(string(inputPath)) - absPath, _ = expand(string(linePath)) // Get absolute path - lastPath = filepath.Base(string(inputPath)) // Save filter - } - - // 2) We take the absolute path we found, and get all dirs in it. - var dirs []string - files, _ := ioutil.ReadDir(absPath) - for _, file := range files { - if file.IsDir() { - dirs = append(dirs, file.Name()) - } - } - - switch lastPath { - case "": - for _, dir := range dirs { - if strings.HasPrefix(dir, lastPath) || lastPath == dir { - tokenized := addSpaceTokens(dir) - suggestions = append(suggestions, tokenized+"/") - } - } - default: - filtered := []string{} - for _, dir := range dirs { - if strings.HasPrefix(dir, lastPath) { - filtered = append(filtered, dir) - } - } - - for _, dir := range filtered { - if !hasPrefix([]rune(lastPath), []rune(dir)) || lastPath == dir { - tokenized := addSpaceTokens(dir) - suggestions = append(suggestions, tokenized+"/") - } - } - - } - - completion.Suggestions = suggestions - return string(lastPath), completion -} - -func addSpaceTokens(in string) (path string) { - items := strings.Split(in, " ") - for i := range items { - if len(items) == i+1 { // If last one, no char, add and return - path += items[i] - return - } - path += items[i] + "\\ " // By default add space char and roll - } - return -} - -func completeLocalPathAndFiles(last string) (string, *readline.CompletionGroup) { - - // Completions - completion := &readline.CompletionGroup{ - Name: "(console) local directory/files", - MaxLength: 10, // The grid system is not yet able to roll on comps if > MaxLength - DisplayType: readline.TabDisplayGrid, - TrimSlash: true, - } - var suggestions []string - - // Any parsing error is silently ignored, for not messing the prompt - processedPath, _ := ParseEnvironmentVariables([]string{last}) - - // Check if processed input is empty - var inputPath string - if len(processedPath) == 1 { - inputPath = processedPath[0] - } - - // Add a slash if the raw input has one but not the processed input - if len(last) > 0 && last[len(last)-1] == '/' { - inputPath += "/" - } - - var linePath string // curated version of the inputPath - var absPath string // absolute path (excluding suffix) of the inputPath - var lastPath string // last directory in the input path - - if strings.HasSuffix(string(inputPath), "/") { - linePath = filepath.Dir(string(inputPath)) // Trim the non needed slash - absPath, _ = expand(string(linePath)) // Get absolute path - - } else if string(inputPath) == "" { - linePath = "." - absPath, _ = expand(string(linePath)) - } else { - linePath = filepath.Dir(string(inputPath)) - absPath, _ = expand(string(linePath)) // Get absolute path - lastPath = filepath.Base(string(inputPath)) // Save filter - } - - // 2) We take the absolute path we found, and get all dirs in it. - var dirs []string - files, _ := ioutil.ReadDir(absPath) - for _, file := range files { - if file.IsDir() { - dirs = append(dirs, file.Name()) - } - } - - switch lastPath { - case "": - for _, file := range files { - if strings.HasPrefix(file.Name(), lastPath) || lastPath == file.Name() { - if file.IsDir() { - suggestions = append(suggestions, file.Name()+"/") - } else { - suggestions = append(suggestions, file.Name()) - } - } - } - default: - filtered := []os.FileInfo{} - for _, file := range files { - if strings.HasPrefix(file.Name(), lastPath) { - filtered = append(filtered, file) - } - } - - for _, file := range filtered { - if !hasPrefix([]rune(lastPath), []rune(file.Name())) || lastPath == file.Name() { - if file.IsDir() { - suggestions = append(suggestions, file.Name()+"/") - } else { - suggestions = append(suggestions, file.Name()) - } - } - } - - } - - completion.Suggestions = suggestions - return string(lastPath), completion -} - -// expand will expand a path with ~ to the $HOME of the current user. -func expand(path string) (string, error) { - if path == "" { - return path, nil - } - home := os.Getenv("HOME") - if home == "" { - usr, err := user.Current() - if err != nil { - return "", err - } - home = usr.HomeDir - } - return filepath.Abs(strings.Replace(path, "~", home, 1)) -} diff --git a/readline/completers/option-arguments.go b/readline/completers/option-arguments.go deleted file mode 100644 index 472c480..0000000 --- a/readline/completers/option-arguments.go +++ /dev/null @@ -1,77 +0,0 @@ -package completers - -import ( - "strings" - - "github.com/jessevdk/go-flags" - - "github.com/maxlandon/readline" -) - -// completeOptionArguments - Completes all values for arguments to a command. Arguments here are different from command options (--option). -// Many categories, from multiple sources in multiple contexts -func completeOptionArguments(cmd *flags.Command, opt *flags.Option, lastWord string) (prefix string, completions []*readline.CompletionGroup) { - - // By default the last word is the prefix - prefix = lastWord - - var comp *readline.CompletionGroup // This group is used as a buffer, to add groups to final completions - - // First of all: some options, no matter their contexts and subject, have default values. - // When we have such an option, we don't bother analyzing context, we just build completions and return. - if len(opt.Choices) > 0 { - comp = &readline.CompletionGroup{ - Name: opt.ValueName, // Value names are specified in struct metadata fields - DisplayType: readline.TabDisplayGrid, - } - for _, choice := range opt.Choices { - if strings.HasPrefix(choice, lastWord) { - comp.Suggestions = append(comp.Suggestions, choice) - } - } - completions = append(completions, comp) - return - } - - // EXAMPLE OF COMPLETING ARGUMENTS BASED ON THEIR NAMES ----------------------------------------------------------------------- - // We have 3 words, potentially different, with which we can filter: - // - // 1) '--option-name' is the string typed as input. - // 2) 'OptionName' is the name of the struct/type for this option. - // 3) 'ValueName' is the name of the value we expect. - // var match = func(name string) bool { - // if strings.Contains(opt.Field().Name, name) { - // return true - // } - // return false - // } - // - // // Sessions - // if match("ImplantID") || match("SessionID") { - // completions = append(completions, sessionIDs(lastWord)) - // } - // - // // Any arguments with a path name. Often we "save" files that need paths, certificates, etc - // if match("Path") || match("Save") || match("Certificate") || match("PrivateKey") { - // switch cmd.Name { - // case constants.WebContentTypeStr, constants.WebUpdateStr, constants.AddWebContentStr, constants.RmWebContentStr: - // // Make an exception for WebPath option in websites commands. - // default: - // switch opt.ValueName { - // case "local-path", "path": - // prefix, comp = completeLocalPath(lastWord) - // completions = append(completions, comp) - // case "local-file", "file": - // prefix, comp = completeLocalPathAndFiles(lastWord) - // completions = append(completions, comp) - // default: - // // We always have a default searching for files, locally - // prefix, comp = completeLocalPathAndFiles(lastWord) - // completions = append(completions, comp) - // } - // - // } - // } - // - return -} diff --git a/readline/completers/patterns.go b/readline/completers/patterns.go deleted file mode 100644 index 6de587a..0000000 --- a/readline/completers/patterns.go +++ /dev/null @@ -1,548 +0,0 @@ -package completers - -import ( - "os/exec" - "reflect" - "strings" - "unicode" - - "github.com/jessevdk/go-flags" -) - -// These functions are just shorthands for checking various conditions on the input line. -// They make the main function more readable, which might be useful, should a logic error pop somewhere. - -// [ Parser Commands & Options ] -------------------------------------------------------------------------- -// ArgumentByName Get the name of a detected command's argument -func argumentByName(command *flags.Command, name string) *flags.Arg { - args := command.Args() - for _, arg := range args { - if arg.Name == name { - return arg - } - } - return nil -} - -// optionByName - Returns an option for a command or a subcommand, identified by name -func optionByName(cmd *flags.Command, option string) *flags.Option { - - if cmd == nil { - return nil - } - // Get all (root) option groups. - groups := cmd.Groups() - - // For each group, build completions - for _, grp := range groups { - // Add each option to completion group - for _, opt := range grp.Options() { - if opt.LongName == option { - return opt - } - } - } - return nil -} - -// [ Menus ] -------------------------------------------------------------------------------------------- -// Is the input line is either empty, or without any detected command ? -func noCommandOrEmpty(args []string, last []rune, command *flags.Command) bool { - if len(args) == 0 || len(args) == 1 && command == nil { - return true - } - return false -} - -// [ Commands ] ------------------------------------------------------------------------------------- -// detectedCommand - Returns the base command from parser if detected, depending on context -func (c *CommandCompleter) detectedCommand(args []string) (command *flags.Command) { - arg := strings.TrimSpace(args[0]) - command = c.parser.Find(arg) - return -} - -// is the command a special command, usually not handled by parser ? -func isSpecialCommand(args []string, command *flags.Command) bool { - - // If command is not nil, return - if command == nil { - // Shell - if args[0] == "!" { - return true - } - // Exit - if args[0] == "exit" { - return true - } - return false - } - return false -} - -// The commmand has been found -func commandFound(command *flags.Command) bool { - if command != nil { - return true - } - return false -} - -// Search for input in $PATH -func commandFoundInPath(input string) bool { - _, err := exec.LookPath(input) - if err != nil { - return false - } - return true -} - -// [ SubCommands ]------------------------------------------------------------------------------------- -// Does the command have subcommands ? -func hasSubCommands(command *flags.Command, args []string) bool { - if len(args) < 2 || command == nil { - return false - } - - if len(command.Commands()) != 0 { - return true - } - - return false -} - -// Does the input has a subcommand in it ? -func subCommandFound(lastWord string, raw []string, command *flags.Command) (sub *flags.Command, ok bool) { - // First, filter redundant spaces. This does not modify the actual line - args := ignoreRedundantSpaces(raw) - - if len(args) <= 1 || command == nil { - return nil, false - } - - sub = command.Find(args[1]) - if sub != nil { - return sub, true - } - - return nil, false -} - -// Is the last input PRECISELY a subcommand. This is used as a brief hint for the subcommand -func lastIsSubCommand(lastWord string, command *flags.Command) bool { - if sub := command.Find(lastWord); sub != nil { - return true - } - return false -} - -// [ Arguments ]------------------------------------------------------------------------------------- -// Does the command have arguments ? -func hasArgs(command *flags.Command) bool { - if len(command.Args()) != 0 { - return true - } - return false -} - -// commandArgumentRequired - Analyses input and sends back the next argument name to provide completion for -func commandArgumentRequired(lastWord string, raw []string, command *flags.Command) (name string, yes bool) { - - // First, filter redundant spaces. This does not modify the actual line - args := ignoreRedundantSpaces(raw) - - // Trim command and subcommand args - var remain []string - if args[0] == command.Name { - remain = args[1:] - } - if len(args) > 1 && args[1] == command.Name { - remain = args[2:] - } - - // The remain may include a "" as a last element, - // which we don't consider as a real remain, so we move it away - switch lastWord { - case "": - case command.Name: - return "", false - } - - // Trim all --option flags and their arguments if they have - remain = filterOptions(remain, command) - - // For each argument, check if needs completion. If not continue, if yes return. - // The arguments remainder is popped according to the number of values expected. - for i, arg := range command.Args() { - - // If it's required and has one argument, check filled. - if arg.Required == 1 && arg.RequiredMaximum == 1 { - - // If last word is the argument, and we are - // last arg in: line keep completing. - if len(remain) < 1 { - return arg.Name, true - } - - // If the we are still writing the argument - if len(remain) == 1 { - if lastWord != "" { - return arg.Name, true - } - } - - // If filed and we are not last arg, continue - if len(remain) > 1 && i < (len(command.Args())-1) { - remain = remain[1:] - continue - } - - continue - } - - // If we need more than one value and we knwo the maximum, - // either return or pop the remain. - if arg.Required > 0 && arg.RequiredMaximum > 1 { - // Pop the corresponding amount of arguments. - var found int - for i := 0; i < len(remain) && i < arg.RequiredMaximum; i++ { - remain = remain[1:] - found++ - } - - // If we still need values: - if len(remain) == 0 && found <= arg.RequiredMaximum { - if lastWord == "" { // We are done, no more completions. - break - } else { - return arg.Name, true - } - } - // Else go on with the next argument - continue - } - - // If has required arguments, with no limit of needs, return true - if arg.Required > 0 && arg.RequiredMaximum == -1 { - return arg.Name, true - } - - // Else, if no requirements and the command has subcommands, - // return so that we complete subcommands - if arg.Required == -1 && len(command.Commands()) > 0 { - continue - } - - // Else, return this argument - // NOTE: This block is after because we always use []type arguments - // AFTER individual argument fields. Thus blocks any args that have - // not been processed. - if arg.Required == -1 { - return arg.Name, true - } - } - - // Once we exited the loop, it means that none of the arguments require completion: - // They are all either optional, or fullfiled according to their required numbers. - // Thus we return none - return "", false -} - -// getRemainingArgs - Filters the input slice from commands and detected option:value pairs, and returns args -func getRemainingArgs(args []string, last []rune, command *flags.Command) (remain []string) { - - var input []string - // Clean subcommand name - if args[0] == command.Name && len(args) >= 2 { - input = args[1:] - } else if len(args) == 1 { - input = args - } - - // For each each argument - for i := 0; i < len(input); i++ { - // Check option prefix - if strings.HasPrefix(input[i], "-") || strings.HasPrefix(input[i], "--") { - // Clean it - cur := strings.TrimPrefix(input[i], "--") - cur = strings.TrimPrefix(cur, "-") - - // Check if option matches any command option - if opt := command.FindOptionByLongName(cur); opt != nil { - boolean := true - if opt.Field().Type == reflect.TypeOf(boolean) { - continue // If option is boolean, don't skip an argument - } - i++ // Else skip next arg in input - continue - } - } - - // Safety check - if input[i] == "" || input[i] == " " { - continue - } - - remain = append(remain, input[i]) - } - - return -} - -// [ Options ]------------------------------------------------------------------------------------- -// commandOptionsAsked - Does the user asks for options in a root command ? -func commandOptionsAsked(args []string, lastWord string, command *flags.Command) bool { - if len(args) >= 2 && (strings.HasPrefix(lastWord, "-") || strings.HasPrefix(lastWord, "--")) { - return true - } - return false -} - -// commandOptionsAsked - Does the user asks for options in a subcommand ? -func subCommandOptionsAsked(args []string, lastWord string, command *flags.Command) bool { - if len(args) > 2 && (strings.HasPrefix(lastWord, "-") || strings.HasPrefix(lastWord, "--")) { - return true - } - return false -} - -// Is the last input argument is a dash ? -func isOptionDash(args []string, last []rune) bool { - if len(args) > 2 && (strings.HasPrefix(string(last), "-") || strings.HasPrefix(string(last), "--")) { - return true - } - return false -} - -// optionIsAlreadySet - Detects in input if an option is already set -func optionIsAlreadySet(args []string, lastWord string, opt *flags.Option) bool { - return false -} - -// Check if option type allows for repetition -func optionNotRepeatable(opt *flags.Option) bool { - return true -} - -// [ Option Values ]------------------------------------------------------------------------------------- -// Is the last input word an option name (--option) ? -func optionArgRequired(args []string, last []rune, group *flags.Group) (opt *flags.Option, yes bool) { - - var lastItem string - var lastOption string - var option *flags.Option - - // If there is argument required we must have 1) command 2) --option inputs at least. - if len(args) <= 2 { - return nil, false - } - - // Check for last two arguments in input - if strings.HasPrefix(args[len(args)-2], "-") || strings.HasPrefix(args[len(args)-2], "--") { - - // Long opts - if strings.HasPrefix(args[len(args)-2], "--") { - lastOption = strings.TrimPrefix(args[len(args)-2], "--") - if opt := group.FindOptionByLongName(lastOption); opt != nil { - option = opt - } - - // Short opts - } else if strings.HasPrefix(args[len(args)-2], "-") { - lastOption = strings.TrimPrefix(args[len(args)-2], "-") - if len(lastOption) > 0 { - if opt := group.FindOptionByShortName(rune(lastOption[0])); opt != nil { - option = opt - } - } - } - - } - - // If option is found, and we still are in writing the argument - if (lastItem == "" && option != nil) || option != nil { - // Check if option is a boolean, if yes return false - boolean := true - if option.Field().Type == reflect.TypeOf(boolean) { - return nil, false - } - - return option, true - } - - // Check for previous argument - if lastItem != "" && option == nil { - if strings.HasPrefix(args[len(args)-2], "-") || strings.HasPrefix(args[len(args)-2], "--") { - - // Long opts - if strings.HasPrefix(args[len(args)-2], "--") { - lastOption = strings.TrimPrefix(args[len(args)-2], "--") - if opt := group.FindOptionByLongName(lastOption); opt != nil { - option = opt - return option, true - } - - // Short opts - } else if strings.HasPrefix(args[len(args)-2], "-") { - lastOption = strings.TrimPrefix(args[len(args)-2], "-") - if opt := group.FindOptionByShortName(rune(lastOption[0])); opt != nil { - option = opt - return option, true - } - } - } - } - - return nil, false -} - -// [ Other ]------------------------------------------------------------------------------------- -// Does the user asks for Environment variables ? -func envVarAsked(args []string, lastWord string) bool { - - // Check if the current word is an environment variable, or if the last part of it is a variable - if len(lastWord) > 1 && strings.HasPrefix(lastWord, "$") { - if strings.LastIndex(lastWord, "/") < strings.LastIndex(lastWord, "$") { - return true - } - return false - } - - // Check if env var is asked in a path or something - if len(lastWord) > 1 { - // If last is a path, it cannot be an env var anymore - if lastWord[len(lastWord)-1] == '/' { - return false - } - - if lastWord[len(lastWord)-1] == '$' { - return true - } - } - - // If we are at the beginning of an env var - if len(lastWord) > 0 && lastWord[len(lastWord)-1] == '$' { - return true - } - - return false -} - -// filterOptions - Check various elements of an option and return a list -func filterOptions(args []string, command *flags.Command) (processed []string) { - - for i := 0; i < len(args); i++ { - arg := args[i] - // --long-name options - if strings.HasPrefix(arg, "--") { - name := strings.TrimPrefix(arg, "--") - if opt := optionByName(command, name); opt != nil { - var boolean = true - if opt.Field().Type == reflect.TypeOf(boolean) { - continue - } - // Else skip the option argument (next item) - i++ - } - continue - } - // -s short options - if strings.HasPrefix(arg, "-") { - name := strings.TrimPrefix(arg, "-") - if opt := optionByName(command, name); opt != nil { - var boolean = true - if opt.Field().Type == reflect.TypeOf(boolean) { - continue - } - // Else skip the option argument (next item) - i++ - } - continue - } - processed = append(processed, arg) - } - - return -} - -// Other Functions -------------------------------------------------------------------------------------------------------------// - -// formatInput - Formats & sanitize the command line input -func formatInput(line []rune) (args []string, last []rune, lastWord string) { - args = strings.Split(string(line), " ") // The readline input as a []string - last = trimSpaceLeft([]rune(args[len(args)-1])) // The last char in input - lastWord = string(last) - return -} - -// FormatInput - Formats & sanitize the command line input -func formatInputHighlighter(line []rune) (args []string, last []rune, lastWord string) { - args = strings.SplitN(string(line), " ", -1) - last = trimSpaceLeft([]rune(args[len(args)-1])) // The last char in input - lastWord = string(last) - return -} - -// ignoreRedundantSpaces - We might have several spaces between each real arguments. -// However these indivual spaces are counted as args themselves. -// For each space arg found, verify that no space args follow, -// and if some are found, delete them. -func ignoreRedundantSpaces(raw []string) (args []string) { - - for i := 0; i < len(raw); i++ { - // Catch a space argument. - if raw[i] == "" { - // The arg evaulated is always kept, because we just adjusted - // the indexing to avoid the ones we don't need - // args = append(args, raw[i]) - - for y, next := range raw[i:] { - if next != "" { - i += y - 1 - break - } - // If we come to the end while not breaking - // we push the outer loop straight to the end. - if y == len(raw[i:])-1 { - i += y - } - } - } else { - // The arg evaulated is always kept, because we just adjusted - // the indexing to avoid the ones we don't need - args = append(args, raw[i]) - } - } - - return -} - -func trimSpaceLeft(in []rune) []rune { - firstIndex := len(in) - for i, r := range in { - if unicode.IsSpace(r) == false { - firstIndex = i - break - } - } - return in[firstIndex:] -} - -func equal(a, b []rune) bool { - if len(a) != len(b) { - return false - } - for i := 0; i < len(a); i++ { - if a[i] != b[i] { - return false - } - } - return true -} - -func hasPrefix(r, prefix []rune) bool { - if len(r) < len(prefix) { - return false - } - return equal(r[:len(prefix)], prefix) -} diff --git a/readline/completers/syntax-highlighter.go b/readline/completers/syntax-highlighter.go deleted file mode 100644 index 8bce99f..0000000 --- a/readline/completers/syntax-highlighter.go +++ /dev/null @@ -1,151 +0,0 @@ -package completers - -import ( - "fmt" - "strings" - - "github.com/jessevdk/go-flags" - - "github.com/maxlandon/readline" -) - -// SyntaxHighlighter - Entrypoint to all input syntax highlighting in the Wiregost console -func (c *CommandCompleter) SyntaxHighlighter(input []rune) (line string) { - - // Format and sanitize input - args, last, lastWord := formatInputHighlighter(input) - - // Remain is all arguments that have not been highlighted, we need it for completing long commands - var remain = args - - // Detect base command automatically - var command = c.detectedCommand(args) - - // Return input as is - if noCommandOrEmpty(remain, last, command) { - return string(input) - } - - // Base command - if commandFound(command) { - line, remain = highlightCommand(remain, command) - - // SubCommand - if sub, ok := subCommandFound(lastWord, args, command); ok { - line, remain = highlightSubCommand(line, remain, sub) - } - - } - - line = processRemain(line, remain) - - return -} - -func highlightCommand(args []string, command *flags.Command) (line string, remain []string) { - line = readline.BOLD + args[0] + readline.RESET + " " - remain = args[1:] - return -} - -func highlightSubCommand(input string, args []string, command *flags.Command) (line string, remain []string) { - line = input - line += readline.BOLD + args[0] + readline.RESET + " " - remain = args[1:] - return -} - -func processRemain(input string, remain []string) (line string) { - - // Check the last is not the last space in input - if len(remain) == 1 && remain[0] == " " { - return input - } - - line = input + strings.Join(remain, " ") - // line = processEnvVars(input, remain) - return -} - -// processEnvVars - Highlights environment variables. NOTE: Rewrite with logic from console/env.go -func processEnvVars(input string, remain []string) (line string) { - - var processed []string - - inputSlice := strings.Split(input, " ") - - // Check already processed input - for _, arg := range inputSlice { - if arg == "" || arg == " " { - continue - } - if strings.HasPrefix(arg, "$") { // It is an env var. - if args := strings.Split(arg, "/"); len(args) > 1 { - for _, a := range args { - fmt.Println(a) - if strings.HasPrefix(a, "$") && a != " " { // It is an env var. - processed = append(processed, "\033[38;5;108m"+readline.DIM+a+readline.RESET) - continue - } - } - } - processed = append(processed, "\033[38;5;108m"+readline.DIM+arg+readline.RESET) - continue - } - processed = append(processed, arg) - } - - // Check remaining args (non-processed) - for _, arg := range remain { - if arg == "" { - continue - } - if strings.HasPrefix(arg, "$") && arg != "$" { // It is an env var. - var full string - args := strings.Split(arg, "/") - if len(args) == 1 { - if strings.HasPrefix(args[0], "$") && args[0] != "" && args[0] != "$" { // It is an env var. - full += "\033[38;5;108m" + readline.DIM + args[0] + readline.RESET - continue - } - } - if len(args) > 1 { - var counter int - for _, arg := range args { - // If var is an env var - if strings.HasPrefix(arg, "$") && arg != "" && arg != "$" { - if counter < len(args)-1 { - full += "\033[38;5;108m" + readline.DIM + args[0] + readline.RESET + "/" - counter++ - continue - } - if counter == len(args)-1 { - full += "\033[38;5;108m" + readline.DIM + args[0] + readline.RESET - counter++ - continue - } - } - - // Else, if we are not at the end of array - if counter < len(args)-1 && arg != "" { - full += arg + "/" - counter++ - } - if counter == len(args)-1 { - full += arg - counter++ - } - } - } - // Else add first var - processed = append(processed, full) - } - } - - line = strings.Join(processed, " ") - - // Very important, keeps the line clear when erasing - // line += " " - - return -} diff --git a/readline/completers/tab-completer.go b/readline/completers/tab-completer.go deleted file mode 100644 index 1c9a942..0000000 --- a/readline/completers/tab-completer.go +++ /dev/null @@ -1,289 +0,0 @@ -package completers - -import ( - "errors" - "fmt" - "strings" - - "github.com/jessevdk/go-flags" - - "github.com/maxlandon/readline" -) - -// CommandCompleter - A completer using a github.com/jessevdk/go-flags Command Parser, in order -// to build completions for commands, arguments, options and their arguments as well. -// This completer needs to be instantiated with its constructor, in order to ensure the parser is not nil. -type CommandCompleter struct { - parser *flags.Parser -} - -// NewCommandCompleter - Instantiate a new tab completer using a github.com/jessevdk/go-flags Command Parser. -func NewCommandCompleter(parser *flags.Parser) (completer *CommandCompleter, err error) { - if parser == nil { - return nil, errors.New("command completer was instantiated with a nil parser") - } - return &CommandCompleter{parser: parser}, nil -} - -// TabCompleter - A default tab completer working with a github.com/jessevdk/go-flags parser. -func (c *CommandCompleter) TabCompleter(line []rune, pos int, dtc readline.DelayedTabContext) (lastWord string, completions []*readline.CompletionGroup) { - - // Format and sanitize input - // @args => All items of the input line - // @last => The last word detected in input line as []rune - // @lastWord => The last word detected in input as string - args, last, lastWord := formatInput(line) - - // Detect base command automatically - var command = c.detectedCommand(args) - - // Propose commands - if noCommandOrEmpty(args, last, command) { - return c.completeMenuCommands(lastWord, pos) - } - - // Check environment variables - if envVarAsked(args, lastWord) { - completeEnvironmentVariables(lastWord) - } - - // Base command has been identified - if commandFound(command) { - // Check environment variables again - if envVarAsked(args, lastWord) { - return completeEnvironmentVariables(lastWord) - } - - // If options are asked for root command, return commpletions. - if len(command.Groups()) > 0 { - for _, grp := range command.Groups() { - if opt, yes := optionArgRequired(args, last, grp); yes { - return completeOptionArguments(command, opt, lastWord) - } - } - } - - // Then propose subcommands. We don't return from here, otherwise it always skips the next steps. - if hasSubCommands(command, args) { - completions = completeSubCommands(args, lastWord, command) - } - - // Handle subcommand if found (maybe we should rewrite this function and use it also for base command) - if sub, ok := subCommandFound(lastWord, args, command); ok { - return handleSubCommand(line, pos, sub) - } - - // If user asks for completions with "-" / "--", show command options. - // We ask this here, after having ensured there is no subcommand invoked. - // This prevails over command arguments, even if they are required. - if commandOptionsAsked(args, lastWord, command) { - return completeCommandOptions(args, lastWord, command) - } - - // Propose argument completion before anything, and if needed - if arg, yes := commandArgumentRequired(lastWord, args, command); yes { - return completeCommandArguments(command, arg, lastWord) - } - - } - - return -} - -// [ Main Completion Functions ] ----------------------------------------------------------------------------------------------------------------- - -// completeMenuCommands - Selects all commands available in a given context and returns them as suggestions -// Many categories, all from command parsers. -func (c *CommandCompleter) completeMenuCommands(lastWord string, pos int) (prefix string, completions []*readline.CompletionGroup) { - - prefix = lastWord // We only return the PREFIX for readline to correctly show suggestions. - - // Check their namespace (which should be their "group" (like utils, core, Jobs, etc)) - for _, cmd := range c.parser.Commands() { - // If command matches readline input - if strings.HasPrefix(cmd.Name, lastWord) { - // Check command group: add to existing group if found - var found bool - for _, grp := range completions { - if grp.Name == cmd.Aliases[0] { - found = true - grp.Suggestions = append(grp.Suggestions, cmd.Name) - grp.Descriptions[cmd.Name] = readline.Dim(cmd.ShortDescription) - } - } - // Add a new group if not found - if !found { - grp := &readline.CompletionGroup{ - Name: cmd.Aliases[0], - Suggestions: []string{cmd.Name}, - Descriptions: map[string]string{ - cmd.Name: readline.Dim(cmd.ShortDescription), - }, - } - completions = append(completions, grp) - } - } - } - - // Make adjustments to the CompletionGroup list: set maxlength depending on items, check descriptions, etc. - for _, grp := range completions { - // If the length of suggestions is too long and we have - // many groups, use grid display. - if len(completions) >= 10 && len(grp.Suggestions) >= 7 { - grp.DisplayType = readline.TabDisplayGrid - } else { - // By default, we use a map of command to descriptions - grp.DisplayType = readline.TabDisplayList - } - } - - return -} - -// completeSubCommands - Takes subcommands and gives them as suggestions -// One category, from one source (a parent command). -func completeSubCommands(args []string, lastWord string, command *flags.Command) (completions []*readline.CompletionGroup) { - - group := &readline.CompletionGroup{ - Name: command.Name, - Suggestions: []string{}, - Descriptions: map[string]string{}, - DisplayType: readline.TabDisplayList, - } - - for _, sub := range command.Commands() { - if strings.HasPrefix(sub.Name, lastWord) { - group.Suggestions = append(group.Suggestions, sub.Name) - group.Descriptions[sub.Name] = readline.DIM + sub.ShortDescription + readline.RESET - } - } - - completions = append(completions, group) - - return -} - -// handleSubCommand - Handles completion for subcommand options and arguments, + any option value related completion -// Many categories, from many sources: this function calls the same functions as the ones previously called for completing its parent command. -func handleSubCommand(line []rune, pos int, command *flags.Command) (lastWord string, completions []*readline.CompletionGroup) { - - args, last, lastWord := formatInput(line) - - // Check environment variables - if envVarAsked(args, lastWord) { - completeEnvironmentVariables(lastWord) - } - - // Check argument options - if len(command.Groups()) > 0 { - for _, grp := range command.Groups() { - if opt, yes := optionArgRequired(args, last, grp); yes { - return completeOptionArguments(command, opt, lastWord) - } - } - } - - // If user asks for completions with "-" or "--". This must take precedence on arguments. - if subCommandOptionsAsked(args, lastWord, command) { - return completeCommandOptions(args, lastWord, command) - } - - // If command has non-filled arguments, propose them first - if arg, yes := commandArgumentRequired(lastWord, args, command); yes { - return completeCommandArguments(command, arg, lastWord) - } - - return -} - -// completeCommandOptions - Yields completion for options of a command, with various decorators -// Many categories, from one source (a command) -func completeCommandOptions(args []string, lastWord string, cmd *flags.Command) (prefix string, completions []*readline.CompletionGroup) { - - prefix = lastWord // We only return the PREFIX for readline to correctly show suggestions. - - // Get all (root) option groups. - groups := cmd.Groups() - - // Append command options not gathered in groups - groups = append(groups, cmd.Group) - - // For each group, build completions - for _, grp := range groups { - - _, comp := completeOptionGroup(lastWord, grp, "") - - // No need to add empty groups, will screw the completion system. - if len(comp.Suggestions) > 0 { - completions = append(completions, comp) - } - } - - // Do the same for global options, which are not part of any group "per-se" - _, gcomp := completeOptionGroup(lastWord, cmd.Group, "global options") - if len(gcomp.Suggestions) > 0 { - completions = append(completions, gcomp) - } - - return -} - -// completeOptionGroup - make completions for a single group of options. Title is optional, not used if empty. -func completeOptionGroup(lastWord string, grp *flags.Group, title string) (prefix string, compGrp *readline.CompletionGroup) { - - compGrp = &readline.CompletionGroup{ - Name: grp.ShortDescription, - Descriptions: map[string]string{}, - DisplayType: readline.TabDisplayList, - Aliases: map[string]string{}, - } - - // An optional title for this comp group. - // Used by global flag options, added to all commands. - if title != "" { - compGrp.Name = title - } - - // Add each option to completion group - for _, opt := range grp.Options() { - - // Check if option is already set, next option if yes - // if optionNotRepeatable(opt) && optionIsAlreadySet(args, lastWord, opt) { - // continue - // } - - // Depending on the current last word, either build a group with option longs only, or with shorts - if strings.HasPrefix("--"+opt.LongName, lastWord) { - optName := "--" + opt.LongName - compGrp.Suggestions = append(compGrp.Suggestions, optName) - - // Add short if there is, and that the prefix is only one dash - if strings.HasPrefix("-", lastWord) { - if opt.ShortName != 0 { - compGrp.Aliases[optName] = "-" + string(opt.ShortName) - } - } - - // Option default value if any - var def string - if len(opt.Default) > 0 { - def = " (default:" - for _, d := range opt.Default { - def += " " + d + "," - } - def = strings.TrimSuffix(def, ",") - def += ")" - } - - desc := fmt.Sprintf(" -- %s%s%s", opt.Description, def, readline.RESET) - compGrp.Descriptions[optName] = desc - } - } - return -} - -// RecursiveGroupCompletion - Handles recursive completion for nested option groups -// Many categories, one source (a command's root option group). Called by the function just above. -func RecursiveGroupCompletion(args []string, last []rune, group *flags.Group) (lastWord string, completions []*readline.CompletionGroup) { - return -} diff --git a/readline/examples/arguments.go b/readline/examples/arguments.go deleted file mode 100644 index d976888..0000000 --- a/readline/examples/arguments.go +++ /dev/null @@ -1,109 +0,0 @@ -package main - -// This file defines a few argument choices for commands - -import ( - "github.com/jessevdk/go-flags" -) - -// Command/option argument choices -var ( - // Logs & components - logLevels = []string{"trace", "debug", "info", "warning", "error"} - loggers = []string{"client", "comm"} - - // Stages / Stagers - implantOS = []string{"windows", "linux", "darwin"} - implantArch = []string{"amd64", "x86"} - implantFmt = []string{"exe", "shared", "service", "shellcode"} - - stageListenerProtocols = []string{"tcp", "http", "https"} - - // MSF - msfStagerProtocols = []string{"tcp", "http", "https"} - msfTransformFormats = []string{ - "bash", - "c", - "csharp", - "dw", - "dword", - "hex", - "java", - "js_be", - "js_le", - "num", - "perl", - "pl", - "powershell", - "ps1", - "py", - "python", - "raw", - "rb", - "ruby", - "sh", - "vbapplication", - "vbscript", - } - - msfEncoders = []string{ - "x86/shikata_ga_nai", - "x64/xor_dynamic", - } - - msfPayloads = map[string][]string{ - "windows": windowsMsfPayloads, - "linux": linuxMsfPayloads, - "osx": osxMsfPayloads, - } - - // ValidPayloads - Valid payloads and OS combos - windowsMsfPayloads = []string{ - "meterpreter_reverse_http", - "meterpreter_reverse_https", - "meterpreter_reverse_tcp", - "meterpreter/reverse_tcp", - "meterpreter/reverse_http", - "meterpreter/reverse_https", - } - linuxMsfPayloads = []string{ - "meterpreter_reverse_http", - "meterpreter_reverse_https", - "meterpreter_reverse_tcp", - } - osxMsfPayloads = []string{ - "meterpreter_reverse_http", - "meterpreter_reverse_https", - "meterpreter_reverse_tcp", - } - - // Comm network protocols - portfwdProtocols = []string{"tcp", "udp"} - transportProtocols = []string{"tcp", "udp", "ip"} - applicationProtocols = []string{"http", "https", "mtls", "quic", "http3", "dns", "named_pipe"} -) - -// loadArgumentCompletions - Adds a bunch of choices for command arguments (and their completions.) -func loadArgumentCompletions(parser *flags.Parser) { - if parser == nil { - return - } - serverCompsAddtional(parser) -} - -// Additional completion mappings for command in the server context -func serverCompsAddtional(parser *flags.Parser) { - - // Stage options - g := parser.Find("generate") - g.FindOptionByLongName("os").Choices = implantOS - g.FindOptionByLongName("arch").Choices = implantArch - g.FindOptionByLongName("format").Choices = implantFmt - - // Stager options (mostly MSF) - gs := g.Find("stager") - gs.FindOptionByLongName("os").Choices = implantOS - gs.FindOptionByLongName("arch").Choices = implantArch - gs.FindOptionByLongName("protocol").Choices = msfStagerProtocols - gs.FindOptionByLongName("msf-format").Choices = msfTransformFormats -} diff --git a/readline/examples/commands.go b/readline/examples/commands.go deleted file mode 100644 index fcd9271..0000000 --- a/readline/examples/commands.go +++ /dev/null @@ -1,315 +0,0 @@ -package main - -import ( - "bufio" - "fmt" - "os" - "os/exec" - "os/user" - "path/filepath" - "strings" - - "github.com/jessevdk/go-flags" - - "github.com/maxlandon/readline" -) - -// This file declares a go-flags parser and a few commands. - -var ( - // commandParser - The command parser used by the example console. - commandParser = flags.NewNamedParser("example", flags.IgnoreUnknown) -) - -func bindCommands() (err error) { - - // core console - // ---------------------------------------------------------------------------------------- - ex, err := commandParser.AddCommand("exit", // Command string - "Exit from the client/server console", // Description (completions, help usage) - "", // Long description - &Exit{}) // Command implementation - ex.Aliases = []string{"core"} - - cd, err := commandParser.AddCommand("cd", - "Change client working directory", - "", - &ChangeClientDirectory{}) - cd.Aliases = []string{"core"} - - ls, err := commandParser.AddCommand("ls", - "List directory contents", - "", - &ListClientDirectories{}) - ls.Aliases = []string{"core"} - - // Log - log, err := commandParser.AddCommand("log", - "Manage log levels of one or more components", - "", - &Log{}) - log.Aliases = []string{"core"} - - // Implant generation - // ---------------------------------------------------------------------------------------- - g, err := commandParser.AddCommand("generate", - "Configure and compile an implant (staged or stager)", - "", - &Generate{}) - g.Aliases = []string{"builds"} - g.SubcommandsOptional = true - - _, err = g.AddCommand("stager", - "Generate a stager shellcode payload using MSFVenom, (to file: --save, to stdout: --format", - "", - &GenerateStager{}) - - r, err := commandParser.AddCommand("regenerate", - "Recompile an implant by name, passed as argument (completed)", - "", - &Regenerate{}) - r.Aliases = []string{"builds"} - - // Add choices completions (and therefore completions) to some of these commands. - loadArgumentCompletions(commandParser) - - return -} - -// Exit - Kill the current client console -type Exit struct{} - -// Execute - Run -func (e *Exit) Execute(args []string) (err error) { - - reader := bufio.NewReader(os.Stdin) - fmt.Print("Confirm exit (Y/y): ") - text, _ := reader.ReadString('\n') - answer := strings.TrimSpace(text) - - if (answer == "Y") || (answer == "y") { - os.Exit(0) - } - - fmt.Println() - return -} - -// ChangeClientDirectory - Change the working directory of the client console -type ChangeClientDirectory struct { - Positional struct { - Path string `description:"local path" required:"1-1"` - } `positional-args:"yes" required:"yes"` -} - -// Execute - Handler for ChangeDirectory -func (cd *ChangeClientDirectory) Execute(args []string) (err error) { - - dir, err := expand(cd.Positional.Path) - - err = os.Chdir(dir) - if err != nil { - fmt.Printf(CommandError+"%s \n", err) - } else { - fmt.Printf(Info+"Changed directory to %s \n", dir) - } - - return -} - -// ListClientDirectories - List directory contents -type ListClientDirectories struct { - Positional struct { - Path []string `description:"local directory/file"` - } `positional-args:"yes"` -} - -// Execute - Command -func (ls *ListClientDirectories) Execute(args []string) error { - - base := []string{"ls", "--color", "-l"} - - if len(ls.Positional.Path) == 0 { - ls.Positional.Path = []string{"."} - } - - fullPaths := []string{} - for _, path := range ls.Positional.Path { - full, _ := expand(path) - fullPaths = append(fullPaths, full) - } - base = append(base, fullPaths...) - - // Print output - out, err := shellExec(base[0], base[1:]) - if err != nil { - fmt.Printf(CommandError+"%s \n", err.Error()) - return nil - } - - // Print output - fmt.Println(out) - - return nil -} - -// shellExec - Execute a program -func shellExec(executable string, args []string) (string, error) { - path, err := exec.LookPath(executable) - if err != nil { - return "", err - } - - cmd := exec.Command(path, args...) - - // Load OS environment - cmd.Env = os.Environ() - - out, err := cmd.CombinedOutput() - - if err != nil { - return "", err - } - return strings.Trim(string(out), "/"), nil -} - -// Generate - Configure and compile an implant -type Generate struct { - StageOptions // Command makes use of full stage options -} - -// StageOptions - All these options, regrouped by area, are used by any command that needs full -// configuration information for a stage Sliver implant. -type StageOptions struct { - // CoreOptions - All options about OS/arch, files to save, debugs, etc. - CoreOptions struct { - OS string `long:"os" short:"o" description:"target host operating system" default:"windows" value-name:"stage OS"` - Arch string `long:"arch" short:"a" description:"target host CPU architecture" default:"amd64" value-name:"stage architectures"` - Format string `long:"format" short:"f" description:"output formats (exe, shared (DLL), service (see 'psexec' for info), shellcode (Windows only)" default:"exe" value-name:"stage formats"` - Profile string `long:"profile-name" description:"implant profile name to use (use with generate-profile)"` - Name string `long:"name" short:"N" description:"implant name to use (overrides random name generation)"` - Save string `long:"save" short:"s" description:"directory/file where to save binary"` - Debug bool `long:"debug" short:"d" description:"enable debug features (incompatible with obfuscation, and prevailing)"` - } `group:"core options"` - - // TransportOptions - All options pertaining to transport/RPC matters - TransportOptions struct { - MTLS []string `long:"mtls" short:"m" description:"mTLS C2 domain(s), comma-separated (ex: mtls://host:port)" env-delim:","` - DNS []string `long:"dns" short:"n" description:"DNS C2 domain(s), comma-separated (ex: dns://mydomain.com)" env-delim:","` - HTTP []string `long:"http" short:"h" description:"HTTP(S) C2 domain(s)" env-delim:","` - NamedPipe []string `long:"named-pipe" short:"p" description:"Named pipe transport strings, comma-separated" env-delim:","` - TCPPivot []string `long:"tcp-pivot" short:"i" description:"TCP pivot transport strings, comma-separated" env-delim:","` - Reconnect int `long:"reconnect" short:"j" description:"attempt to reconnect every n second(s)" default:"60"` - MaxErrors int `long:"max-errors" short:"k" description:"max number of transport errors" default:"10"` - } `group:"transport options"` - - // SecurityOptions - All security-oriented options like restrictions. - SecurityOptions struct { - LimitDatetime string `long:"limit-datetime" short:"w" description:"limit execution to before datetime"` - LimitDomain bool `long:"limit-domain-joined" short:"D" description:"limit execution to domain joined machines"` - LimitUsername string `long:"limit-username" short:"U" description:"limit execution to specified username"` - LimitHosname string `long:"limit-hostname" short:"H" description:"limit execution to specified hostname"` - LimitFileExits string `long:"limit-file-exists" short:"F" description:"limit execution to hosts with this file in the filesystem"` - } `group:"security options"` - - // EvasionOptions - All proactive security options (obfuscation, evasion, etc) - EvasionOptions struct { - Canary []string `long:"canary" short:"c" description:"DNS canary domain strings, comma-separated" env-delim:","` - SkipSymbols bool `long:"skip-obfuscation" short:"b" description:"skip binary/symbol obfuscation"` - Evasion bool `long:"evasion" short:"e" description:"enable evasion features"` - } `group:"evasion options"` -} - -// Execute - Configure and compile an implant -func (g *Generate) Execute(args []string) (err error) { - save := g.CoreOptions.Save - if save == "" { - save, _ = os.Getwd() - } - - fmt.Println("Executed 'generate' command. ") - return -} - -// Regenerate - Recompile an implant by name, passed as argument (completed) -type Regenerate struct { - Positional struct { - ImplantName string `description:"Name of Sliver implant to recompile" required:"1-1"` - } `positional-args:"yes" required:"yes"` - Save string `long:"save" short:"s" description:"Directory/file where to save binary"` -} - -// Execute - Recompile an implant with a given profile -func (r *Regenerate) Execute(args []string) (err error) { - fmt.Println("Executed 'regenerate' command. ") - return -} - -// GenerateStager - Generate a stager payload using MSFVenom -type GenerateStager struct { - PayloadOptions struct { - OS string `long:"os" short:"o" description:"target host operating system" default:"windows" value-name:"stage OS"` - Arch string `long:"arch" short:"a" description:"target host CPU architecture" default:"amd64" value-name:"stage architectures"` - Format string `long:"msf-format" short:"f" description:"output format (MSF Venom formats). List is auto-completed" default:"raw" value-name:"MSF Venom transform formats"` - BadChars string `long:"badchars" short:"b" description:"bytes to exclude from stage shellcode"` - Save string `long:"save" short:"s" description:"directory to save the generated stager to"` - } `group:"payload options"` - TransportOptions struct { - LHost string `long:"lhost" short:"l" description:"listening host address" required:"true"` - LPort int `long:"lport" short:"p" description:"listening host port" default:"8443"` - Protocol string `long:"protocol" short:"P" description:"staging protocol (tcp/http/https)" default:"tcp" value-name:"stager protocol"` - } `group:"transport options"` -} - -// Execute - Generate a stager payload using MSFVenom -func (g *GenerateStager) Execute(args []string) (err error) { - fmt.Println("Executed 'generate stager' subcommand. ") - return -} - -// Log - Log management commands. Sets log level by default. -type Log struct { - Positional struct { - Level string `description:"log level to filter by" required:"1-1"` - Components []string `description:"components on which to apply log filter" required:"1"` - } `positional-args:"yes" required:"true"` -} - -// Execute - Set the log level of one or more components -func (l *Log) Execute(args []string) (err error) { - fmt.Println("Executed 'log' command. ") - return -} - -var ( - Info = fmt.Sprintf("%s[-]%s ", readline.BLUE, readline.RESET) - Warn = fmt.Sprintf("%s[!]%s ", readline.YELLOW, readline.RESET) - Error = fmt.Sprintf("%s[!]%s ", readline.RED, readline.RESET) - Success = fmt.Sprintf("%s[*]%s ", readline.GREEN, readline.RESET) - - Infof = fmt.Sprintf("%s[-] ", readline.BLUE) // Infof - formatted - Warnf = fmt.Sprintf("%s[!] ", readline.YELLOW) // Warnf - formatted - Errorf = fmt.Sprintf("%s[!] ", readline.RED) // Errorf - formatted - Sucessf = fmt.Sprintf("%s[*] ", readline.GREEN) // Sucessf - formatted - - RPCError = fmt.Sprintf("%s[RPC Error]%s ", readline.RED, readline.RESET) - CommandError = fmt.Sprintf("%s[Command Error]%s ", readline.RED, readline.RESET) - ParserError = fmt.Sprintf("%s[Parser Error]%s ", readline.RED, readline.RESET) - DBError = fmt.Sprintf("%s[DB Error]%s ", readline.RED, readline.RESET) -) - -// expand will expand a path with ~ to the $HOME of the current user. -func expand(path string) (string, error) { - if path == "" { - return path, nil - } - home := os.Getenv("HOME") - if home == "" { - usr, err := user.Current() - if err != nil { - return "", err - } - home = usr.HomeDir - } - return filepath.Abs(strings.Replace(path, "~", home, 1)) -} diff --git a/readline/examples/main.go b/readline/examples/main.go deleted file mode 100644 index 16fd4de..0000000 --- a/readline/examples/main.go +++ /dev/null @@ -1,171 +0,0 @@ -package main - -import ( - "fmt" - "strings" - - "github.com/jessevdk/go-flags" - - "github.com/maxlandon/readline" - "github.com/maxlandon/readline/completers" -) - -// This file shows a typical way of using readline in a loop. - -func main() { - // Instantiate a console object - console := newConsole() - - // Bind commands to the console - bindCommands() - - // Setup the console completers, prompts, and input modes - console.setup() - - // Start the readline loop (blocking) - console.Start() -} - -// newConsole - Instantiates a new console with some default behavior. -// We modify/add elements of behavior later in setup. -func newConsole() *console { - console := &console{ - shell: readline.NewInstance(), - parser: commandParser, - } - return console -} - -// console - A simple console example. -type console struct { - shell *readline.Instance - parser *flags.Parser -} - -// setup - The console sets up various elements such as the completion system, hints, -// syntax highlighting, prompt system, commands binding, and client environment loading. -func (c *console) setup() (err error) { - - // Input mode & defails - c.shell.InputMode = readline.Vim // Could be readline.Emacs for emacs input mode. - c.shell.ShowVimMode = true - c.shell.VimModeColorize = true - - // Prompt: we want a two-line prompt, with a custom indicator after the Vim status - c.shell.SetPrompt("readline ") - c.shell.Multiline = true - c.shell.MultilinePrompt = " > " - - // Instantiate a default completer associated with the parser - // declared in commands.go, and embedded into the console struct. - // The error is muted, because we don't pass an nil parser, therefore no problems. - defaultCompleter, _ := completers.NewCommandCompleter(c.parser) - - // Register the completer for command/option completions, hints and syntax highlighting. - // The completer can handle all of them. - c.shell.TabCompleter = defaultCompleter.TabCompleter - c.shell.HintText = defaultCompleter.HintCompleter - c.shell.SyntaxHighlighter = defaultCompleter.SyntaxHighlighter - - // History: by default the history is in-memory, use it with Ctrl-R - - return -} - -// Start - The console has a working RPC connection: we setup all -// things pertaining to the console itself, and start the input loop. -func (c *console) Start() (err error) { - - // Setup console elements - err = c.setup() - if err != nil { - return fmt.Errorf("Console setup failed: %s", err) - } - - // Start input loop - for { - // Read input line - line, _ := c.Readline() - - // Split and sanitize input - sanitized, empty := sanitizeInput(line) - if empty { - continue - } - - // Process various tokens on input (environment variables, paths, etc.) - // These tokens will be expaneded by completers anyway, so this is not absolutely required. - envParsed, _ := completers.ParseEnvironmentVariables(sanitized) - - // Other types of tokens, needed by commands who expect a certain type - // of arguments, such as paths with spaces. - tokenParsed := c.parseTokens(envParsed) - - // Execute the command and print any errors - if _, parserErr := c.parser.ParseArgs(tokenParsed); parserErr != nil { - fmt.Println(readline.RED + "[Error] " + readline.RESET + parserErr.Error() + "\n") - } - } -} - -// Readline - Add an empty line between input line and command output. -func (c *console) Readline() (line string, err error) { - line, err = c.shell.Readline() - fmt.Println() - return -} - -// sanitizeInput - Trims spaces and other unwished elements from the input line. -func sanitizeInput(line string) (sanitized []string, empty bool) { - - // Assume the input is not empty - empty = false - - // Trim border spaces - trimmed := strings.TrimSpace(line) - if len(line) < 1 { - empty = true - return - } - unfiltered := strings.Split(trimmed, " ") - - // Catch any eventual empty items - for _, arg := range unfiltered { - if arg != "" { - sanitized = append(sanitized, arg) - } - } - return -} - -// parseTokens - Parse and process any special tokens that are not treated by environment-like parsers. -func (c *console) parseTokens(sanitized []string) (parsed []string) { - - // PATH SPACE TOKENS - // Catch \ tokens, which have been introduced in paths where some directories have spaces in name. - // For each of these splits, we concatenate them with the next string. - // This will also inspect commands/options/arguments, but there is no reason why a backlash should be present in them. - var pathAdjusted []string - var roll bool - var arg string - for i := range sanitized { - if strings.HasSuffix(sanitized[i], "\\") { - // If we find a suffix, replace with a space. Go on with next input - arg += strings.TrimSuffix(sanitized[i], "\\") + " " - roll = true - } else if roll { - // No suffix but part of previous input. Add it and go on. - arg += sanitized[i] - pathAdjusted = append(pathAdjusted, arg) - arg = "" - roll = false - } else { - // Default, we add our path and go on. - pathAdjusted = append(pathAdjusted, sanitized[i]) - } - } - parsed = pathAdjusted - - // Add new function here, act on parsed []string from now on, not sanitized - return -} 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/readline/vimdelete.go b/readline/vimdelete.go index 7a07259..f5c1806 100644 --- a/readline/vimdelete.go +++ b/readline/vimdelete.go @@ -142,6 +142,10 @@ func (rl *Instance) viDeleteByAdjust(adjust int) { rl.updateHelpers() } +func (rl *Instance) DeleteByAmount(adjust int) { + rl.viDeleteByAdjust(adjust) +} + func (rl *Instance) vimDeleteToken(r rune) bool { tokens, _, _ := tokeniseSplitSpaces(rl.line, 0) pos := int(r) - 48 // convert ASCII to integer diff --git a/runnermode.go b/runnermode.go index 55adfdc..9e7a3ff 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. @@ -51,9 +53,7 @@ end) */ 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() @@ -62,43 +62,6 @@ func runnerModeLoader(rtm *rt.Runtime) *rt.Table { return mod } -// #interface runner -// setMode(cb) -// This is the same as the `hilbish.runnerMode` function. -// It takes a callback, which will be used to execute all interactive input. -// In normal cases, neither callbacks should be overrided by the user, -// as the higher level functions listed below this will handle it. -// #param cb function -func _runnerMode() {} - -// #interface runner -// sh(cmd) -// Runs a command in Hilbish's shell script interpreter. -// This is the equivalent of using `source`. -// #param cmd string -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 - } - - _, exitCode, cont, err := execSh(aliases.Resolve(cmd)) - var luaErr rt.Value = rt.NilValue - if err != nil { - luaErr = rt.StringValue(err.Error()) - } - runnerRet := rt.NewTable() - 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("err"), luaErr) - - return c.PushingNext(t.Runtime, rt.TableValue(runnerRet)), nil -} - // #interface runner // lua(cmd) // Evaluates `cmd` as Lua input. This is the same as using `dofile` 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/testplugin/testplugin.go b/testplugin/testplugin.go deleted file mode 100644 index 2d8a41b..0000000 --- a/testplugin/testplugin.go +++ /dev/null @@ -1,9 +0,0 @@ -package main - -import ( - rt "github.com/arnodel/golua/runtime" -) - -func Loader(rtm *rt.Runtime) rt.Value { - return rt.StringValue("hello world!") -} diff --git a/testplugin/testplugin.so b/testplugin/testplugin.so deleted file mode 100644 index 3c83992..0000000 Binary files a/testplugin/testplugin.so and /dev/null differ diff --git a/sink.go b/util/sink.go similarity index 68% rename from sink.go rename to util/sink.go index 3aa5507..5abd954 100644 --- a/sink.go +++ b/util/sink.go @@ -1,35 +1,32 @@ -package main +package util import ( "bufio" + "bytes" "fmt" "io" "os" "strings" - "hilbish/util" - rt "github.com/arnodel/golua/runtime" ) var sinkMetaKey = rt.StringValue("hshsink") // #type -// A sink is a structure that has input and/or output to/from -// a desination. -type sink struct{ - writer *bufio.Writer - reader *bufio.Reader +// A sink is a structure that has input and/or output to/from a desination. +type Sink struct{ + Rw *bufio.ReadWriter file *os.File - ud *rt.UserData + UserData *rt.UserData autoFlush bool } -func setupSinkType(rtm *rt.Runtime) { +func SinkLoader(rtm *rt.Runtime) *rt.Table { sinkMeta := rt.NewTable() sinkMethods := rt.NewTable() - sinkFuncs := map[string]util.LuaExport{ + sinkFuncs := map[string]LuaExport{ "flush": {luaSinkFlush, 1, false}, "read": {luaSinkRead, 1, false}, "readAll": {luaSinkReadAll, 1, false}, @@ -37,7 +34,7 @@ func setupSinkType(rtm *rt.Runtime) { "write": {luaSinkWrite, 2, false}, "writeln": {luaSinkWriteln, 2, false}, } - util.SetExports(l, sinkMethods, sinkFuncs) + SetExports(rtm, sinkMethods, sinkFuncs) sinkIndex := func(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { s, _ := sinkArg(c, 0) @@ -64,10 +61,25 @@ func setupSinkType(rtm *rt.Runtime) { } sinkMeta.Set(rt.StringValue("__index"), rt.FunctionValue(rt.NewGoFunction(sinkIndex, "__index", 2, false))) - l.SetRegistry(sinkMetaKey, rt.TableValue(sinkMeta)) + rtm.SetRegistry(sinkMetaKey, rt.TableValue(sinkMeta)) + + exports := map[string]LuaExport{ + "new": {luaSinkNew, 0, false}, + } + + mod := rt.NewTable() + SetExports(rtm, mod, exports) + + return mod } +func luaSinkNew(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + snk := NewSink(t.Runtime, new(bytes.Buffer)) + + return c.PushingNext1(t.Runtime, rt.UserDataValue(snk.UserData)), nil +} + // #member // readAll() -> string // --- @returns string @@ -82,11 +94,17 @@ func luaSinkReadAll(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return nil, err } + if s.autoFlush { + s.Rw.Flush() + } + lines := []string{} for { - line, err := s.reader.ReadString('\n') + line, err := s.Rw.ReadString('\n') if err != nil { if err == io.EOF { + // We still want to add the data we read + lines = append(lines, line) break } @@ -113,7 +131,7 @@ func luaSinkRead(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return nil, err } - str, _ := s.reader.ReadString('\n') + str, _ := s.Rw.ReadString('\n') return c.PushingNext1(t.Runtime, rt.StringValue(str)), nil } @@ -135,9 +153,9 @@ func luaSinkWrite(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return nil, err } - s.writer.Write([]byte(data)) + s.Rw.Write([]byte(data)) if s.autoFlush { - s.writer.Flush() + s.Rw.Flush() } return c.Next(), nil @@ -160,9 +178,9 @@ func luaSinkWriteln(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return nil, err } - s.writer.Write([]byte(data + "\n")) + s.Rw.Write([]byte(data + "\n")) if s.autoFlush { - s.writer.Flush() + s.Rw.Flush() } return c.Next(), nil @@ -181,7 +199,7 @@ func luaSinkFlush(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return nil, err } - s.writer.Flush() + s.Rw.Flush() return c.Next(), nil } @@ -212,11 +230,25 @@ func luaSinkAutoFlush(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.Next(), nil } -func newSinkInput(r io.Reader) *sink { - s := &sink{ - reader: bufio.NewReader(r), +func NewSink(rtm *rt.Runtime, Rw io.ReadWriter) *Sink { + s := &Sink{ + Rw: bufio.NewReadWriter(bufio.NewReader(Rw), bufio.NewWriter(Rw)), + autoFlush: true, } - s.ud = sinkUserData(s) + s.UserData = sinkUserData(rtm, s) + + if f, ok := Rw.(*os.File); ok { + s.file = f + } + + return s +} + +func NewSinkInput(rtm *rt.Runtime, r io.Reader) *Sink { + s := &Sink{ + Rw: bufio.NewReadWriter(bufio.NewReader(r), nil), + } + s.UserData = sinkUserData(rtm, s) if f, ok := r.(*os.File); ok { s.file = f @@ -225,17 +257,17 @@ func newSinkInput(r io.Reader) *sink { return s } -func newSinkOutput(w io.Writer) *sink { - s := &sink{ - writer: bufio.NewWriter(w), +func NewSinkOutput(rtm *rt.Runtime, w io.Writer) *Sink { + s := &Sink{ + Rw: bufio.NewReadWriter(nil, bufio.NewWriter(w)), autoFlush: true, } - s.ud = sinkUserData(s) + s.UserData = sinkUserData(rtm, s) return s } -func sinkArg(c *rt.GoCont, arg int) (*sink, error) { +func sinkArg(c *rt.GoCont, arg int) (*Sink, error) { s, ok := valueToSink(c.Arg(arg)) if !ok { return nil, fmt.Errorf("#%d must be a sink", arg + 1) @@ -244,17 +276,17 @@ func sinkArg(c *rt.GoCont, arg int) (*sink, error) { return s, nil } -func valueToSink(val rt.Value) (*sink, bool) { +func valueToSink(val rt.Value) (*Sink, bool) { u, ok := val.TryUserData() if !ok { return nil, false } - s, ok := u.Value().(*sink) + s, ok := u.Value().(*Sink) return s, ok } -func sinkUserData(s *sink) *rt.UserData { - sinkMeta := l.Registry(sinkMetaKey) +func sinkUserData(rtm *rt.Runtime, s *Sink) *rt.UserData { + sinkMeta := rtm.Registry(sinkMetaKey) return rt.NewUserData(s, sinkMeta.AsTable()) } diff --git a/util/streams.go b/util/streams.go new file mode 100644 index 0000000..11f9308 --- /dev/null +++ b/util/streams.go @@ -0,0 +1,11 @@ +package util + +import ( + "io" +) + +type Streams struct { + Stdout io.Writer + Stderr io.Writer + Stdin io.Reader +} diff --git a/util/util.go b/util/util.go index 0fcd4b0..b32d865 100644 --- a/util/util.go +++ b/util/util.go @@ -2,14 +2,78 @@ package util import ( "bufio" + "context" + "errors" + "fmt" "io" + "path/filepath" "strings" "os" + "os/exec" "os/user" + "runtime" + "syscall" rt "github.com/arnodel/golua/runtime" ) +var ErrNotExec = errors.New("not executable") +var ErrNotFound = errors.New("not found") + +type ExecError struct{ + Typ string + Cmd string + Code int + Colon bool + Err error +} + +func (e ExecError) Error() string { + return fmt.Sprintf("%s: %s", e.Cmd, e.Typ) +} + +func (e ExecError) sprint() error { + sep := " " + if e.Colon { + sep = ": " + } + + return fmt.Errorf("hilbish: %s%s%s", e.Cmd, sep, e.Err.Error()) +} + +func IsExecError(err error) (ExecError, bool) { + if exErr, ok := err.(ExecError); ok { + return exErr, true + } + + fields := strings.Split(err.Error(), ": ") + knownTypes := []string{ + "not-found", + "not-executable", + } + + if len(fields) > 1 && Contains(knownTypes, fields[1]) { + var colon bool + var e error + switch fields[1] { + case "not-found": + e = ErrNotFound + case "not-executable": + colon = true + e = ErrNotExec + } + + return ExecError{ + Cmd: fields[0], + Typ: fields[1], + Colon: colon, + Err: e, + }, true + } + + return ExecError{}, false +} + // 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(rtm *rt.Runtime, module *rt.Table, field string, value rt.Value) { @@ -36,6 +100,15 @@ func DoString(rtm *rt.Runtime, code string) (rt.Value, error) { return ret, err } +func MustDoString(rtm *rt.Runtime, code string) rt.Value { + val, err := DoString(rtm, code) + if err != nil { + panic(err) + } + + return val +} + // DoFile runs the contents of the file in the Lua runtime. func DoFile(rtm *rt.Runtime, path string) error { f, err := os.Open(path) @@ -141,3 +214,67 @@ func AbbrevHome(path string) string { return path } + +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:"} + } else { + skip = []string{"./", "/", "../", "~/"} + } + for _, s := range skip { + if strings.HasPrefix(file, s) { + 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 + } else if err == nil { + return path, nil + } + } + + return "", os.ErrNotExist +} + +func Contains(s []string, e string) bool { + for _, a := range s { + if strings.ToLower(a) == strings.ToLower(e) { + return true + } + } + return false +} + +func HandleExecErr(err error) (exit uint8) { + ctx := context.TODO() + + 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 + } + exit = uint8(128 + status.Signal()) + return + } + exit = uint8(status.ExitStatus()) + return + } + exit = 1 + return + case *exec.Error: + // did not start + //fmt.Fprintf(hc.Stderr, "%v\n", err) + exit = 127 + default: return + } + + return +} diff --git a/execfile_unix.go b/util/util_unix.go similarity index 52% rename from execfile_unix.go rename to util/util_unix.go index 44f924a..92813c8 100644 --- a/execfile_unix.go +++ b/util/util_unix.go @@ -1,17 +1,12 @@ -// +build linux darwin +//go:build unix -package main +package util import ( "os" - "syscall" ) -var bgProcAttr *syscall.SysProcAttr = &syscall.SysProcAttr{ - Setpgid: true, -} - -func findExecutable(path string, inPath, dirs bool) error { +func FindExecutable(path string, inPath, dirs bool) error { f, err := os.Stat(path) if err != nil { return err @@ -25,5 +20,5 @@ func findExecutable(path string, inPath, dirs bool) error { return nil } } - return errNotExec + return ErrNotExec } diff --git a/execfile_windows.go b/util/util_windows.go similarity index 54% rename from execfile_windows.go rename to util/util_windows.go index 4b3feef..3321033 100644 --- a/execfile_windows.go +++ b/util/util_windows.go @@ -1,18 +1,13 @@ -// +build windows +//go:build windows -package main +package util import ( "path/filepath" "os" - "syscall" ) -var bgProcAttr *syscall.SysProcAttr = &syscall.SysProcAttr{ - CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP, -} - -func findExecutable(path string, inPath, dirs bool) error { +func FindExecutable(path string, inPath, dirs bool) error { nameExt := filepath.Ext(path) pathExts := filepath.SplitList(os.Getenv("PATHEXT")) if inPath { @@ -26,15 +21,15 @@ func findExecutable(path string, inPath, dirs bool) error { } else { _, err := os.Stat(path) if err == nil { - if contains(pathExts, nameExt) { return nil } - return errNotExec + if Contains(pathExts, nameExt) { return nil } + return ErrNotExec } } } else { _, err := os.Stat(path) if err == nil { - if contains(pathExts, nameExt) { return nil } - return errNotExec + if Contains(pathExts, nameExt) { return nil } + return ErrNotExec } } diff --git a/vars.go b/vars.go index 1be257c..84dbc0a 100644 --- a/vars.go +++ b/vars.go @@ -11,8 +11,8 @@ var ( // Version info var ( - ver = "v2.2.3" - releaseName = "Poppy" + ver = "v3.0.0" + releaseName = "Hyacinth" gitCommit string gitBranch string diff --git a/vars_darwin.go b/vars_darwin.go index 8ec83ba..be7a4c1 100644 --- a/vars_darwin.go +++ b/vars_darwin.go @@ -1,4 +1,4 @@ -// +build darwin +//go:build darwin package main @@ -15,7 +15,5 @@ var ( .. hilbish.userDir.config .. '/hilbish/?/?.lua;' .. hilbish.userDir.config .. '/hilbish/?.lua'` dataDir = "/usr/local/share/hilbish" - preloadPath = dataDir + "/nature/init.lua" - sampleConfPath = dataDir + "/.hilbishrc.lua" // Path to default/sample config defaultConfDir = getenv("XDG_CONFIG_HOME", "~/.config") ) diff --git a/vars_linux.go b/vars_unix.go similarity index 76% rename from vars_linux.go rename to vars_unix.go index e1160ba..6bf47f5 100644 --- a/vars_linux.go +++ b/vars_unix.go @@ -1,4 +1,4 @@ -// +build linux +//go:build unix && !darwin package main @@ -14,8 +14,6 @@ var ( .. hilbish.userDir.config .. '/hilbish/?/init.lua;' .. hilbish.userDir.config .. '/hilbish/?/?.lua;' .. hilbish.userDir.config .. '/hilbish/?.lua'` - dataDir = "/usr/local/share/hilbish" - preloadPath = dataDir + "/nature/init.lua" - sampleConfPath = dataDir + "/.hilbishrc.lua" // Path to default/sample config + dataDir = "" defaultConfDir = "" ) diff --git a/vars_windows.go b/vars_windows.go index d1bd7b6..4c46539 100644 --- a/vars_windows.go +++ b/vars_windows.go @@ -1,4 +1,4 @@ -// +build windows +//go:build windows package main @@ -10,8 +10,6 @@ var ( .. hilbish.userDir.config .. '\\Hilbish\\libs\\?\\init.lua;' .. hilbish.userDir.config .. '\\Hilbish\\libs\\?\\?.lua;' .. hilbish.userDir.config .. '\\Hilbish\\libs\\?.lua;'` - dataDir = util.ExpandHome("~\\Appdata\\Roaming\\Hilbish") // ~ and \ gonna cry? - preloadPath = dataDir + "\\nature\\init.lua" - sampleConfPath = dataDir + "\\.hilbishrc.lua" // Path to default/sample config + dataDir = util.ExpandHome("~\\Appdata\\Roaming\\Hilbish") // ~ and \, gonna cry? defaultConfDir = "" ) 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.