diff --git a/api.go b/api.go index 3d0ba262..b64b43c2 100644 --- a/api.go +++ b/api.go @@ -6,10 +6,13 @@ // #field user Username of the user // #field host Hostname of the machine // #field dataDir Directory for Hilbish data files, including the docs and default modules +// #field defaultConfDir Default directory Hilbish runs its config file from +// #field confFile File to run as Hilbish config, this is only set with the -C flag // #field interactive Is Hilbish in an interactive shell? // #field login Is Hilbish the login shell? // #field vimMode Current Vim input mode of Hilbish (will be nil if not in Vim input mode) // #field exitCode Exit code of the last executed command +// #field exitCode If Hilbish is currently running any interactive input package main import ( @@ -44,10 +47,8 @@ var exports = map[string]util.LuaExport{ "hinter": {hlhinter, 1, false}, "multiprompt": {hlmultiprompt, 1, false}, "prependPath": {hlprependPath, 1, false}, - "prompt": {hlprompt, 1, true}, "inputMode": {hlinputMode, 1, false}, "interval": {hlinterval, 2, false}, - "read": {hlread, 1, false}, "timeout": {hltimeout, 2, false}, "which": {hlwhich, 1, false}, } @@ -79,6 +80,8 @@ func hilbishLoad(rtm *rt.Runtime) (rt.Value, func()) { util.SetField(rtm, mod, "host", rt.StringValue(host)) util.SetField(rtm, mod, "home", rt.StringValue(curuser.HomeDir)) util.SetField(rtm, mod, "dataDir", rt.StringValue(dataDir)) + util.SetField(rtm, mod, "defaultConfDir", rt.StringValue(defaultConfDir)) + util.SetField(rtm, mod, "confFile", rt.StringValue(confPath)) util.SetField(rtm, mod, "interactive", rt.BoolValue(interactive)) util.SetField(rtm, mod, "login", rt.BoolValue(login)) util.SetField(rtm, mod, "vimMode", rt.NilValue) @@ -194,88 +197,6 @@ func hlcwd(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.PushingNext1(t.Runtime, rt.StringValue(cwd)), nil } -// read(prompt) -> input (string) -// Read input from the user, using Hilbish's line editor/input reader. -// This is a separate instance from the one Hilbish actually uses. -// Returns `input`, will be nil if Ctrl-D is pressed, or an error occurs. -// #param prompt? string Text to print before input, can be empty. -// #returns string|nil -func hlread(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { - luaprompt := c.Arg(0) - if typ := luaprompt.Type(); typ != rt.StringType && typ != rt.NilType { - return nil, errors.New("expected #1 to be a string") - } - prompt, ok := luaprompt.TryString() - if !ok { - // if we are here and `luaprompt` is not a string, it's nil - // substitute with an empty string - prompt = "" - } - - lualr := &lineReader{ - rl: readline.NewInstance(), - } - lualr.SetPrompt(prompt) - - input, err := lualr.Read() - if err != nil { - return c.Next(), nil - } - - return c.PushingNext1(t.Runtime, rt.StringValue(input)), nil -} - -/* -prompt(str, typ) -Changes the shell prompt to the provided string. -There are a few verbs that can be used in the prompt text. -These will be formatted and replaced with the appropriate values. -`%d` - Current working directory -`%u` - Name of current user -`%h` - Hostname of device -#param str string -#param typ? string Type of prompt, being left or right. Left by default. -#example --- the default hilbish prompt without color -hilbish.prompt '%u %d ∆' --- or something of old: -hilbish.prompt '%u@%h :%d $' --- prompt: user@hostname: ~/directory $ -#example -*/ -func hlprompt(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { - err := c.Check1Arg() - if err != nil { - return nil, err - } - p, err := c.StringArg(0) - if err != nil { - return nil, err - } - typ := "left" - // optional 2nd arg - if len(c.Etc()) != 0 { - ltyp := c.Etc()[0] - var ok bool - typ, ok = ltyp.TryString() - if !ok { - return nil, errors.New("bad argument to run (expected string, got " + ltyp.TypeName() + ")") - } - } - - switch typ { - case "left": - prompt = p - lr.SetPrompt(fmtPrompt(prompt)) - case "right": - lr.SetRightPrompt(fmtPrompt(p)) - default: - return nil, errors.New("expected prompt type to be right or left, got " + typ) - } - - return c.Next(), nil -} - // multiprompt(str) // Changes the text prompt when Hilbish asks for more input. // This will show up when text is incomplete, like a missing quote diff --git a/exec.go b/exec.go index 63dac576..647bbf9a 100644 --- a/exec.go +++ b/exec.go @@ -1,7 +1,6 @@ package main import ( - "errors" "fmt" "os" "strings" @@ -10,10 +9,6 @@ import ( //"github.com/yuin/gopher-lua/parse" ) -var errNotExec = errors.New("not executable") -var errNotFound = errors.New("not found") -var runnerMode rt.Value = rt.NilValue - func runInput(input string, priv bool) { running = true runnerRun := hshMod.Get(rt.StringValue("runner")).AsTable().Get(rt.StringValue("run")) diff --git a/golibs/readline/lua.go b/golibs/readline/lua.go index 9efbc48f..bdce2b5a 100644 --- a/golibs/readline/lua.go +++ b/golibs/readline/lua.go @@ -9,6 +9,7 @@ package readline import ( "fmt" "io" + "strings" "hilbish/util" @@ -28,6 +29,8 @@ func (rl *Readline) luaLoader(rtm *rt.Runtime) (rt.Value, func()) { "readChar": {rlReadChar, 1, false}, "setVimRegister": {rlSetRegister, 3, false}, "log": {rlLog, 2, false}, + "prompt": {rlPrompt, 2, false}, + "refreshPrompt": {rlRefreshPrompt, 1, false}, } util.SetExports(rtm, rlMethods, rlMethodss) @@ -251,6 +254,53 @@ func rlLog(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.Next(), nil } +// #member +// prompt(text) +// Sets the prompt of the line reader. This is the text that shows up before user input. +func rlPrompt(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.CheckNArgs(2); err != nil { + return nil, err + } + + rl, err := rlArg(c, 0) + if err != nil { + return nil, err + } + + p, err := c.StringArg(1) + if err != nil { + return nil, err + } + fmt.Println(p) + + halfPrompt := strings.Split(p, "\n") + if len(halfPrompt) > 1 { + rl.Multiline = true + rl.SetPrompt(strings.Join(halfPrompt[:len(halfPrompt)-1], "\n")) + rl.MultilinePrompt = halfPrompt[len(halfPrompt)-1:][0] + } else { + rl.Multiline = false + rl.MultilinePrompt = "" + rl.SetPrompt(p) + } + + return c.Next(), nil +} + +func rlRefreshPrompt(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + + rl, err := rlArg(c, 0) + if err != nil { + return nil, err + } + + rl.RefreshPromptInPlace("") + + return c.Next(), nil +} func rlArg(c *rt.GoCont, arg int) (*Readline, error) { j, ok := valueToRl(c.Arg(arg)) if !ok { diff --git a/lua.go b/lua.go index 3a51eaaf..8c36c618 100644 --- a/lua.go +++ b/lua.go @@ -16,14 +16,19 @@ import ( "github.com/arnodel/golua/lib" "github.com/arnodel/golua/lib/debuglib" rt "github.com/arnodel/golua/runtime" + "github.com/pborman/getopt" ) -var minimalconf = `hilbish.prompt '& '` - func luaInit() { l = rt.New(os.Stdout) loadLibs(l) + luaArgs := rt.NewTable() + for i, arg := range getopt.Args() { + luaArgs.Set(rt.IntValue(int64(i)), rt.StringValue(arg)) + } + + l.GlobalEnv().Set(rt.StringValue("args"), rt.TableValue(luaArgs)) yarnPool := yarn.New(yarnloadLibs) lib.LoadLibs(l, yarnPool.Loader) @@ -36,6 +41,7 @@ func luaInit() { err1 := util.DoFile(l, "nature/init.lua") if err1 != nil { + fmt.Println(err1) err2 := util.DoFile(l, filepath.Join(dataDir, "nature", "init.lua")) if err2 != nil { fmt.Fprintln(os.Stderr, "Missing nature module, some functionality and builtins will be missing.") @@ -98,14 +104,3 @@ func yarnloadLibs(r *rt.Runtime) { lib.LoadLibs(l, lr.rl.Loader) } - -func runConfig(confpath string) { - if !interactive { - return - } - err := util.DoFile(l, confpath) - if err != nil { - fmt.Fprintln(os.Stderr, err, "\nAn error has occured while loading your config! Falling back to minimal default config.") - util.DoString(l, minimalconf) - } -} diff --git a/main.go b/main.go index 77b18477..d02393ed 100644 --- a/main.go +++ b/main.go @@ -2,40 +2,36 @@ package main import ( "bufio" - "errors" "fmt" - "io" "os" "os/exec" "os/user" "path/filepath" "runtime" "strings" - "syscall" - "hilbish/util" "hilbish/golibs/bait" "hilbish/golibs/commander" + "hilbish/util" rt "github.com/arnodel/golua/runtime" "github.com/pborman/getopt" - "github.com/maxlandon/readline" "golang.org/x/term" ) var ( - l *rt.Runtime + l *rt.Runtime lr *lineReader luaCompletions = map[string]*rt.Closure{} - confDir string + confDir string userDataDir string - curuser *user.User + curuser *user.User - hooks *bait.Bait - cmds *commander.Commander - defaultConfPath string + hooks *bait.Bait + cmds *commander.Commander + confPath string defaultHistPath string ) @@ -62,7 +58,7 @@ func main() { // i honestly dont know what directories to use for this switch runtime.GOOS { case "linux", "darwin": - userDataDir = getenv("XDG_DATA_HOME", curuser.HomeDir + "/.local/share") + userDataDir = getenv("XDG_DATA_HOME", curuser.HomeDir+"/.local/share") default: // this is fine on windows, dont know about others userDataDir = confDir @@ -75,7 +71,7 @@ func main() { // else do ~ substitution defaultConfDir = filepath.Join(util.ExpandHome(defaultConfDir), "hilbish") } - defaultConfPath = filepath.Join(defaultConfDir, "init.lua") + defaultConfPath := filepath.Join(defaultConfDir, "init.lua") if defaultHistDir == "" { defaultHistDir = filepath.Join(userDataDir, "hilbish") } else { @@ -95,6 +91,7 @@ func main() { loginshflag := getopt.Lookup('l').Seen() interactiveflag := getopt.Lookup('i').Seen() noexecflag := getopt.Lookup('n').Seen() + confPath = *configflag if *helpflag { getopt.PrintUsage(os.Stdout) @@ -105,7 +102,7 @@ func main() { interactive = true } - if fileInfo, _ := os.Stdin.Stat(); (fileInfo.Mode() & os.ModeCharDevice) == 0 || !term.IsTerminal(int(os.Stdin.Fd())) { + if fileInfo, _ := os.Stdin.Stat(); (fileInfo.Mode()&os.ModeCharDevice) == 0 || !term.IsTerminal(int(os.Stdin.Fd())) { interactive = false } @@ -143,29 +140,6 @@ func main() { go handleSignals() - // If user's config doesn't exixt, - if _, err := os.Stat(defaultConfPath); os.IsNotExist(err) && *configflag == defaultConfPath { - // Read default from current directory - // (this is assuming the current dir is Hilbish's git) - _, err := os.ReadFile(".hilbishrc.lua") - confpath := ".hilbishrc.lua" - if err != nil { - // If it wasnt found, go to the real sample conf - sampleConfigPath := filepath.Join(dataDir, ".hilbishrc.lua") - _, err = os.ReadFile(sampleConfigPath) - confpath = sampleConfigPath - if err != nil { - fmt.Println("could not find .hilbishrc.lua or", sampleConfigPath) - return - } - } - - runConfig(confpath) - } else { - runConfig(*configflag) - } - hooks.Emit("hilbish.init") - if fileInfo, _ := os.Stdin.Stat(); (fileInfo.Mode() & os.ModeCharDevice) == 0 { scanner := bufio.NewScanner(bufio.NewReader(os.Stdin)) for scanner.Scan() { @@ -180,12 +154,6 @@ func main() { } if getopt.NArgs() > 0 { - luaArgs := rt.NewTable() - for i, arg := range getopt.Args() { - luaArgs.Set(rt.IntValue(int64(i)), rt.StringValue(arg)) - } - - l.GlobalEnv().Set(rt.StringValue("args"), rt.TableValue(luaArgs)) err := util.DoFile(l, getopt.Arg(0)) if err != nil { fmt.Fprintln(os.Stderr, err) @@ -195,71 +163,73 @@ func main() { } initialized = true -input: - for interactive { - running = false + /* + input: + for interactive { + running = false - input, err := lr.Read() + input, err := lr.Read() - if err == io.EOF { - // Exit if user presses ^D (ctrl + d) - hooks.Emit("hilbish.exit") - break - } - if err != nil { - if err == readline.CtrlC { - fmt.Println("^C") - hooks.Emit("hilbish.cancel") - } else { - // If we get a completely random error, print - fmt.Fprintln(os.Stderr, err) - if errors.Is(err, syscall.ENOTTY) { - // what are we even doing here? - panic("not a tty") - } - <-make(chan struct{}) - } - continue - } - var priv bool - if strings.HasPrefix(input, " ") { - priv = true - } - - input = strings.TrimSpace(input) - if len(input) == 0 { - running = true - hooks.Emit("command.exit", 0) - continue - } - - if strings.HasSuffix(input, "\\") { - print("\n") - for { - input, err = continuePrompt(strings.TrimSuffix(input, "\\") + "\n", false) - if err != nil { - running = true - lr.SetPrompt(fmtPrompt(prompt)) - goto input // continue inside nested loop - } - if !strings.HasSuffix(input, "\\") { + if err == io.EOF { + // Exit if user presses ^D (ctrl + d) + hooks.Emit("hilbish.exit") break } + if err != nil { + if err == readline.CtrlC { + fmt.Println("^C") + hooks.Emit("hilbish.cancel") + } else { + // If we get a completely random error, print + fmt.Fprintln(os.Stderr, err) + if errors.Is(err, syscall.ENOTTY) { + // what are we even doing here? + panic("not a tty") + } + <-make(chan struct{}) + } + continue + } + var priv bool + if strings.HasPrefix(input, " ") { + priv = true + } + + input = strings.TrimSpace(input) + if len(input) == 0 { + running = true + hooks.Emit("command.exit", 0) + continue + } + + if strings.HasSuffix(input, "\\") { + print("\n") + for { + input, err = continuePrompt(strings.TrimSuffix(input, "\\")+"\n", false) + if err != nil { + running = true + lr.SetPrompt(fmtPrompt(prompt)) + goto input // continue inside nested loop + } + if !strings.HasSuffix(input, "\\") { + break + } + } + } + + runInput(input, priv) + + termwidth, _, err := term.GetSize(0) + if err != nil { + continue + } + fmt.Printf("\u001b[7m∆\u001b[0m" + strings.Repeat(" ", termwidth-1) + "\r") } - } - - runInput(input, priv) - - termwidth, _, err := term.GetSize(0) - if err != nil { - continue - } - fmt.Printf("\u001b[7m∆\u001b[0m" + strings.Repeat(" ", termwidth - 1) + "\r") - } - + */ exit(0) } +/* func continuePrompt(prev string, newline bool) (string, error) { hooks.Emit("multiline", nil) lr.SetPrompt(multilinePrompt) @@ -279,6 +249,7 @@ func continuePrompt(prev string, newline bool) (string, error) { return prev + cont, nil } +*/ // This semi cursed function formats our prompt (obviously) func fmtPrompt(prompt string) string { @@ -300,7 +271,7 @@ func fmtPrompt(prompt string) string { } for i, v := range args { - if i % 2 == 0 { + if i%2 == 0 { args[i] = "%" + v } } @@ -354,5 +325,5 @@ func getVersion() string { } func cut(slice []string, idx int) []string { - return append(slice[:idx], slice[idx + 1:]...) + return append(slice[:idx], slice[idx+1:]...) } diff --git a/nature/editor.lua b/nature/editor.lua index 002d3409..8898825e 100644 --- a/nature/editor.lua +++ b/nature/editor.lua @@ -21,7 +21,11 @@ function editorMt.__index(_, key) end return function(...) - return editor[key](editor, ...) + local args = {...} + if args[1] == hilbish.editor then + table.remove(args, 1) + end + return editor[key](editor, table.unpack(args)) end end diff --git a/nature/hilbish.lua b/nature/hilbish.lua index 6ae9a5d1..9da7a1ba 100644 --- a/nature/hilbish.lua +++ b/nature/hilbish.lua @@ -1,5 +1,7 @@ -- @module hilbish local bait = require 'bait' +local fs = require 'fs' +local readline = require 'readline' local snail = require 'snail' hilbish.snail = snail.new() @@ -7,6 +9,81 @@ hilbish.snail:run 'true' -- to "initialize" snail bait.catch('hilbish.cd', function(path) hilbish.snail:dir(path) end) + +local function abbrevHome(path) + if path:sub(1, hilbish.home:len()) == hilbish.home then + return fs.join('~', path:sub(hilbish.home:len() + 1)) + end +end + +local function fmtPrompt(p) + return p:gsub('%%(%w)', function(c) + if c == 'd' then + return abbrevHome(hilbish.cwd()) + elseif c == 'u' then + return hilbish.user + elseif c == 'h' then + return hilbish.host + end + end) +end + +--- prompt(str, typ) +--- Changes the shell prompt to the provided string. +--- There are a few verbs that can be used in the prompt text. +--- These will be formatted and replaced with the appropriate values. +--- `%d` - Current working directory +--- `%u` - Name of current user +--- `%h` - Hostname of device +--- #param str string +--- #param typ? string Type of prompt, being left or right. Left by default. +--- #example +--- -- the default hilbish prompt without color +--- hilbish.prompt '%u %d ∆' +--- -- or something of old: +--- hilbish.prompt '%u@%h :%d $' +--- -- prompt: user@hostname: ~/directory $ +--- #example +-- @param p string +-- @param typ string Type of prompt, either left or right +function hilbish.prompt(p, typ) + if type(p) ~= 'string' then + error('expected #1 to be string, got ' .. type(p)) + end + + if not typ or typ == 'left' then + hilbish.editor:prompt(fmtPrompt(p)) + if not hilbish.running then + hilbish.editor:refreshPrompt() + end + elseif typ == 'right' then + hilbish.editor:rightPrompt(fmtPrompt(p)) + if not hilbish.running then + hilbish.editor:refreshPrompt() + end + else + error('expected prompt type to be right or left, got ' .. tostring(typ)) + end +end + +--- read(prompt) -> input (string) +--- Read input from the user, using Hilbish's line editor/input reader. +--- This is a separate instance from the one Hilbish actually uses. +--- Returns `input`, will be nil if Ctrl-D is pressed, or an error occurs. +-- @param prompt? string Text to print before input, can be empty. +-- @returns string|nil +function hilbish.read(prompt) + prompt = prompt or '' + if type(prompt) ~= 'string' then + error 'expected #1 to be a string' + end + + local rl = readline.new() + rl:prompt(prompt) + + return rl:read() +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. diff --git a/nature/init.lua b/nature/init.lua index 4f973f33..b37bf63b 100644 --- a/nature/init.lua +++ b/nature/init.lua @@ -18,6 +18,7 @@ table.insert(package.searchers, function(module) return function() return hilbish.module.load(path) end, path end) +require 'nature.editor' require 'nature.hilbish' require 'nature.processors' @@ -28,7 +29,6 @@ require 'nature.vim' require 'nature.runner' require 'nature.hummingbird' require 'nature.abbr' -require 'nature.editor' local shlvl = tonumber(os.getenv 'SHLVL') if shlvl ~= nil then @@ -95,3 +95,81 @@ end) bait.catch('command.not-executable', function(cmd) print(string.format('hilbish: %s: not executable', cmd)) end) + +local function runConfig(path) + if not hilbish.interactive then return end + + local _, err = pcall(dofile, path) + if err then + print(err) + print 'An error has occured while loading your config!\n' + hilbish.prompt '& ' + else + bait.throw 'hilbish.init' + end +end + +local _, err = pcall(fs.stat, hilbish.confFile) +if err and tostring(err):match 'no such file' and hilbish.confFile == fs.join(hilbish.defaultConfDir, 'init.lua') then + -- Run config from current directory (assuming this is Hilbish's git) + local _, err = pcall(fs.stat, '.hilbishrc.lua') + local confpath = '.hilbishrc.lua' + + if err then + -- If it wasnt found go to system sample config + confpath = fs.join(hilbish.dataDir, confpath) + local _, err = pcall(fs.stat, confpath) + if err then + print('could not find .hilbishrc.lua or ' .. confpath) + return + end + end + + runConfig(confpath) +else + runConfig(hilbish.confFile) +end + +-- TODO: hilbish.exit function, stop jobs and timers. +local function exit(code) + os.exit(code) +end + +while hilbish.interactive do + hilbish.running = false + + local ok, res = pcall(function() return hilbish.editor:read() end) + if not ok and tostring(res):lower():match 'eof' then + bait.throw 'hilbish.exit' + exit(0) + end + if not ok then + if tostring(res):lower():match 'ctrl%+c' then + print '^C' + bait.throw 'hilbish.cancel' + else + error(res) + io.read() + end + goto continue + end + --- @type string + local input = res + + local priv = false + if res:sub(1, 1) == ' ' then + priv = true + end + input = input:gsub('%s+', '') + + if input:len() == 0 then + hilbish.running = true + bait.throw('command.exit', 0 ) + goto continue + end + + hilbish.running = true + hilbish.runner.run(input, priv) + + ::continue:: +end diff --git a/rl.go b/rl.go index e316967e..5985d387 100644 --- a/rl.go +++ b/rl.go @@ -3,7 +3,6 @@ package main import ( "fmt" "io" - "strings" "hilbish/util" @@ -232,29 +231,6 @@ func (lr *lineReader) Read() (string, error) { return s, err // might get another error } -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] - } else { - lr.rl.Multiline = false - lr.rl.MultilinePrompt = "" - lr.rl.SetPrompt(p) - } - if initialized && !running { - lr.rl.RefreshPromptInPlace("") - } -} - -func (lr *lineReader) SetRightPrompt(p string) { - lr.rl.SetRightPrompt(p) - if initialized && !running { - lr.rl.RefreshPromptInPlace("") - } -} - func (lr *lineReader) AddHistory(cmd string) { lr.fileHist.Write(cmd) } diff --git a/vars.go b/vars.go index 86ed2532..01328a10 100644 --- a/vars.go +++ b/vars.go @@ -2,16 +2,16 @@ package main // String vars that are free to be changed at compile time var ( - defaultHistDir = "" + defaultHistDir = "" commonRequirePaths = "';./libs/?/init.lua;./?/init.lua;./?/?.lua'" - prompt string + prompt string multilinePrompt = "> " ) // Version info var ( - ver = "v2.4.0" + ver = "v2.4.0" releaseName = "Moonflower" gitCommit string @@ -20,10 +20,9 @@ var ( // Flags var ( - running bool // Is a command currently running + running bool // Is a command currently running interactive bool - login bool // Are we the login shell? - noexecute bool // Should we run Lua or only report syntax errors + login bool // Are we the login shell? + noexecute bool // Should we run Lua or only report syntax errors initialized bool ) -