+
+
+dirs.push(dir)
+
+
+
+
+
+Add `dir` to the recent directories list.
+#### Parameters
+`dir` **`string`**
+
+
+
+
+
+
+
+
+
+
+dirs.setOld(d)
+
+
+
+
+
+Sets the old directory string.
+#### Parameters
+`d` **`string`**
+
+
diff --git a/docs/nature/doc.md b/docs/nature/doc.md
new file mode 100644
index 00000000..f940c0d3
--- /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.|
+
+
+
+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`**
+
+
+
+
+
+
+
+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`**
+
+
+
+
diff --git a/editor.go b/editor.go
index 67248ff2..cae15921 100644
--- a/editor.go
+++ b/editor.go
@@ -13,11 +13,12 @@ import (
func editorLoader(rtm *rt.Runtime) *rt.Table {
exports := map[string]util.LuaExport{
/*
- "insert": {editorInsert, 1, false},
- "setVimRegister": {editorSetRegister, 1, false},
- "getVimRegister": {editorGetRegister, 2, false},
- "getLine": {editorGetLine, 0, false},
- "readChar": {editorReadChar, 0, false},
+ "insert": {editorInsert, 1, false},
+ "setVimRegister": {editorSetRegister, 1, false},
+ "getVimRegister": {editorGetRegister, 2, false},
+ "getLine": {editorGetLine, 0, false},
+ "readChar": {editorReadChar, 0, false},
+ "deleteByAmount": {editorDeleteByAmount, 1, false},
*/
}
@@ -49,7 +50,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 {
@@ -108,3 +109,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 b80a660e..a2935bba 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 00000000..94c84dfb
--- /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 00000000..9f8d634f
--- /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 0828e01c..2496e5cc 100644
--- a/exec.go
+++ b/exec.go
@@ -1,540 +1,36 @@
package main
import (
- "bytes"
- "context"
"errors"
- "os/exec"
- "fmt"
- "io"
"os"
- "os/signal"
- "path/filepath"
- "runtime"
"strings"
- "syscall"
- "time"
-
- "hilbish/moonlight"
- //"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 moonlight.Value = moonlight.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() == moonlight.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)
+ /*
+ 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 {
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
- }
- }
-
- if err != nil {
- if exErr, ok := isExecError(err); ok {
- hooks.Emit("command." + exErr.typ, exErr.cmd)
- } else {
- 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 moonlight.Value, userInput string) (input string, exitCode uint8, continued bool, runnerErr, err error) {
- runnerRet, err := l.Call1(runr, moonlight.StringValue(userInput))
- if err != nil {
- return "", 124, false, nil, err
- }
-
- var runner *moonlight.Table
- var ok bool
- if runner, ok = moonlight.TryTable(runnerRet); !ok {
- fmt.Fprintln(os.Stderr, "runner did not return a table")
- exitCode = 125
- input = userInput
- return
- }
-
- if code, ok := runner.Get(moonlight.StringValue("exitCode")).TryInt(); ok {
- exitCode = uint8(code)
- }
-
- if inp, ok := runner.Get(moonlight.StringValue("input")).TryString(); ok {
- input = inp
- }
-
- if errStr, ok := runner.Get(moonlight.StringValue("err")).TryString(); ok {
- runnerErr = fmt.Errorf("%s", errStr)
- }
-
- if c, ok := runner.Get(moonlight.StringValue("continue")).TryBool(); ok {
- continued = c
- }
- return
-}
-
-func handleSh(cmdString string) (input string, exitCode uint8, cont bool, runErr error) {
- shRunner := hshMod.Get(moonlight.StringValue("runner")).AsTable().Get(moonlight.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
- }
-
- 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))
-
- //t := rt.NewThread(l)
- sig := make(chan os.Signal)
- exit := make(chan bool)
-
- luaexitcode := rt.IntValue(63)
- var err error
- go func() {
- defer func() {
- if r := recover(); r != nil {
- exit <- true
- }
- }()
-
- signal.Notify(sig, os.Interrupt)
- select {
- case <-sig:
- //t.KillContext()
- return
- }
-
- }()
-
- go func() {
- // TODO: call in thread function?
- //luaexitcode, err = l.CallInThread1(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.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 == '"' {
@@ -550,22 +46,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)
}
@@ -577,11 +57,3 @@ func splitInput(input string) ([]string, string) {
return cmdArgs, cmdstr.String()
}
-
-func cmdFinish(code uint8, cmdstr string, private bool) {
- hshMod.SetField("exitCode", moonlight.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", moonlight.IntValue(int64(code)), cmdstr, private)
-}
diff --git a/go.mod b/go.mod
index 81ff8d17..a779056c 100644
--- a/go.mod
+++ b/go.mod
@@ -29,7 +29,7 @@ require (
golang.org/x/text v0.26.0 // indirect
)
-replace mvdan.cc/sh/v3 => github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20240720131751-805c301321fd
+replace mvdan.cc/sh/v3 => github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20240815163633-562273e09b73
replace github.com/maxlandon/readline => ./readline
diff --git a/go.sum b/go.sum
index 0ed45646..47e67c7c 100644
--- a/go.sum
+++ b/go.sum
@@ -1,38 +1,19 @@
-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/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.20240720131751-805c301321fd h1:THNle0FR2g7DMO1y3Bx1Zr7rYeiLXt3st3UkxEsMzL4=
-github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20240720131751-805c301321fd/go.mod h1:YZalN5H7WNQw3DGij6IvHsEhn5YMW7M2FCwG6gnfKy4=
-github.com/aarzilli/golua v0.0.0-20210507130708-11106aa57765 h1:N6gB4UCRBZz8twlJbMFiCKj0zX5Et2nFU/LRafT4x80=
-github.com/aarzilli/golua v0.0.0-20210507130708-11106aa57765/go.mod h1:hMjfaJVSqVnxenMlsxrq3Ni+vrm9Hs64tU4M7dhUoO4=
+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/aarzilli/golua v0.0.0-20250217091409-248753f411c4 h1:gW5i3FQAMcbkNgo/A87gCKAbBMalAO8BlPIMo9Gk2Ow=
github.com/aarzilli/golua v0.0.0-20250217091409-248753f411c4/go.mod h1:hMjfaJVSqVnxenMlsxrq3Ni+vrm9Hs64tU4M7dhUoO4=
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=
github.com/arnodel/strftime v0.1.6/go.mod h1:5NbK5XqYK8QpRZpqKNt4OlxLtIB8cotkLk4KTKzJfWs=
-github.com/atsushinee/go-markdown-generator v0.0.0-20191121114853-83f9e1f68504 h1:R1/AOzdMbopSliUTTEHvHbyNmnZ3YxY5GvdhTkpPsSY=
-github.com/atsushinee/go-markdown-generator v0.0.0-20191121114853-83f9e1f68504/go.mod h1:kHBCvAXJIatTX1pw6tLiOspjGc3MhUDRlog9yrCUS+k=
github.com/atsushinee/go-markdown-generator v0.0.0-20231027094725-92d26ffbe778 h1:iBzH7EQLFyjkpwXihHWf7QbbzfYfxAlyP4pTjCJbnMw=
github.com/atsushinee/go-markdown-generator v0.0.0-20231027094725-92d26ffbe778/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/blackfireio/osinfo v1.1.0 h1:1LMkMiFL42+Brx7r3MKuf7UTlXBRgebFLJQAfoFafj8=
github.com/blackfireio/osinfo v1.1.0/go.mod h1:Pd987poVNmd5Wsx6PRPw4+w7kLlf9iJxoRKPtPAjOrA=
-github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
-github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/evilsocket/islazy v1.11.0 h1:B5w6uuS6ki6iDG+aH/RFeoMb8ijQh/pGabewqp2UeJ0=
github.com/evilsocket/islazy v1.11.0/go.mod h1:muYH4x5MB5YRdkxnrOtrXLIBX6LySj1uFIqys94LKdo=
-github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
-github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
-github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
-github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
-github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
@@ -42,23 +23,13 @@ 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/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.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
-github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
-golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
-golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
-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/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
-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/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
diff --git a/golibs/bait/bait.go b/golibs/bait/bait.go
index 6f3edf6b..b01bb128 100644
--- a/golibs/bait/bait.go
+++ b/golibs/bait/bait.go
@@ -48,7 +48,7 @@ type Recoverer func(event string, handler *Listener, err interface{})
type Listener struct {
typ listenerType
once bool
- caller func(...interface{})
+ caller func(...interface{}) rt.Value
luaCaller *moonlight.Closure
}
@@ -69,10 +69,11 @@ func New(rtm *moonlight.Runtime) *Bait {
}
// Emit throws an event.
-func (b *Bait) Emit(event string, args ...interface{}) {
+func (b *Bait) Emit(event string, args ...interface{}) []rt.Value {
+ var returns []rt.Value
handles := b.handlers[event]
if handles == nil {
- return
+ return nil
}
for idx, handle := range handles {
@@ -96,29 +97,38 @@ func (b *Bait) Emit(event string, args ...interface{}) {
luaArgs = append(luaArgs, luarg)
}
/*
- _, err := b.rtm.Call1(funcVal, luaArgs...)
+ luaRet, err := rt.Call1(b.rtm.MainThread(), funcVal, luaArgs...)
if err != nil {
if event != "error" {
b.Emit("error", event, handle.luaCaller, err.Error())
- return
+ return nil
}
// if there is an error in an error event handler, panic instead
// (calls the go recoverer function)
panic(err)
}
+
+ if luaRet != rt.NilValue {
+ returns = append(returns, luaRet)
+ }
*/
} else {
- handle.caller(args...)
+ ret := handle.caller(args...)
+ if ret != rt.NilValue {
+ returns = append(returns, ret)
+ }
}
if handle.once {
b.removeListener(event, idx)
}
}
+
+ return returns
}
// On adds a Go function handler for an event.
-func (b *Bait) On(event string, handler func(...interface{})) *Listener {
+func (b *Bait) On(event string, handler func(...interface{}) rt.Value) *Listener {
listener := &Listener{
typ: goListener,
caller: handler,
@@ -162,7 +172,7 @@ func (b *Bait) OffLua(event string, handler *moonlight.Closure) {
}
// Once adds a Go function listener for an event that only runs once.
-func (b *Bait) Once(event string, handler func(...interface{})) *Listener {
+func (b *Bait) Once(event string, handler func(...interface{}) rt.Value) *Listener {
listener := &Listener{
typ: goListener,
once: true,
@@ -227,29 +237,6 @@ func (b *Bait) Loader(rtm *moonlight.Runtime) moonlight.Value {
return moonlight.TableValue(mod)
}
-func handleHook(t *rt.Thread, c *rt.GoCont, name string, catcher *rt.Closure, args ...interface{}) {
- funcVal := rt.FunctionValue(catcher)
- var luaArgs []rt.Value
- for _, arg := range args {
- var luarg rt.Value
- switch arg.(type) {
- case rt.Value:
- luarg = arg.(rt.Value)
- default:
- luarg = rt.AsValue(arg)
- }
- luaArgs = append(luaArgs, luarg)
- }
- _, err := rt.Call1(t, funcVal, luaArgs...)
- if err != nil {
- e := rt.NewError(rt.StringValue(err.Error()))
- e = e.AddContext(c.Next(), 1)
- // panicking here won't actually cause hilbish to panic and instead will
- // print the error and remove the hook (look at emission recover from above)
- panic(e)
- }
-}
-
// catch(name, cb)
// Catches an event. This function can be used to act on events.
// #param name string The name of the hook.
@@ -378,8 +365,8 @@ func (b *Bait) bthrow(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
for i, v := range c.Etc() {
ifaceSlice[i] = v
}
- b.Emit(name, ifaceSlice...)
+ ret := b.Emit(name, ifaceSlice...)
- return c.Next(), nil
+ return c.PushingNext(t.Runtime, ret...), nil
}
*/
diff --git a/golibs/fs/fs.go b/golibs/fs/fs.go
index 10d943a4..99f97542 100644
--- a/golibs/fs/fs.go
+++ b/golibs/fs/fs.go
@@ -19,38 +19,26 @@ import (
"github.com/arnodel/golua/lib/iolib"
rt "github.com/arnodel/golua/runtime"
- "mvdan.cc/sh/v3/interp"
)
-type fs struct {
- runner *interp.Runner
-}
-
-func New(runner *interp.Runner) *fs {
- return &fs{
- runner: runner,
- }
-}
-
-func (f *fs) Loader(rtm *moonlight.Runtime) moonlight.Value {
- println("fs loader called")
+func Loader(mlr *moonlight.Runtime) moonlight.Value {
exports := map[string]moonlight.Export{
/*
- "cd": util.LuaExport{f.fcd, 1, false},
- "mkdir": util.LuaExport{f.fmkdir, 2, false},
- "stat": util.LuaExport{f.fstat, 1, false},
- "abs": util.LuaExport{f.fabs, 1, false},
- "basename": util.LuaExport{f.fbasename, 1, false},
- "glob": util.LuaExport{f.fglob, 1, false},
- "join": util.LuaExport{f.fjoin, 0, true},
- "pipe": util.LuaExport{f.fpipe, 0, false},
+ "cd": util.LuaExport{fcd, 1, false},
+ "mkdir": util.LuaExport{fmkdir, 2, false},
+ "stat": util.LuaExport{fstat, 1, false},
+ "abs": util.LuaExport{fabs, 1, false},
+ "basename": util.LuaExport{fbasename, 1, false},
+ "glob": util.LuaExport{fglob, 1, false},
+ "join": util.LuaExport{fjoin, 0, true},
+ "pipe": util.LuaExport{fpipe, 0, false},
*/
- "readdir": {f.freaddir, 1, false},
- "dir": {f.fdir, 1, false},
+ "dir": {fdir, 1, false},
+ "readdir": {freaddir, 1, false},
}
mod := moonlight.NewTable()
- rtm.SetExports(mod, exports)
+ mlr.SetExports(mod, exports)
mod.SetField("pathSep", moonlight.StringValue(string(os.PathSeparator)))
mod.SetField("pathListSep", moonlight.StringValue(string(os.PathListSeparator)))
@@ -63,7 +51,7 @@ func (f *fs) Loader(rtm *moonlight.Runtime) moonlight.Value {
// This can be used to resolve short paths like `..` to `/home/user`.
// #param path string
// #returns string
-func (f *fs) fabs(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
+func fabs(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
path, err := c.StringArg(0)
if err != nil {
return nil, err
@@ -83,7 +71,7 @@ func (f *fs) fabs(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
// `.` will be returned.
// #param path string Path to get the base name of.
// #returns string
-func (f *fs) fbasename(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
+func fbasename(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
@@ -98,23 +86,33 @@ func (f *fs) fbasename(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
// cd(dir)
// Changes Hilbish's directory to `dir`.
// #param dir string Path to change directory to.
-func (f *fs) fcd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
- if err := c.Check1Arg(); err != nil {
- return nil, err
+func fcd(mlr *moonlight.Runtime) error {
+ if err := mlr.Check1Arg(); err != nil {
+ return err
}
- path, err := c.StringArg(0)
+ path, err := mlr.StringArg(0)
if err != nil {
- return nil, err
+ return err
}
path = util.ExpandHome(strings.TrimSpace(path))
+ oldWd, _ := os.Getwd()
+
+ abspath, err := filepath.Abs(path)
+ if err != nil {
+ return err
+ }
err = os.Chdir(path)
if err != nil {
- return nil, err
+ return err
}
- interp.Dir(path)(f.runner)
- return c.Next(), err
+ mlr.DoString(fmt.Sprintf(`
+ local bait = require 'bait'
+ bait.throw('hilbish.cd', '%s', '%s')
+ `, abspath, oldWd))
+
+ return err
}
// dir(path) -> string
@@ -122,7 +120,7 @@ func (f *fs) fcd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
// `~/Documents/doc.txt` then this function will return `~/Documents`.
// #param path string Path to get the directory for.
// #returns string
-func (f *fs) fdir(mlr *moonlight.Runtime) error {
+func fdir(mlr *moonlight.Runtime) error {
if err := mlr.Check1Arg(); err != nil {
return err
}
@@ -154,7 +152,7 @@ print(matches)
-- -> {'init.lua', 'code.lua'}
#example
*/
-func (f *fs) fglob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
+func fglob(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
@@ -188,7 +186,7 @@ print(fs.join(hilbish.userDir.config, 'hilbish'))
-- -> '/home/user/.config/hilbish' on Linux
#example
*/
-func (f *fs) fjoin(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
+func fjoin(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
strs := make([]string, len(c.Etc()))
for i, v := range c.Etc() {
if v.Type() != rt.StringType {
@@ -215,7 +213,7 @@ func (f *fs) fjoin(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
fs.mkdir('./foo/bar', true)
#example
*/
-func (f *fs) fmkdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
+func fmkdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.CheckNArgs(2); err != nil {
return nil, err
}
@@ -246,7 +244,7 @@ func (f *fs) fmkdir(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
// The type returned is a Lua file, same as returned from `io` functions.
// #returns File
// #returns File
-func (f *fs) fpipe(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
+func fpipe(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
rf, wf, err := os.Pipe()
if err != nil {
return nil, err
@@ -262,7 +260,7 @@ func (f *fs) fpipe(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
// Returns a list of all files and directories in the provided path.
// #param dir string
// #returns table
-func (f *fs) freaddir(mlr *moonlight.Runtime) error {
+func freaddir(mlr *moonlight.Runtime) error {
if err := mlr.Check1Arg(); err != nil {
return err
}
@@ -311,7 +309,7 @@ Would print the following:
]]--
#example
*/
-func (f *fs) fstat(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
+func fstat(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
if err := c.Check1Arg(); err != nil {
return nil, err
}
diff --git a/golibs/snail/lua.go b/golibs/snail/lua.go
new file mode 100644
index 00000000..8546a7c4
--- /dev/null
+++ b/golibs/snail/lua.go
@@ -0,0 +1,223 @@
+// 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/moonlight"
+ "hilbish/util"
+
+ "github.com/arnodel/golua/lib/iolib"
+ rt "github.com/arnodel/golua/runtime"
+ "mvdan.cc/sh/v3/interp"
+ "mvdan.cc/sh/v3/syntax"
+)
+
+var snailMetaKey = rt.StringValue("hshsnail")
+
+func Loader(mlr *moonlight.Runtime) moonlight.Value {
+ snailMeta := moonlight.NewTable()
+ snailMethods := moonlight.NewTable()
+ snailFuncs := map[string]moonlight.Export{
+ "run": {snailrun, 3, false},
+ "dir": {snaildir, 2, false},
+ }
+ mlr.SetExports(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)))
+ mlr.SetRegistry(snailMetaKey, moonlight.TableValue(snailMeta))
+
+ exports := map[string]moonlight.Export{
+ "new": {snailnew, 0, false},
+ }
+
+ mod := moonlight.NewTable()
+ mlr.SetExports(mod, exports)
+
+ return moonlight.TableValue(mod)
+}
+
+// new() -> @Snail
+// Creates a new Snail instance.
+func snailnew(mlr *moonlight.Runtime) error {
+ s := New(mlr)
+
+ mlr.PushNext1(moonlight.UserDataValue(snailUserData(s)))
+ return 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(mlr *moonlight.Runtime) error {
+ if err := mlr.CheckNArgs(2); err != nil {
+ return err
+ }
+
+ s, err := snailArg(mlr, 0)
+ if err != nil {
+ return err
+ }
+
+ cmd, err := mlr.StringArg(1)
+ if err != nil {
+ return err
+ }
+
+ streams := &util.Streams{}
+ thirdArg := mlr.Arg(2)
+ switch thirdArg.Type() {
+ case moonlight.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 moonlight.NilType: // noop
+ default:
+ return errors.New("expected 3rd arg to be a table")
+ }
+
+ var newline bool
+ var cont bool
+ var luaErr moonlight.Value = moonlight.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 = moonlight.StringValue(err.Error())
+ }
+ }
+ }
+
+ runnerRet := moonlight.NewTable()
+ runnerRet.SetField("input", moonlight.StringValue(cmd))
+ runnerRet.SetField("exitCode", moonlight.IntValue(int64(exitCode)))
+ runnerRet.SetField("continue", moonlight.BoolValue(cont))
+ runnerRet.SetField("newline", moonlight.BoolValue(newline))
+ runnerRet.SetField("err", luaErr)
+ runnerRet.SetField("bg", moonlight.BoolValue(bg))
+
+ mlr.PushNext1(moonlight.TableValue(runnerRet))
+ return 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(mlr *moonlight.Runtime) error {
+ if err := mlr.CheckNArgs(2); err != nil {
+ return err
+ }
+
+ s, err := snailArg(mlr, 0)
+ if err != nil {
+ return err
+ }
+
+ dir, err := mlr.StringArg(1)
+ if err != nil {
+ return err
+ }
+
+ interp.Dir(dir)(s.runner)
+ return 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(mlr *moonlight.Runtime, arg int) (*Snail, error) {
+ s, ok := valueToSnail(mlr.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) *moonlight.UserData {
+ snailMeta := s.runtime.Registry(snailMetaKey)
+ return moonlight.NewUserData(s, moonlight.ToTable(s.runtime, snailMeta))
+}
diff --git a/golibs/snail/snail.go b/golibs/snail/snail.go
new file mode 100644
index 00000000..48bee2ed
--- /dev/null
+++ b/golibs/snail/snail.go
@@ -0,0 +1,308 @@
+package snail
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "os/signal"
+ "runtime"
+ "strings"
+ "time"
+
+ "hilbish/moonlight"
+ "hilbish/util"
+
+ rt "github.com/arnodel/golua/runtime"
+ "mvdan.cc/sh/v3/shell"
+
+ //"github.com/yuin/gopher-lua/parse"
+ "mvdan.cc/sh/v3/expand"
+ "mvdan.cc/sh/v3/interp"
+ "mvdan.cc/sh/v3/syntax"
+)
+
+// #type
+// A Snail is a shell script interpreter instance.
+type Snail struct {
+ runner *interp.Runner
+ runtime *moonlight.Runtime
+}
+
+func New(mlr *moonlight.Runtime) *Snail {
+ runner, _ := interp.New()
+
+ return &Snail{
+ runner: runner,
+ runtime: mlr,
+ }
+}
+
+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()
+
+ replacer := strings.NewReplacer("[", "\\[", "]", "\\]")
+
+ 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, _ := s.runtime.DoString("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 = s.runtime.MustDoString(fmt.Sprintf(`return hilbish.aliases.resolve [[%s]]`, replacer.Replace(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 := s.runtime.MustDoString("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/job.go b/job.go
index 408619ba..ba290dd4 100644
--- a/job.go
+++ b/job.go
@@ -57,8 +57,8 @@ func (j *job) start() error {
}
j.setHandle(&cmd)
}
- // bgProcAttr is defined in execfile_
.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
@@ -137,7 +137,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 0a038b12..2caa4aea 100644
--- a/job_unix.go
+++ b/job_unix.go
@@ -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 26818b5c..1ac46468 100644
--- a/job_windows.go
+++ b/job_windows.go
@@ -4,8 +4,13 @@ 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 64b35385..8306418d 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"
"hilbish/moonlight"
@@ -17,33 +19,25 @@ var minimalconf = `hilbish.prompt '& '`
func luaInit() {
l = moonlight.NewRuntime()
- setupSinkType()
l.LoadLibrary(hilbishLoader, "hilbish")
// yes this is stupid, i know
l.DoString("hilbish = require 'hilbish'")
- // Add fs and terminal module module to Lua
- f := fs.New(runner)
- l.LoadLibrary(f.Loader, "fs")
-
- l.LoadLibrary(terminal.Loader, "terminal")
-
- cmds = commander.New(l)
- l.LoadLibrary(cmds.Loader, "commander")
-
hooks = bait.New(l)
hooks.SetRecoverer(func(event string, handler *bait.Listener, err interface{}) {
fmt.Println("Error in `error` hook handler:", err)
hooks.Off(event, handler)
})
l.LoadLibrary(hooks.Loader, "bait")
+
+ // Add Ctrl-C handler
/*
- // Add Ctrl-C handler
- hooks.On("signal.sigint", func(...interface{}) {
+ hooks.On("signal.sigint", func(...interface{}) rt.Value {
if !interactive {
os.Exit(0)
}
+ return rt.NilValue
})
lr.rl.RawInputCallback = func(r []rune) {
@@ -51,6 +45,13 @@ func luaInit() {
}
*/
+ l.LoadLibrary(fs.Loader, "fs")
+ l.LoadLibrary(terminal.Loader, "terminal")
+ l.LoadLibrary(snail.Loader, "snail")
+
+ cmds = commander.New(l)
+ l.LoadLibrary(cmds.Loader, "commander")
+
// Add more paths that Lua can require from
_, err := l.DoString("package.path = package.path .. " + requirePaths)
if err != nil {
@@ -60,7 +61,7 @@ func luaInit() {
err1 := l.DoFile("nature/init.lua")
if err1 != nil {
- err2 := l.DoFile(preloadPath)
+ err2 := l.DoFile(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 ad038207..d0e1cb6b 100644
--- a/main.go
+++ b/main.go
@@ -22,7 +22,6 @@ import (
"github.com/pborman/getopt"
"github.com/maxlandon/readline"
"golang.org/x/term"
- "mvdan.cc/sh/v3/interp"
)
var (
@@ -39,16 +38,27 @@ var (
cmds *commander.Commander
defaultConfPath string
defaultHistPath string
- runner *interp.Runner
)
func main() {
- runner, _ = interp.New()
+ 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 {
@@ -142,10 +152,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
}
}
@@ -224,8 +235,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))
@@ -249,16 +261,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)
@@ -305,15 +325,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/moonlight/runtime_golua.go b/moonlight/runtime_golua.go
index 96ba4c6c..30cca618 100644
--- a/moonlight/runtime_golua.go
+++ b/moonlight/runtime_golua.go
@@ -48,3 +48,12 @@ func (mlr *Runtime) PushNext1(v Value) {
func (mlr *Runtime) Call1(val Value, args ...Value) (Value, error) {
return rt.Call1(mlr.rt.MainThread(), val, args...)
}
+
+// Get a value from the registry.
+func (mlr *Runtime) Registry(key Value) Value {
+ return mlr.rt.Registry(key)
+}
+
+func (mlr *Runtime) SetRegistry(key, value Value) {
+ mlr.rt.SetRegistry(key, value)
+}
diff --git a/moonlight/userdata_golua.go b/moonlight/userdata_golua.go
new file mode 100644
index 00000000..0b8d6eee
--- /dev/null
+++ b/moonlight/userdata_golua.go
@@ -0,0 +1,21 @@
+//go:build !midnight
+
+package moonlight
+
+import (
+ rt "github.com/arnodel/golua/runtime"
+)
+
+type UserData struct {
+ ud *rt.UserData
+}
+
+func NewUserData(v interface{}, meta *Table) *UserData {
+ return &UserData{
+ ud: rt.NewUserData(v, meta.lt),
+ }
+}
+
+func UserDataValue(u *UserData) Value {
+ return rt.UserDataValue(u.ud)
+}
diff --git a/moonlight/util_golua.go b/moonlight/util_golua.go
index 64e61cd0..4289a0b0 100644
--- a/moonlight/util_golua.go
+++ b/moonlight/util_golua.go
@@ -1,4 +1,5 @@
//go:build !midnight
+
package moonlight
import (
@@ -20,6 +21,15 @@ func (mlr *Runtime) DoString(code string) (Value, error) {
return ret, err
}
+func (mlr *Runtime) MustDoString(code string) Value {
+ val, err := mlr.DoString(code)
+ if err != nil {
+ panic(err)
+ }
+
+ return val
+}
+
// DoFile runs the contents of the file in the Lua runtime.
func (mlr *Runtime) DoFile(path string) error {
f, err := os.Open(path)
@@ -28,7 +38,7 @@ func (mlr *Runtime) DoFile(path string) error {
if err != nil {
return err
}
-
+
reader := bufio.NewReader(f)
c, err := reader.ReadByte()
if err != nil && err != io.EOF {
@@ -59,7 +69,7 @@ func (mlr *Runtime) DoFile(path string) error {
}
return err
}
-
+
buf = append(buf, line...)
}
diff --git a/moonlight/value_golua.go b/moonlight/value_golua.go
index 5989f96f..b0e91742 100644
--- a/moonlight/value_golua.go
+++ b/moonlight/value_golua.go
@@ -1,4 +1,5 @@
//go:build !midnight
+
package moonlight
import (
@@ -9,11 +10,13 @@ var NilValue = rt.NilValue
type Value = rt.Value
type ValueType = rt.ValueType
+
const (
- IntType = rt.IntType
- StringType = rt.StringType
+ NilType = rt.NilType
+ IntType = rt.IntType
+ StringType = rt.StringType
FunctionType = rt.FunctionType
- TableType = rt.TableType
+ TableType = rt.TableType
)
func Type(v Value) ValueType {
@@ -40,7 +43,7 @@ func ToString(v Value) string {
return v.AsString()
}
-func ToTable(v Value) *Table {
+func ToTable(mlr *Runtime, v Value) *Table {
return convertToMoonlightTable(v.AsTable())
}
diff --git a/nature/abbr.lua b/nature/abbr.lua
new file mode 100644
index 00000000..cbe89ff5
--- /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 7cfe4a2d..723b8288 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,12 @@ 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)
end)
diff --git a/nature/dirs.lua b/nature/dirs.lua
index 328b4b79..db559546 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 f0b7e119..a21312ab 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] = lunacolors.format('{greyBg}' .. ' ' .. M.highlight(line:sub(0, longest))
+ 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 fe4c31c4..2badfaea 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
diff --git a/nature/greenhouse/page.lua b/nature/greenhouse/page.lua
index 51d1440f..185ef618 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 00000000..6ae9a5d1
--- /dev/null
+++ b/nature/hilbish.lua
@@ -0,0 +1,79 @@
+-- @module hilbish
+local bait = require 'bait'
+local snail = require 'snail'
+
+hilbish.snail = snail.new()
+hilbish.snail:run 'true' -- to "initialize" snail
+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 581e92c1..88cb88f6 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 c900c688..8888774e 100644
--- a/nature/init.lua
+++ b/nature/init.lua
@@ -22,12 +22,16 @@ else
pcall = unsafe_pcall
end
+require 'nature.hilbish'
+require 'nature.processors'
+
require 'nature.commands'
require 'nature.completions'
require 'nature.opts'
require 'nature.vim'
require 'nature.runner'
require 'nature.hummingbird'
+require 'nature.abbr'
local shlvl = tonumber(os.getenv 'SHLVL')
if shlvl ~= nil then
diff --git a/nature/opts/autocd.lua b/nature/opts/autocd.lua
index ce682303..2c8a2b7d 100644
--- a/nature/opts/autocd.lua
+++ b/nature/opts/autocd.lua
@@ -1,18 +1,27 @@
local fs = require 'fs'
-local oldShRunner = hilbish.runner.sh
-function hilbish.runner.sh(input)
- local res = oldShRunner(input)
+hilbish.processors.add {
+ name = 'hilbish.autocd',
+ func = function(path)
+ if hilbish.opts.autocd then
+ local ok, stat = pcall(fs.stat, path)
+ if ok and stat.isDir then
+ local oldPath = hilbish.cwd()
- if res.exit ~= 0 and hilbish.opts.autocd then
- local ok, stat = pcall(fs.stat, res.input)
- if ok and stat.isDir then
- -- discard here to not append the cd, which will be in history
- local _, exitCode, err = hilbish.runner.sh('cd ' .. res.input)
- res.exitCode = exitCode
- res.err = err
+ local absPath = fs.abs(path)
+ fs.cd(path)
+
+ bait.throw('cd', path, oldPath)
+ bait.throw('hilbish.cd', absPath, oldPath)
+
+ end
+ return {
+ continue = not ok
+ }
+ else
+ return {
+ continue = true
+ }
end
end
-
- return res
-end
+}
diff --git a/nature/opts/init.lua b/nature/opts/init.lua
index 474ea3bc..ca99caed 100644
--- a/nature/opts/init.lua
+++ b/nature/opts/init.lua
@@ -2,7 +2,7 @@ hilbish.opts = {}
local function setupOpt(name, default)
hilbish.opts[name] = default
- pcall(require, 'nature.opts.' .. name)
+ local ok, err = pcall(require, 'nature.opts.' .. name)
end
local defaultOpts = {
@@ -14,7 +14,9 @@ The nice lil shell for {blue}Lua{reset} fanatics!
motd = true,
fuzzy = false,
notifyJobFinish = true,
- crimmas = true
+ crimmas = true,
+ tips = true,
+ processorSkipList = {}
}
for optsName, default in pairs(defaultOpts) do
diff --git a/nature/opts/motd.lua b/nature/opts/motd.lua
index c1f31b45..064ef931 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 00000000..c951b2e1
--- /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/processors.lua b/nature/processors.lua
new file mode 100644
index 00000000..f36d7ace
--- /dev/null
+++ b/nature/processors.lua
@@ -0,0 +1,57 @@
+-- @module hilbish.processors
+
+hilbish.processors = {
+ list = {},
+ sorted = {}
+}
+
+function hilbish.processors.add(processor)
+ if not processor.name then
+ error 'processor is missing name'
+ end
+
+ if not processor.func then
+ error 'processor is missing function'
+ end
+
+ table.insert(hilbish.processors.list, processor)
+ table.sort(hilbish.processors.list, function(a, b) return a.priority < b.priority end)
+end
+
+local function contains(search, needle)
+ for _, p in ipairs(search) do
+ if p == needle then
+ return true
+ end
+ end
+
+ return false
+end
+
+--- Run all command processors, in order by priority.
+--- It returns the processed command (which may be the same as the passed command)
+--- and a boolean which states whether to proceed with command execution.
+function hilbish.processors.execute(command, opts)
+ opts = opts or {}
+ opts.skip = opts.skip or {}
+
+ local continue = true
+ local history
+ for _, processor in ipairs(hilbish.processors.list) do
+ if not contains(opts.skip, processor.name) then
+ local processed = processor.func(command)
+ if processed.history ~= nil then history = processed.history end
+ if processed.command then command = processed.command end
+ if not processed.continue then
+ continue = false
+ break
+ end
+ end
+ end
+
+ return {
+ command = command,
+ continue = continue,
+ history = history
+ }
+end
diff --git a/nature/runner.lua b/nature/runner.lua
index 235ab774..cc5b67e0 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,91 @@ 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)
+ bait.throw('command.preprocess', input)
+ local processed = hilbish.processors.execute(input, {
+ skip = hilbish.opts.processorSkipList
+ })
+ priv = processed.history ~= nil and (not processed.history) or priv
+ if not processed.continue then
+ finishExec(0, '', true)
+ return
+ end
+
+ local command = hilbish.aliases.resolve(processed.command)
+ bait.throw('command.preexec', processed.command, command)
+
+ ::rerun::
+ local runner = hilbish.runner.get(currentRunner)
+ local ok, out = pcall(runner.run, processed.command)
+ if not ok then
+ io.stderr:write(out .. '\n')
+ finishExec(124, out.input, priv)
+ return
+ end
+
+ if out.continue then
+ local contInput = continuePrompt(processed.command, out.newline)
+ if contInput then
+ processed.command = 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 +193,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/readline/comp-group.go b/readline/comp-group.go
index b2ee4b89..74b528a3 100644
--- a/readline/comp-group.go
+++ b/readline/comp-group.go
@@ -14,6 +14,7 @@ type CompletionGroup struct {
Suggestions []string
Aliases map[string]string // A candidate has an alternative name (ex: --long, -l option flags)
Descriptions map[string]string // Items descriptions
+ ItemDisplays map[string]string // What to display the item as (can be used for styling items)
DisplayType TabDisplayType // Map, list or normal
MaxLength int // Each group can be limited in the number of comps offered
diff --git a/readline/comp-list.go b/readline/comp-list.go
index cdcda8fb..403cf5d1 100644
--- a/readline/comp-list.go
+++ b/readline/comp-list.go
@@ -217,6 +217,11 @@ func (g *CompletionGroup) writeList(rl *Instance) (comp string) {
alt = strings.Repeat(" ", maxLengthAlt+1) // + 2 to keep account of spaces
}
+ styledSugg, ok := g.ItemDisplays[item]
+ if ok {
+ sugg = fmt.Sprintf("\r%s%-"+cellWidth+"s", highlight(y, 0), fmtEscape(styledSugg))
+ }
+
// Description
description := g.Descriptions[g.Suggestions[i]]
if len(description) > maxDescWidth {
diff --git a/readline/completers/command-arguments.go b/readline/completers/command-arguments.go
deleted file mode 100644
index 912ac7ee..00000000
--- 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 ae77aabb..00000000
--- 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 e838961d..00000000
--- 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 fcec4c5e..00000000
--- 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 472c4809..00000000
--- 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 6de587ad..00000000
--- 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 8bce99fa..00000000
--- 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 1c9a9422..00000000
--- 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 d976888a..00000000
--- 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 fcd92718..00000000
--- 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 16fd4dec..00000000
--- 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 6c6a67c2..d0c54fe0 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/instance.go b/readline/instance.go
index 163bffee..3f52bed5 100644
--- a/readline/instance.go
+++ b/readline/instance.go
@@ -240,7 +240,7 @@ func NewInstance() *Instance {
var err error
rl.regexSearch, err = regexp.Compile("(?i)" + string(rl.tfLine))
if err != nil {
- rl.RefreshPromptLog(err.Error())
+ //rl.RefreshPromptLog(err.Error())
rl.infoText = []rune(Red("Failed to match search regexp"))
}
diff --git a/readline/readline.go b/readline/readline.go
index 627bff49..7282071b 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/vimdelete.go b/readline/vimdelete.go
index 7a07259c..f5c1806c 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/rl.go b/rl.go
index 50151b13..ee970e4f 100644
--- a/rl.go
+++ b/rl.go
@@ -14,9 +14,10 @@ import (
)
type lineReader struct {
- rl *readline.Instance
+ rl *readline.Instance
fileHist *fileHistory
}
+
var hinter *rt.Closure
var highlighter *rt.Closure
@@ -55,18 +56,24 @@ func newLineReader(prompt string, noHist bool) *lineReader {
rl.ViModeCallback = func(mode readline.ViMode) {
modeStr := ""
switch mode {
- case readline.VimKeys: modeStr = "normal"
- case readline.VimInsert: modeStr = "insert"
- case readline.VimDelete: modeStr = "delete"
- case readline.VimReplaceOnce, readline.VimReplaceMany: modeStr = "replace"
+ case readline.VimKeys:
+ modeStr = "normal"
+ case readline.VimInsert:
+ modeStr = "insert"
+ case readline.VimDelete:
+ modeStr = "delete"
+ case readline.VimReplaceOnce, readline.VimReplaceMany:
+ modeStr = "replace"
}
setVimMode(modeStr)
}
rl.ViActionCallback = func(action readline.ViAction, args []string) {
actionStr := ""
switch action {
- case readline.VimActionPaste: actionStr = "paste"
- case readline.VimActionYank: actionStr = "yank"
+ case readline.VimActionPaste:
+ actionStr = "paste"
+ case readline.VimActionYank:
+ actionStr = "yank"
}
hooks.Emit("hilbish.vimAction", actionStr, args)
}
@@ -77,12 +84,12 @@ func newLineReader(prompt string, noHist bool) *lineReader {
fmt.Println(err)
return []rune{}
}
-
+
hintText := ""
if luaStr, ok := retVal.TryString(); ok {
hintText = luaStr
}
-
+
return []rune(hintText)
}
rl.SyntaxHighlighter = func(line []rune) string {
@@ -92,12 +99,12 @@ func newLineReader(prompt string, noHist bool) *lineReader {
fmt.Println(err)
return string(line)
}
-
+
highlighted := ""
if luaStr, ok := retVal.TryString(); ok {
highlighted = luaStr
}
-
+
return highlighted
}
setupTabCompleter(rl)
@@ -121,8 +128,8 @@ func (lr *lineReader) SetPrompt(p string) {
halfPrompt := strings.Split(p, "\n")
if len(halfPrompt) > 1 {
lr.rl.Multiline = true
- lr.rl.SetPrompt(strings.Join(halfPrompt[:len(halfPrompt) - 1], "\n"))
- lr.rl.MultilinePrompt = halfPrompt[len(halfPrompt) - 1:][0]
+ lr.rl.SetPrompt(strings.Join(halfPrompt[:len(halfPrompt)-1], "\n"))
+ lr.rl.MultilinePrompt = halfPrompt[len(halfPrompt)-1:][0]
} else {
lr.rl.Multiline = false
lr.rl.MultilinePrompt = ""
@@ -154,17 +161,17 @@ func (lr *lineReader) Resize() {
// #interface history
// command history
-// The history interface deals with command history.
+// The history interface deals with command history.
// This includes the ability to override functions to change the main
// method of saving history.
func (lr *lineReader) Loader(rtm *rt.Runtime) *rt.Table {
lrLua := map[string]util.LuaExport{
/*
- "add": {lr.luaAddHistory, 1, false},
- "all": {lr.luaAllHistory, 0, false},
- "clear": {lr.luaClearHistory, 0, false},
- "get": {lr.luaGetHistory, 1, false},
- "size": {lr.luaSize, 0, false},
+ "add": {lr.luaAddHistory, 1, false},
+ "all": {lr.luaAllHistory, 0, false},
+ "clear": {lr.luaClearHistory, 0, false},
+ "get": {lr.luaGetHistory, 1, false},
+ "size": {lr.luaSize, 0, false},
*/
}
diff --git a/rl_notmidnight.go b/rl_notmidnight.go
index 219439fe..6ff44623 100644
--- a/rl_notmidnight.go
+++ b/rl_notmidnight.go
@@ -1,4 +1,5 @@
//go:build !midnight
+
package main
import (
@@ -13,7 +14,7 @@ func setupTabCompleter(rl *readline.Instance) {
term := rt.NewTerminationWith(l.UnderlyingRuntime().MainThread().CurrentCont(), 2, false)
compHandle := hshMod.Get(rt.StringValue("completion")).AsTable().Get(rt.StringValue("handler"))
err := rt.Call(l.UnderlyingRuntime().MainThread(), compHandle, []rt.Value{rt.StringValue(string(line)),
- rt.IntValue(int64(pos))}, term)
+ rt.IntValue(int64(pos))}, term)
var compGroups []*readline.CompletionGroup
if err != nil {
@@ -46,10 +47,15 @@ func setupTabCompleter(rl *readline.Instance) {
items := []string{}
itemDescriptions := make(map[string]string)
+ itemDisplays := make(map[string]string)
+ itemAliases := make(map[string]string)
util.ForEach(luaCompItems.AsTable(), func(lkey rt.Value, lval rt.Value) {
if keytyp := lkey.Type(); keytyp == rt.StringType {
+ // TODO: remove in 3.0
// ['--flag'] = {'description', '--flag-alias'}
+ // OR
+ // ['--flag'] = {description = '', alias = '', display = ''}
itemName, ok := lkey.TryString()
vlTbl, okk := lval.TryTable()
if !ok && !okk {
@@ -59,18 +65,30 @@ func setupTabCompleter(rl *readline.Instance) {
items = append(items, itemName)
itemDescription, ok := vlTbl.Get(rt.IntValue(1)).TryString()
+ if !ok {
+ // if we can't get it by number index, try by string key
+ itemDescription, _ = vlTbl.Get(rt.StringValue("description")).TryString()
+ }
+ itemDescriptions[itemName] = itemDescription
+
+ // display
+ if itemDisplay, ok := vlTbl.Get(rt.StringValue("display")).TryString(); ok {
+ itemDisplays[itemName] = itemDisplay
+ }
+
+ itemAlias, ok := vlTbl.Get(rt.IntValue(2)).TryString()
+ if !ok {
+ // if we can't get it by number index, try by string key
+ itemAlias, _ = vlTbl.Get(rt.StringValue("alias")).TryString()
+ }
+ itemAliases[itemName] = itemAlias
+ } else if keytyp == rt.IntType {
+ vlStr, ok := lval.TryString()
if !ok {
// TODO: error
return
}
- itemDescriptions[itemName] = itemDescription
- } else if keytyp == rt.IntType {
- vlStr, ok := lval.TryString()
- if !ok {
- // TODO: error
- return
- }
- items = append(items, vlStr)
+ items = append(items, vlStr)
} else {
// TODO: error
return
@@ -79,18 +97,22 @@ func setupTabCompleter(rl *readline.Instance) {
var dispType readline.TabDisplayType
switch luaCompType.AsString() {
- case "grid": dispType = readline.TabDisplayGrid
- case "list": dispType = readline.TabDisplayList
+ case "grid":
+ dispType = readline.TabDisplayGrid
+ case "list":
+ dispType = readline.TabDisplayList
// need special cases, will implement later
//case "map": dispType = readline.TabDisplayMap
}
compGroups = append(compGroups, &readline.CompletionGroup{
- DisplayType: dispType,
+ DisplayType: dispType,
+ Aliases: itemAliases,
Descriptions: itemDescriptions,
- Suggestions: items,
- TrimSlash: false,
- NoSpace: true,
+ ItemDisplays: itemDisplays,
+ Suggestions: items,
+ TrimSlash: false,
+ NoSpace: true,
})
})
diff --git a/runnermode.go b/runnermode.go
index 9dcd40d2..a1487240 100644
--- a/runnermode.go
+++ b/runnermode.go
@@ -1,6 +1,8 @@
package main
-import "hilbish/moonlight"
+import (
+ "hilbish/moonlight"
+)
// #interface runner
// interactive command runner customization
@@ -17,6 +19,7 @@ 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`.)
+<<<<<<< HEAD
- `exitCode` (number): A numerical code to indicate the exit result.
- `input` (string): The user input. This will be used to add
to the history.
@@ -27,6 +30,20 @@ It can be set to a few special values for Hilbish to throw the right hooks and h
`[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`.
+>>>>>>> master
Here is a simple example of a fennel runner. It falls back to
shell script if fennel eval has an error.
@@ -45,57 +62,17 @@ hilbish.runnerMode(function(input)
end)
```
*/
-func runnerModeLoader(rtm *moonlight.Runtime) *moonlight.Table {
+func runnerModeLoader(mlr *moonlight.Runtime) *moonlight.Table {
exports := map[string]moonlight.Export{
- "sh": {shRunner, 1, false},
- "lua": {luaRunner, 1, false},
- "setMode": {hlrunnerMode, 1, false},
+ "lua": {luaRunner, 1, false},
}
mod := moonlight.NewTable()
- rtm.SetExports(mod, exports)
+ mlr.SetExports(mod, exports)
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(mlr *moonlight.Runtime) error {
- if err := mlr.Check1Arg(); err != nil {
- return err
- }
- cmd, err := mlr.StringArg(0)
- if err != nil {
- return err
- }
-
- _, exitCode, cont, err := execSh(aliases.Resolve(cmd))
- var luaErr moonlight.Value = moonlight.NilValue
- if err != nil {
- luaErr = moonlight.StringValue(err.Error())
- }
- runnerRet := moonlight.NewTable()
- runnerRet.SetField("input", moonlight.StringValue(cmd))
- runnerRet.SetField("exitCode", moonlight.IntValue(int64(exitCode)))
- runnerRet.SetField("continue", moonlight.BoolValue(cont))
- runnerRet.SetField("err", luaErr)
-
- mlr.PushNext1(moonlight.TableValue(runnerRet))
- return nil
-}
-
// #interface runner
// lua(cmd)
// Evaluates `cmd` as Lua input. This is the same as using `dofile`
diff --git a/testplugin/testplugin.go b/testplugin/testplugin.go
deleted file mode 100644
index 2d8a41be..00000000
--- 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 3c83992c..00000000
Binary files a/testplugin/testplugin.so and /dev/null differ
diff --git a/sink.go b/util/sink.go
similarity index 52%
rename from sink.go
rename to util/sink.go
index ec526340..b2ab48c5 100644
--- a/sink.go
+++ b/util/sink.go
@@ -1,4 +1,4 @@
-package main
+package util
import (
"bufio"
@@ -16,61 +16,76 @@ import (
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
- file *os.File
- ud *rt.UserData
+// A sink is a structure that has input and/or output to/from a desination.
+type Sink struct {
+ Rw *bufio.ReadWriter
+ file *os.File
+ UserData *rt.UserData
autoFlush bool
}
-func setupSinkType() {
+func SinkLoader(mlr *moonlight.Runtime) *moonlight.Table {
//sinkMeta := moonlight.NewTable()
- sinkMethods := moonlight.NewTable()
- sinkFuncs := map[string]moonlight.Export{
- /*
- "flush": {luaSinkFlush, 1, false},
- "read": {luaSinkRead, 1, false},
- "readAll": {luaSinkReadAll, 1, false},
- "autoFlush": {luaSinkAutoFlush, 2, false},
- "write": {luaSinkWrite, 2, false},
- "writeln": {luaSinkWriteln, 2, false},
- */
- }
- l.SetExports(sinkMethods, sinkFuncs)
-/*
- sinkIndex := func(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
- s, _ := sinkArg(c, 0)
+ /*
+ sinkMethods := moonlight.NewTable()
+ sinkFuncs := map[string]moonlight.Export{
+ "flush": {luaSinkFlush, 1, false},
+ "read": {luaSinkRead, 1, false},
+ "readAll": {luaSinkReadAll, 1, false},
+ "autoFlush": {luaSinkAutoFlush, 2, false},
+ "write": {luaSinkWrite, 2, false},
+ "writeln": {luaSinkWriteln, 2, false},
+ }
+ */
+ //l.SetExports(sinkMethods, sinkFuncs)
+ /*
+ sinkIndex := func(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
+ s, _ := sinkArg(c, 0)
- arg := c.Arg(1)
- val := sinkMethods.Get(arg)
+ arg := c.Arg(1)
+ val := sinkMethods.Get(arg)
+
+ if val != rt.NilValue {
+ return c.PushingNext1(t.Runtime, val), nil
+ }
+
+ keyStr, _ := arg.TryString()
+
+ switch keyStr {
+ case "pipe":
+ val = rt.BoolValue(false)
+ if s.file != nil {
+ fileInfo, _ := s.file.Stat();
+ val = rt.BoolValue(fileInfo.Mode() & os.ModeCharDevice == 0)
+ }
+ }
- if val != rt.NilValue {
return c.PushingNext1(t.Runtime, val), nil
}
- keyStr, _ := arg.TryString()
+ sinkMeta.Set(rt.StringValue("__index"), rt.FunctionValue(rt.NewGoFunction(sinkIndex, "__index", 2, false)))
+ mlr.SetRegistry(sinkMetaKey, rt.TableValue(sinkMeta))
+ */
- switch keyStr {
- case "pipe":
- val = rt.BoolValue(false)
- if s.file != nil {
- fileInfo, _ := s.file.Stat();
- val = rt.BoolValue(fileInfo.Mode() & os.ModeCharDevice == 0)
- }
- }
-
- return c.PushingNext1(t.Runtime, val), nil
+ exports := map[string]moonlight.Export{
+ "new": {luaSinkNew, 0, false},
}
- sinkMeta.Set(rt.StringValue("__index"), rt.FunctionValue(rt.NewGoFunction(sinkIndex, "__index", 2, false)))
- l.SetRegistry(sinkMetaKey, rt.TableValue(sinkMeta))
-*/
+ mod := moonlight.NewTable()
+ mlr.SetExports(mod, exports)
+
+ return mod
}
+func luaSinkNew(mlr *moonlight.Runtime) error {
+ /*
+ snk := NewSink(t.Runtime, new(bytes.Buffer))
+
+ return c.PushingNext1(t.Runtime, rt.UserDataValue(snk.UserData)), nil
+ */
+ return nil
+}
// #member
// readAll() -> string
@@ -86,11 +101,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
}
@@ -117,7 +138,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
}
@@ -139,9 +160,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
@@ -164,9 +185,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
@@ -185,7 +206,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
}
@@ -216,11 +237,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(mlr *moonlight.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(mlr *moonlight.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
@@ -229,38 +264,38 @@ func newSinkInput(r io.Reader) *sink {
return s
}
-func newSinkOutput(w io.Writer) *sink {
- s := &sink{
- writer: bufio.NewWriter(w),
+func NewSinkOutput(mlr *moonlight.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)
+ return nil, fmt.Errorf("#%d must be a sink", arg+1)
}
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.UnderlyingRuntime().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 00000000..11f93088
--- /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 44e8f318..e93b320d 100644
--- a/util/util.go
+++ b/util/util.go
@@ -1,14 +1,79 @@
package util
import (
+ "context"
+ "errors"
+ "fmt"
+ "os"
+ "os/exec"
"os/user"
+ "path/filepath"
+ "runtime"
"strings"
+ "syscall"
"hilbish/moonlight"
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(module *rt.Table, field string, value rt.Value) {
@@ -69,3 +134,68 @@ 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 57%
rename from execfile_unix.go
rename to util/util_unix.go
index 82c738b6..92813c8d 100644
--- a/execfile_unix.go
+++ b/util/util_unix.go
@@ -1,17 +1,12 @@
//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 56%
rename from execfile_windows.go
rename to util/util_windows.go
index 3d6ef61b..3321033c 100644
--- a/execfile_windows.go
+++ b/util/util_windows.go
@@ -1,18 +1,13 @@
//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 bad94dbe..86ed2532 100644
--- a/vars.go
+++ b/vars.go
@@ -11,8 +11,8 @@ var (
// Version info
var (
- ver = "v2.3.0"
- releaseName = "Alyssum"
+ ver = "v2.4.0"
+ releaseName = "Moonflower"
gitCommit string
gitBranch string
diff --git a/vars_darwin.go b/vars_darwin.go
index 43215d5b..be7a4c10 100644
--- a/vars_darwin.go
+++ b/vars_darwin.go
@@ -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_unix.go b/vars_unix.go
index f90fa55b..6bf47f5f 100644
--- a/vars_unix.go
+++ b/vars_unix.go
@@ -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 f724fc20..4c46539c 100644
--- a/vars_windows.go
+++ b/vars_windows.go
@@ -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 = ""
)