diff --git a/.hilbishrc.lua b/.hilbishrc.lua index 5d6382b..249f97e 100644 --- a/.hilbishrc.lua +++ b/.hilbishrc.lua @@ -1,18 +1,39 @@ -- Default Hilbish config +local hilbish = require 'hilbish' local lunacolors = require 'lunacolors' local bait = require 'bait' local ansikit = require 'ansikit' +local unreadCount = 0 +local running = false local function doPrompt(fail) hilbish.prompt(lunacolors.format( '{blue}%u {cyan}%d ' .. (fail and '{red}' or '{green}') .. '∆ ' )) end +local function doNotifyPrompt() + if running or unreadCount == hilbish.messages.unreadCount() then return end + + local notifPrompt = string.format('• %s unread notification%s', hilbish.messages.unreadCount(), hilbish.messages.unreadCount() > 1 and 's' or '') + unreadCount = hilbish.messages.unreadCount() + hilbish.prompt(lunacolors.blue(notifPrompt), 'right') + + hilbish.timeout(function() + hilbish.prompt('', 'right') + end, 3000) +end + doPrompt() +bait.catch('command.preexec', function() + running = true +end) + bait.catch('command.exit', function(code) + running = false doPrompt(code ~= 0) + doNotifyPrompt() end) bait.catch('hilbish.vimMode', function(mode) @@ -22,3 +43,7 @@ bait.catch('hilbish.vimMode', function(mode) ansikit.cursorStyle(ansikit.lineCursor) end end) + +bait.catch('hilbish.notification', function(notif) + doNotifyPrompt() +end) diff --git a/CHANGELOG.md b/CHANGELOG.md index f73c4cb..6a5250f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,25 @@ - `read()` method for retrieving input (so now the `in` sink of commanders is useful) - `flush()` and `autoFlush()` related to flushing outputs - `pipe` property to check if a sink with input is a pipe (like stdin) +- Add fuzzy search to history search (enable via `hilbish.opts.fuzzy = true`) - Show indexes on cdr list - Fix doc command not displaying correct subdocs when using shorthand api doc access (`doc api hilbish.jobs` as an example) +- `hilbish.messages` interface (details in [#219]) +- `hilbish.notification` signal when a message/notification is sent +- `notifyJobFinish` opt to send a notification when background jobs are +completed. +- Allow numbered arg substitutions in aliases. + - Example: `hilbish.alias('hello', 'echo %1 says hello')` allows the user to run `hello hilbish` + which will output `hilbish says hello`. - Greenhouse - Greenhouse is a pager library and program. Basic usage is `greenhouse ` - Using this also brings enhancements to the `doc` command like easy navigation of neighboring doc files. +### Fixed +- Return the prefix when calling `hilbish.completions.call` + +[#219]: https://github.com/Rosettea/Hilbish/issues/219 ### Fixed - Replaced `sed` in-place editing with `grep` and `mv` for compatibility with BSD utils diff --git a/aliases.go b/aliases.go index bfacc43..8b815b3 100644 --- a/aliases.go +++ b/aliases.go @@ -1,6 +1,8 @@ package main import ( + "regexp" + "strconv" "strings" "sync" @@ -46,9 +48,32 @@ func (a *aliasModule) Resolve(cmdstr string) string { a.mu.RLock() defer a.mu.RUnlock() - args := strings.Split(cmdstr, " ") + arg, _ := regexp.Compile(`[\\]?%\d+`) + + args, _ := splitInput(cmdstr) + if len(args) == 0 { + // this shouldnt reach but...???? + return cmdstr + } + for a.aliases[args[0]] != "" { alias := a.aliases[args[0]] + alias = arg.ReplaceAllStringFunc(alias, func(a string) string { + idx, _ := strconv.Atoi(a[1:]) + if strings.HasPrefix(a, "\\") || idx == 0 { + return strings.TrimPrefix(a, "\\") + } + + if idx + 1 > len(args) { + return a + } + val := args[idx] + args = cut(args, idx) + cmdstr = strings.Join(args, " ") + + return val + }) + cmdstr = alias + strings.TrimPrefix(cmdstr, args[0]) cmdArgs, _ := splitInput(cmdstr) args = cmdArgs diff --git a/api.go b/api.go index a440693..61aac21 100644 --- a/api.go +++ b/api.go @@ -2,6 +2,7 @@ // The Hilbish module includes the core API, containing // interfaces and functions which directly relate to shell functionality. // #field ver The version of Hilbish +// #field goVersion The version of Go that Hilbish was compiled with // #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 @@ -110,6 +111,7 @@ func hilbishLoad(rtm *rt.Runtime) (rt.Value, func()) { } util.SetFieldProtected(fakeMod, mod, "ver", rt.StringValue(getVersion())) + util.SetFieldProtected(fakeMod, mod, "goVersion", rt.StringValue(runtime.Version())) util.SetFieldProtected(fakeMod, mod, "user", rt.StringValue(username)) util.SetFieldProtected(fakeMod, mod, "host", rt.StringValue(host)) util.SetFieldProtected(fakeMod, mod, "home", rt.StringValue(curuser.HomeDir)) diff --git a/complete.go b/complete.go index 51b426f..0c70e07 100644 --- a/complete.go +++ b/complete.go @@ -253,15 +253,16 @@ func callLuaCompleter(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { } // we must keep the holy 80 cols - completerReturn, err := rt.Call1(l.MainThread(), - rt.FunctionValue(completecb), rt.StringValue(query), - rt.StringValue(ctx), rt.TableValue(fields)) + cont := c.Next() + err = rt.Call(l.MainThread(), rt.FunctionValue(completecb), + []rt.Value{rt.StringValue(query), rt.StringValue(ctx), rt.TableValue(fields)}, + cont) if err != nil { return nil, err } - return c.PushingNext1(t.Runtime, completerReturn), nil + return cont, nil } // #interface completions diff --git a/docs/api/hilbish/_index.md b/docs/api/hilbish/_index.md index bb00b14..a683172 100644 --- a/docs/api/hilbish/_index.md +++ b/docs/api/hilbish/_index.md @@ -13,6 +13,7 @@ interfaces and functions which directly relate to shell functionality. ## Interface fields - `ver`: The version of Hilbish +- `goVersion`: The version of Go that Hilbish was compiled with - `user`: Username of the user - `host`: Hostname of the machine - `dataDir`: Directory for Hilbish data files, including the docs and default modules diff --git a/docs/hooks/command.md b/docs/hooks/command.md index cd1ae3c..2d29f4b 100644 --- a/docs/hooks/command.md +++ b/docs/hooks/command.md @@ -1,3 +1,8 @@ ++ `command.preexec` -> input, cmdStr > Thrown before a command +is executed. The `input` is the user written command, while `cmdStr` +is what will be executed (`input` will have aliases while `cmdStr` +will have alias resolved input). + + `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. diff --git a/docs/hooks/hilbish.md b/docs/hooks/hilbish.md index 3d6d2ea..7118901 100644 --- a/docs/hooks/hilbish.md +++ b/docs/hooks/hilbish.md @@ -7,3 +7,6 @@ like yanking or pasting text. See `doc vim-mode actions` for more info. + `hilbish.cancel` > Sent when the user cancels their input with Ctrl-C. + ++ `hilbish.notification` -> message > Sent when a message is +sent. diff --git a/go.mod b/go.mod index 52b274a..c17d906 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/chuckpreslar/emission v0.0.0-20170206194824-a7ddd980baf9 github.com/maxlandon/readline v0.1.0-beta.0.20211027085530-2b76cabb8036 github.com/pborman/getopt v1.1.0 + github.com/sahilm/fuzzy v0.1.0 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 mvdan.cc/sh/v3 v3.5.1 diff --git a/go.sum b/go.sum index 74a351b..1917008 100644 --- a/go.sum +++ b/go.sum @@ -49,6 +49,8 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.1-0.20210923151022-86f73c517451 h1:d1PiN4RxzIFXCJTvRkvSkKqwtRAl5ZV4lATKtQI0B7I= github.com/rogpeppe/go-internal v1.8.1-0.20210923151022-86f73c517451/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= +github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= +github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4= diff --git a/lua.go b/lua.go index 0a7c115..e46d27b 100644 --- a/lua.go +++ b/lua.go @@ -68,7 +68,7 @@ func luaInit() { } // Add more paths that Lua can require from - err := util.DoString(l, "package.path = package.path .. " + requirePaths) + _, err := util.DoString(l, "package.path = package.path .. " + requirePaths) if err != nil { fmt.Fprintln(os.Stderr, "Could not add Hilbish require paths! Libraries will be missing. This shouldn't happen.") } diff --git a/main.go b/main.go index 4fa321c..90caa47 100644 --- a/main.go +++ b/main.go @@ -106,7 +106,7 @@ func main() { } if *verflag { - fmt.Printf("Hilbish %s\n", getVersion()) + fmt.Printf("Hilbish %s\nCompiled with %s\n", getVersion(), runtime.Version()) os.Exit(0) } @@ -289,7 +289,7 @@ func removeDupes(slice []string) []string { func contains(s []string, e string) bool { for _, a := range s { - if a == e { + if strings.ToLower(a) == strings.ToLower(e) { return true } } @@ -324,3 +324,7 @@ func getVersion() string { return v.String() } + +func cut(slice []string, idx int) []string { + return append(slice[:idx], slice[idx + 1:]...) +} diff --git a/nature/hummingbird.lua b/nature/hummingbird.lua new file mode 100644 index 0000000..581e92c --- /dev/null +++ b/nature/hummingbird.lua @@ -0,0 +1,84 @@ +local bait = require 'bait' +local commander = require 'commander' +local lunacolors = require 'lunacolors' + +local M = {} +local counter = 0 +local unread = 0 +M._messages = {} +M.icons = { + INFO = '', + SUCCESS = '', + WARN = '', + ERROR = '' +} + +hilbish.messages = {} + +--- Represents a Hilbish message. +--- @class hilbish.message +--- @field icon string Unicode (preferably standard emoji) icon for the message notification. +--- @field title string Title of the message (like an email subject). +--- @field text string Contents of the message. +--- @field channel string Short identifier of the message. `hilbish` and `hilbish.*` is preserved for internal Hilbish messages. +--- @field summary string A short summary of the message. +--- @field read boolean Whether the full message has been read or not. + +function expect(tbl, field) + if not tbl[field] or tbl[field] == '' then + error(string.format('expected field %s in message')) + end +end + +--- Sends a message. +--- @param message hilbish.message +function hilbish.messages.send(message) + expect(message, 'text') + expect(message, 'title') + counter = counter + 1 + unread = unread + 1 + message.index = counter + message.read = false + + M._messages[message.index] = message + bait.throw('hilbish.notification', message) +end + +function hilbish.messages.read(idx) + local msg = M._messages[idx] + if msg then + M._messages[idx].read = true + unread = unread - 1 + end +end + +function hilbish.messages.readAll(idx) + for _, msg in ipairs(hilbish.messages.all()) do + hilbish.messages.read(msg.index) + end +end + +function hilbish.messages.unreadCount() + return unread +end + +function hilbish.messages.delete(idx) + local msg = M._messages[idx] + if not msg then + error(string.format('invalid message index %d', idx or -1)) + end + + M._messages[idx] = nil +end + +function hilbish.messages.clear() + for _, msg in ipairs(hilbish.messages.all()) do + hilbish.messages.delete(msg.index) + end +end + +function hilbish.messages.all() + return M._messages +end + +return M diff --git a/nature/init.lua b/nature/init.lua index d1f919c..9e78135 100644 --- a/nature/init.lua +++ b/nature/init.lua @@ -11,6 +11,7 @@ require 'nature.completions' require 'nature.opts' require 'nature.vim' require 'nature.runner' +require 'nature.hummingbird' local shlvl = tonumber(os.getenv 'SHLVL') if shlvl ~= nil then diff --git a/nature/opts/init.lua b/nature/opts/init.lua index ae95ee1..56c34ba 100644 --- a/nature/opts/init.lua +++ b/nature/opts/init.lua @@ -16,7 +16,7 @@ setmetatable(hilbish.opts, { local function setupOpt(name, default) opts[name] = default - require('nature.opts.' .. name) + pcall(require, 'nature.opts.' .. name) end local defaultOpts = { @@ -25,7 +25,9 @@ local defaultOpts = { greeting = string.format([[Welcome to {magenta}Hilbish{reset}, {cyan}%s{reset}. The nice lil shell for {blue}Lua{reset} fanatics! ]], hilbish.user), - motd = true + motd = true, + fuzzy = false, + notifyJobFinish = true } for optsName, default in pairs(defaultOpts) do diff --git a/nature/opts/notifyJobFinish.lua b/nature/opts/notifyJobFinish.lua new file mode 100644 index 0000000..a8841a1 --- /dev/null +++ b/nature/opts/notifyJobFinish.lua @@ -0,0 +1,23 @@ +local bait = require 'bait' +local lunacolors = require 'lunacolors' + +bait.catch('job.done', function(job) + if not hilbish.opts.notifyJobFinish then return end + local notifText = string.format(lunacolors.format [[ +Background job with ID#%d has exited (PID %d). +Command string: {bold}{yellow}%s{reset}]], job.id, job.pid, job.cmd) + + if job.stdout ~= '' then + notifText = notifText .. '\n\nStandard output:\n' .. job.stdout + end + if job.stderr ~= '' then + notifText = notifText .. '\n\nStandard error:\n' .. job.stderr + end + + hilbish.messages.send { + channel = 'jobNotify', + title = string.format('Job ID#%d Exited', job.id), + summary = string.format(lunacolors.format 'Background job with command {bold}{yellow}%s{reset} has finished running!', job.cmd), + text = notifText + } +end) diff --git a/readline/comp-group.go b/readline/comp-group.go index 0c53ed1..b2ee4b8 100644 --- a/readline/comp-group.go +++ b/readline/comp-group.go @@ -71,10 +71,9 @@ func (g *CompletionGroup) init(rl *Instance) { // The rx parameter is passed, as the shell already checked that the search pattern is valid. func (g *CompletionGroup) updateTabFind(rl *Instance) { - suggs := make([]string, 0) - + suggs := rl.Searcher(rl.search, g.Suggestions) // We perform filter right here, so we create a new completion group, and populate it with our results. - for i := range g.Suggestions { + /*for i := range g.Suggestions { if rl.regexSearch == nil { continue } if rl.regexSearch.MatchString(g.Suggestions[i]) { suggs = append(suggs, g.Suggestions[i]) @@ -82,7 +81,7 @@ func (g *CompletionGroup) updateTabFind(rl *Instance) { // this is a list so lets also check the descriptions suggs = append(suggs, g.Suggestions[i]) } - } + }*/ // We overwrite the group's items, (will be refreshed as soon as something is typed in the search) g.Suggestions = suggs diff --git a/readline/instance.go b/readline/instance.go index 039f040..a477246 100644 --- a/readline/instance.go +++ b/readline/instance.go @@ -112,8 +112,10 @@ type Instance struct { modeAutoFind bool // for when invoked via ^R or ^F outside of [tab] searchMode FindMode // Used for varying hints, and underlying functions called regexSearch *regexp.Regexp // Holds the current search regex match + search string mainHist bool // Which history stdin do we want histInfo []rune // We store a piece of hist info, for dual history sources + Searcher func(string, []string) []string // // History ----------------------------------------------------------------------------------- @@ -229,6 +231,25 @@ func NewInstance() *Instance { rl.HintFormatting = "\x1b[2m" rl.evtKeyPress = make(map[string]func(string, []rune, int) *EventReturn) rl.TempDirectory = os.TempDir() + rl.Searcher = func(needle string, haystack []string) []string { + suggs := make([]string, 0) + + var err error + rl.regexSearch, err = regexp.Compile("(?i)" + string(rl.tfLine)) + if err != nil { + rl.RefreshPromptLog(err.Error()) + rl.infoText = []rune(Red("Failed to match search regexp")) + } + + for _, hay := range haystack { + if rl.regexSearch == nil { continue } + if rl.regexSearch.MatchString(hay) { + suggs = append(suggs, hay) + } + } + + return suggs + } // Registers rl.initRegisters() diff --git a/readline/tab.go b/readline/tab.go index e6522e6..d00decc 100644 --- a/readline/tab.go +++ b/readline/tab.go @@ -94,7 +94,7 @@ func (rl *Instance) getTabSearchCompletion() { rl.getCurrentGroup() // Set the info for this completion mode - rl.infoText = append([]rune("Completion search: "), rl.tfLine...) + rl.infoText = append([]rune("Completion search: " + UNDERLINE + BOLD), rl.tfLine...) for _, g := range rl.tcGroups { g.updateTabFind(rl) @@ -102,7 +102,7 @@ func (rl *Instance) getTabSearchCompletion() { // If total number of matches is zero, we directly change the info, and return if comps, _, _ := rl.getCompletionCount(); comps == 0 { - rl.infoText = append(rl.infoText, []rune(DIM+RED+" ! no matches (Ctrl-G/Esc to cancel)"+RESET)...) + rl.infoText = append(rl.infoText, []rune(RESET+DIM+RED+" ! no matches (Ctrl-G/Esc to cancel)"+RESET)...) } } diff --git a/readline/tabfind.go b/readline/tabfind.go index aa38259..830dad3 100644 --- a/readline/tabfind.go +++ b/readline/tabfind.go @@ -1,9 +1,5 @@ package readline -import ( - "regexp" -) - // FindMode defines how the autocomplete suggestions display type FindMode int @@ -30,12 +26,7 @@ func (rl *Instance) updateTabFind(r []rune) { rl.tfLine = append(rl.tfLine, r...) // The search regex is common to all search modes - var err error - rl.regexSearch, err = regexp.Compile("(?i)" + string(rl.tfLine)) - if err != nil { - rl.RefreshPromptLog(err.Error()) - rl.infoText = []rune(Red("Failed to match search regexp")) - } + rl.search = string(rl.tfLine) // We update and print rl.clearHelpers() diff --git a/readline/tui-effects.go b/readline/tui-effects.go index 491ef98..5610b10 100644 --- a/readline/tui-effects.go +++ b/readline/tui-effects.go @@ -14,6 +14,7 @@ var ( // effects BOLD = "\033[1m" DIM = "\033[2m" + UNDERLINE = "\033[4m" RESET = "\033[0m" // colors RED = "\033[31m" diff --git a/rl.go b/rl.go index 96b8451..17ea4df 100644 --- a/rl.go +++ b/rl.go @@ -7,8 +7,9 @@ import ( "hilbish/util" - "github.com/maxlandon/readline" rt "github.com/arnodel/golua/runtime" + "github.com/maxlandon/readline" + "github.com/sahilm/fuzzy" ) type lineReader struct { @@ -24,6 +25,24 @@ func newLineReader(prompt string, noHist bool) *lineReader { rl: rl, } + regexSearcher := rl.Searcher + rl.Searcher = func(needle string, haystack []string) []string { + fz, _ := util.DoString(l, "return hilbish.opts.fuzzy") + fuzz, ok := fz.TryBool() + if !fuzz || !ok { + return regexSearcher(needle, haystack) + } + + matches := fuzzy.Find(needle, haystack) + suggs := make([]string, 0) + + for _, match := range matches { + suggs = append(suggs, match.Str) + } + + return suggs + } + // we don't mind hilbish.read rl instances having completion, // but it cant have shared history if !noHist { diff --git a/util/util.go b/util/util.go index 45e33dc..0fcd4b0 100644 --- a/util/util.go +++ b/util/util.go @@ -26,13 +26,14 @@ func SetFieldProtected(module, realModule *rt.Table, field string, value rt.Valu } // DoString runs the code string in the Lua runtime. -func DoString(rtm *rt.Runtime, code string) error { +func DoString(rtm *rt.Runtime, code string) (rt.Value, error) { chunk, err := rtm.CompileAndLoadLuaChunk("", []byte(code), rt.TableValue(rtm.GlobalEnv())) + var ret rt.Value if chunk != nil { - _, err = rt.Call1(rtm.MainThread(), rt.FunctionValue(chunk)) + ret, err = rt.Call1(rtm.MainThread(), rt.FunctionValue(chunk)) } - return err + return ret, err } // DoFile runs the contents of the file in the Lua runtime. diff --git a/website/content/docs/features/notifications.md b/website/content/docs/features/notifications.md new file mode 100644 index 0000000..c3a9b53 --- /dev/null +++ b/website/content/docs/features/notifications.md @@ -0,0 +1,39 @@ +--- +title: Notification +description: Get notified of shell actions. +layout: doc +menu: + docs: + parent: "Features" +--- + +Hilbish features a simple notification system which can be +used by other plugins and parts of the shell to notify the user +of various actions. This is used via the `hilbish.message` interface. + +A `message` is defined as a table with the following properties: +- `icon`: A unicode/emoji icon for the notification. +- `title`: The title of the message +- `text`: Message text/body +- `channel`: The source of the message. This should be a +unique and easily readable text identifier. +- `summary`: A short summary of the notification and message. +If this is not present and you are using this to display messages, +you should take part of the `text` instead. + +The `hilbish.message` interface provides the following functions: +- `send(message)`: Sends a message and emits the `hilbish.notification` +signal. DO NOT emit the `hilbish.notification` signal directly, or +the message will not be stored by the message handler. +- `read(idx)`: Marks message at `idx` as read. +- `delete(idx)`: Removes message at `idx`. +- `readAll()`: Marks all messages as read. +- `clear()`: Deletes all messages. + +There are a few simple use cases of this notification/messaging system. +It could also be used as some "inter-shell" messaging system (???) but +is intended to display to users. + +An example is notifying users of completed jobs/commands ran in the background. +Any Hilbish-native command (think the upcoming Greenhouse pager) can display +it. diff --git a/website/content/docs/features/runner-mode.md b/website/content/docs/features/runner-mode.md index 8774de9..58b55dd 100644 --- a/website/content/docs/features/runner-mode.md +++ b/website/content/docs/features/runner-mode.md @@ -13,8 +13,8 @@ is that it runs Lua first and then falls back to shell script. In some cases, someone might want to switch to just shell script to avoid it while interactive but still have a Lua config, or go full Lua to use -Hilbish as a REPL. This also allows users to add alternative languages, -instead of either like Fennel. +Hilbish as a REPL. This also allows users to add alternative languages like +Fennel as the interactive script runner. Runner mode can also be used to handle specific kinds of input before evaluating like normal, which is how [Link.hsh](https://github.com/TorchedSammy/Link.hsh)