Compare commits

...

15 Commits

Author SHA1 Message Date
TorchedSammy 65435572d4
docs: clarify what a scope is 2021-11-22 19:40:00 -05:00
TorchedSammy c0abeee648
docs: fixed heading size for bugfixes in 0.7 2021-11-22 19:38:55 -05:00
TorchedSammy a38625d821
docs: changelog for 0.7 2021-11-22 19:38:06 -05:00
TorchedSammy 4e244e141f
fix: remove variables heading if there is none, account for global in new module doc format 2021-11-22 19:20:57 -05:00
TorchedSammy f741c2709c
fix: check and print module isnt found instead of failing 2021-11-22 19:20:31 -05:00
TorchedSammy 179947cd98
fix: only add __docProp metatable from SetField function 2021-11-22 19:19:36 -05:00
TorchedSammy 43ddab699f
feat: add in shell documentation for module properties 2021-11-22 18:59:28 -05:00
TorchedSammy 538ba3547f
chore: reorder code 2021-11-22 18:58:30 -05:00
TorchedSammy e67c549714
fix: get function docs for normal modules (regression from 52a6eb2125) 2021-11-22 18:29:26 -05:00
TorchedSammy 812de48558
fix: completions not working with aliases commands (resolves #82) 2021-11-22 18:01:59 -05:00
TorchedSammy 52a6eb2125
docs: document available bait hooks 2021-11-22 16:36:32 -05:00
TorchedSammy c61b428d67
fix: panic when error occurs in commander
check if commander has an error before trying to get a return code
2021-11-22 15:40:56 -05:00
TorchedSammy af4c3885ae
feat: add signal hooks for sigint and sigwinch (#80) 2021-11-22 15:13:05 -05:00
TorchedSammy 94a8b4ad47
feat: add prependPath function (resolves #81) 2021-11-22 12:46:39 -05:00
TorchedSammy 72cf776882
fix: use golang path list separator to not be unix specific 2021-11-22 12:45:45 -05:00
12 changed files with 204 additions and 38 deletions

View File

@ -2,6 +2,41 @@
This is the changelog for the Hilbish shell made in Go and Lua. This is the changelog for the Hilbish shell made in Go and Lua.
## [0.7.0] - 2021-11-22
### Added
- `hilbish.interactive` and `hilbish.login` properties to figure out if Hilbish is interactive or a login shell, respectively.
- `hilbish.read` function to take input more elegantly than Lua's `io.read`
- Tab Completion Enhancements
- A new tab complete API has been added. It is the single `complete` function which takes a "scope" (example: `command.<cmdname>`) and a callback which is
expected to return a table. Users can now add custom completions for specific commands.
An example is:
```lua
complete('command.git', function()
return {
'add',
'version',
commit = {
'--message',
'--verbose',
'<file>'
}
}
end)
```
For `git`, Hilbish will complete commands add, version and commit. For the commit subcommand, it will complete the flags and/or files which `<file>` is used to represent.
- Hilbish will now complete binaries in $PATH, or any executable to a path (like `./` or `../`)
- Files with spaces will be automatically put in quotes and completions will work for them now.
- `prependPath` function (#81)
- Signal hooks (#80)
- This allows scripts to add their own way of handling terminal resizes (if you'd need that) or Ctrl-C
- Module properties (like `hilbish.ver`) are documented with the `doc` command.
- Document bait hooks
### Fixed
- The prompt won't come up on terminal resize anymore.
- `appendPath` should work properly on Windows.
- A panic when a commander has an error has been fixed.
## [0.6.1] - 2021-10-21 ## [0.6.1] - 2021-10-21
### Fixed ### Fixed
- Require paths now use the `dataDir` variable so there is no need to change it anymore unless you want to add more paths - Require paths now use the `dataDir` variable so there is no need to change it anymore unless you want to add more paths
@ -249,6 +284,7 @@ This input for example will prompt for more input to complete:
First "stable" release of Hilbish. First "stable" release of Hilbish.
[0.7.0]: https://github.com/Rosettea/Hilbish/compare/v0.6.1...v0.7.0
[0.6.1]: https://github.com/Rosettea/Hilbish/compare/v0.6.0...v0.6.1 [0.6.1]: https://github.com/Rosettea/Hilbish/compare/v0.6.0...v0.6.1
[0.6.0]: https://github.com/Rosettea/Hilbish/compare/v0.5.1...v0.6.0 [0.6.0]: https://github.com/Rosettea/Hilbish/compare/v0.5.1...v0.6.0
[0.5.1]: https://github.com/Rosettea/Hilbish/compare/v0.5.0...v0.5.1 [0.5.1]: https://github.com/Rosettea/Hilbish/compare/v0.5.0...v0.5.1

View File

@ -0,0 +1,7 @@
+ `command.exit` -> code, cmdStr > Thrown when a command exits.
`code` is the exit code of the command, and `cmdStr` is the command that was run.
+ `command.not-found` -> cmdStr > Thrown when a command is not found.
+ `command.no-perm` -> cmdStr > Thrown when Hilbish attempts to execute a file but
has no permission.

View File

@ -0,0 +1,8 @@
Here is listed all scopes for bait hooks. If a hook is related to a command,
it will have the `command` scope, as example.
Here is the format for a doc for a hook:
+ <hook name> -> <args> > <description>
`<args>` just means the arguments of the hook. If a hook doc has the format
of `arg...`, it means the hook can take/recieve any number of `arg`.

View File

@ -0,0 +1,3 @@
+ `signal.sigint` > Sent when Hilbish receives SIGINT (used to Ctrl-C).
+ `signal.resize` > Sent when the terminal is resized.

View File

@ -20,15 +20,19 @@ func New() Bait {
func (b *Bait) Loader(L *lua.LState) int { func (b *Bait) Loader(L *lua.LState) int {
mod := L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{}) mod := L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{})
util.Document(L, mod,
`Bait is the event emitter for Hilbish. Why name it bait?
Because it throws hooks that you can catch (emits events
that you can listen to) and because why not, fun naming
is fun. This is what you will use if you want to listen
in on hooks to know when certain things have happened,
like when you've changed directory, a command has
failed, etc. To find all available hooks, see doc hooks.`)
L.SetField(mod, "throw", luar.New(L, b.bthrow)) L.SetField(mod, "throw", luar.New(L, b.bthrow))
L.SetField(mod, "catch", luar.New(L, b.bcatch)) L.SetField(mod, "catch", luar.New(L, b.bcatch))
util.Document(L, mod, `Bait is the event emitter for Hilbish. Why name it bait?
Because it throws hooks that you can catch
(emits events that you can listen to) and because why not,
fun naming is fun. This is what you will use if you want to
listen in on hooks to know when certain things have happened,
like when you've changed directory, a command has failed, etc.`)
L.Push(mod) L.Push(mod)
return 1 return 1

View File

@ -32,13 +32,13 @@ func HilbishLoader(L *lua.LState) int {
username = strings.Split(username, "\\")[1] // for some reason Username includes the hostname on windows username = strings.Split(username, "\\")[1] // for some reason Username includes the hostname on windows
} }
L.SetField(mod, "ver", lua.LString(version)) util.SetField(L, mod, "ver", lua.LString(version), "Hilbish version")
L.SetField(mod, "user", lua.LString(username)) util.SetField(L, mod, "user", lua.LString(username), "Username of user")
L.SetField(mod, "host", lua.LString(host)) util.SetField(L, mod, "host", lua.LString(host), "Host name of the machine")
L.SetField(mod, "home", lua.LString(homedir)) util.SetField(L, mod, "home", lua.LString(homedir), "Home directory of the user")
L.SetField(mod, "dataDir", lua.LString(dataDir)) util.SetField(L, mod, "dataDir", lua.LString(dataDir), "Directory for Hilbish's data files")
L.SetField(mod, "interactive", lua.LBool(interactive)) util.SetField(L, mod, "interactive", lua.LBool(interactive), "If this is an interactive shell")
L.SetField(mod, "login", lua.LBool(interactive)) util.SetField(L, mod, "login", lua.LBool(interactive), "Whether this is a login shell")
xdg := L.NewTable() xdg := L.NewTable()
L.SetField(xdg, "config", lua.LString(confDir)) L.SetField(xdg, "config", lua.LString(confDir))

25
lua.go
View File

@ -32,6 +32,7 @@ func LuaInit() {
l.SetGlobal("multiprompt", l.NewFunction(hshmlprompt)) l.SetGlobal("multiprompt", l.NewFunction(hshmlprompt))
l.SetGlobal("alias", l.NewFunction(hshalias)) l.SetGlobal("alias", l.NewFunction(hshalias))
l.SetGlobal("appendPath", l.NewFunction(hshappendPath)) l.SetGlobal("appendPath", l.NewFunction(hshappendPath))
l.SetGlobal("prependPath", l.NewFunction(hshprependPath))
l.SetGlobal("exec", l.NewFunction(hshexec)) l.SetGlobal("exec", l.NewFunction(hshexec))
l.SetGlobal("goro", luar.New(l, hshgoroutine)) l.SetGlobal("goro", luar.New(l, hshgoroutine))
l.SetGlobal("timeout", luar.New(l, hshtimeout)) l.SetGlobal("timeout", luar.New(l, hshtimeout))
@ -58,6 +59,13 @@ func LuaInit() {
hooks = bait.New() hooks = bait.New()
l.PreloadModule("bait", hooks.Loader) l.PreloadModule("bait", hooks.Loader)
// Add Ctrl-C handler
hooks.Em.On("signal.sigint", func() {
if !interactive {
os.Exit(0)
}
})
l.SetGlobal("complete", l.NewFunction(hshcomplete)) l.SetGlobal("complete", l.NewFunction(hshcomplete))
// Add more paths that Lua can require from // Add more paths that Lua can require from
@ -140,7 +148,7 @@ func hshappendPath(L *lua.LState) int {
// if dir isnt already in $PATH, add it // if dir isnt already in $PATH, add it
if !strings.Contains(pathenv, dir) { if !strings.Contains(pathenv, dir) {
os.Setenv("PATH", pathenv + ":" + dir) os.Setenv("PATH", pathenv + string(os.PathListSeparator) + dir)
} }
return 0 return 0
@ -226,3 +234,18 @@ func hshcomplete(L *lua.LState) int {
return 0 return 0
} }
// prependPath(dir)
// Prepends `dir` to $PATH
func hshprependPath(L *lua.LState) int {
dir := L.CheckString(1)
dir = strings.Replace(dir, "~", curuser.HomeDir, 1)
pathenv := os.Getenv("PATH")
// if dir isnt already in $PATH, add in
if !strings.Contains(pathenv, dir) {
os.Setenv("PATH", dir + string(os.PathListSeparator) + pathenv)
}
return 0
}

View File

@ -265,13 +265,12 @@ func HandleSignals() {
for s := range c { for s := range c {
switch s { switch s {
case os.Interrupt: case os.Interrupt:
hooks.Em.Emit("signals.sigint")
if !running { if !running {
if !interactive {
os.Exit(0)
}
lr.ClearInput() lr.ClearInput()
} }
case syscall.SIGWINCH: case syscall.SIGWINCH:
hooks.Em.Emit("signals.resize")
if !running { if !running {
lr.Resize() lr.Resize()
} }

View File

@ -49,41 +49,90 @@ commander.register('doc', function(args)
local moddocPath = hilbish.dataDir .. '/docs/' local moddocPath = hilbish.dataDir .. '/docs/'
local globalDesc = [[ local globalDesc = [[
These are the global Hilbish functions that are always available and not part of a module.]] These are the global Hilbish functions that are always available and not part of a module.]]
local modDocFormat = [[
%s
%s
# Functions
]]
if #args > 0 then if #args > 0 then
local mod = table.concat(args, ' '):gsub('^%s*(.-)%s*$', '%1') local mod = args[1]
local f = io.open(moddocPath .. mod .. '.txt', 'rb') local f = io.open(moddocPath .. mod .. '.txt', 'rb')
local funcdocs = nil
if not f then if not f then
print('Could not find docs for module named ' .. mod .. '.') -- assume subdir
return 1 -- dataDir/docs/<mod>/<mod>.txt
moddocPath = moddocPath .. mod .. '/'
local subdocName = args[2]
if not subdocName then
subdocName = 'index'
end
f = io.open(moddocPath .. subdocName .. '.txt', 'rb')
if not f then
print('No documentation found for ' .. mod .. '.')
return
end
funcdocs = f:read '*a'
local subdocs = table.map(fs.readdir(moddocPath), function(f)
return lunacolors.underline(lunacolors.blue(string.gsub(f, '.txt', '')))
end)
if subdocName == 'index' then
funcdocs = funcdocs .. '\nSubdocs: ' .. table.concat(subdocs, ', ')
end
end end
local desc = (mod == 'global' and globalDesc or getmetatable(require(mod)).__doc) if not funcdocs then
local funcdocs = f:read '*a' funcdocs = f:read '*a'
end
local desc = ''
local ok = pcall(require, mod)
local backtickOccurence = 0 local backtickOccurence = 0
print(desc .. '\n\n' .. lunacolors.format(funcdocs:sub(1, #funcdocs - 1):gsub('`', function() local formattedFuncs = lunacolors.format(funcdocs:sub(1, #funcdocs - 1):gsub('`', function()
backtickOccurence = backtickOccurence + 1 backtickOccurence = backtickOccurence + 1
if backtickOccurence % 2 == 0 then if backtickOccurence % 2 == 0 then
return '{reset}' return '{reset}'
else else
return '{underline}{green}' return '{underline}{green}'
end end
end))) end))
if mod == 'global' or ok then
local props = {}
local propstr = ''
local modDesc = globalDesc
if ok then
local modmt = getmetatable(require(mod))
modDesc = modmt.__doc
if modmt.__docProp then
-- not all modules have docs for properties
props = table.map(modmt.__docProp, function(v, k)
return lunacolors.underline(lunacolors.blue(k)) .. ' > ' .. v
end)
end
end
if #props > 0 then
propstr = '\n# Properties\n' .. table.concat(props, '\n') .. '\n'
end
desc = string.format(modDocFormat, modDesc, propstr)
end
print(desc .. formattedFuncs)
f:close() f:close()
return return
end end
local modules = table.map(fs.readdir(moddocPath), function(f) local modules = table.map(fs.readdir(moddocPath), function(f)
return lunacolors.underline(lunacolors.blue(f:sub(1, -5))) return lunacolors.underline(lunacolors.blue(string.gsub(f, '.txt', '')))
end) end)
io.write [[ io.write [[
Welcome to Hilbish's doc tool! Here you can find documentation for builtin Welcome to Hilbish's doc tool! Here you can find documentation for builtin
functions and other things. functions and other things.
Usage: doc <module> Usage: doc <section> [subdoc]
A section is a module or a literal section and a subdoc is a subsection for it.
Available modules: ]] Available sections: ]]
print(table.concat(modules, ', ')) print(table.concat(modules, ', '))

18
rl.go
View File

@ -8,9 +8,9 @@ package main
// this is normal readline // this is normal readline
import ( import (
"os"
"path/filepath" "path/filepath"
"strings" "strings"
"os"
"github.com/Rosettea/readline" "github.com/Rosettea/readline"
"github.com/yuin/gopher-lua" "github.com/yuin/gopher-lua"
@ -25,12 +25,24 @@ func NewLineReader(prompt string) *LineReader {
var completions []string var completions []string
// trim whitespace from ctx // trim whitespace from ctx
ctx = strings.TrimLeft(ctx, " ") ctx = strings.TrimLeft(ctx, " ")
fields := strings.Split(ctx, " ") fields, ctx := splitInput(ctx)
if len(fields) == 0 { if len(fields) == 0 {
return nil return nil
} }
for aliases[fields[0]] != "" {
alias := aliases[fields[0]]
ctx = alias + strings.TrimPrefix(ctx, fields[0])
fields = strings.Split(ctx, " ")
if aliases[fields[0]] == alias {
break
}
if aliases[fields[0]] != "" {
continue
}
}
if len(fields) == 1 { if len(fields) == 1 {
prefixes := []string{"./", "../"} prefixes := []string{"./", "../"}
for _, prefix := range prefixes { for _, prefix := range prefixes {

View File

@ -124,6 +124,13 @@ func execCommand(cmd string) error {
NRet: 1, NRet: 1,
Protect: true, Protect: true,
}, luacmdArgs) }, luacmdArgs)
if err != nil {
fmt.Fprintln(os.Stderr,
"Error in command:\n\n" + err.Error())
return interp.NewExitStatus(1)
}
luaexitcode := l.Get(-1) luaexitcode := l.Get(-1)
var exitcode uint8 = 0 var exitcode uint8 = 0
@ -133,10 +140,6 @@ func execCommand(cmd string) error {
exitcode = uint8(code) exitcode = uint8(code)
} }
if err != nil {
fmt.Fprintln(os.Stderr,
"Error in command:\n\n" + err.Error())
}
cmdFinish(exitcode, argstring) cmdFinish(exitcode, argstring)
return interp.NewExitStatus(exitcode) return interp.NewExitStatus(exitcode)
} }

View File

@ -2,9 +2,31 @@ package util
import "github.com/yuin/gopher-lua" import "github.com/yuin/gopher-lua"
// Document adds a documentation string to a module.
// It is accessible via the __doc metatable.
func Document(L *lua.LState, module lua.LValue, doc string) { func Document(L *lua.LState, module lua.LValue, doc string) {
mt := L.NewTable() mt := L.GetMetatable(module)
if mt == lua.LNil {
mt = L.NewTable()
L.SetMetatable(module, mt)
}
L.SetField(mt, "__doc", lua.LString(doc)) L.SetField(mt, "__doc", lua.LString(doc))
}
// SetField sets a field in a table, adding docs for it.
// It is accessible via the __docProp metatable. It is a table of the names of the fields.
func SetField(L *lua.LState, module lua.LValue, field string, value lua.LValue, doc string) {
mt := L.GetMetatable(module)
if mt == lua.LNil {
mt = L.NewTable()
docProp := L.NewTable()
L.SetField(mt, "__docProp", docProp)
L.SetMetatable(module, mt) L.SetMetatable(module, mt)
} }
docProp := L.GetTable(mt, lua.LString("__docProp"))
L.SetField(docProp, field, lua.LString(doc))
L.SetField(module, field, value)
}