Compare commits

..

21 Commits

Author SHA1 Message Date
sammyette e288826be4
chore: merge from master 2023-10-25 00:25:33 -04:00
sammyette ae356f6ffa
docs: add details to changelog 2023-10-25 00:24:17 -04:00
sammyette d8a7a4332d
fix(commands/greenhouse): remove extra space in title, set pipe page title to stdin 2023-10-25 00:00:59 -04:00
sammyette 8fabdf4502
fix: correct no files check to compensate for pipes 2023-10-24 23:59:53 -04:00
sammyette 003100d3c4
fix: dont concat subdocs list if on main page 2023-10-24 23:52:53 -04:00
sammyette 7288f85e9a
feat(commands/doc): add subdocs as additional pages 2023-10-24 23:51:42 -04:00
sammyette a5b6fc8eda
fix: return cmdstr in alias resolve instead of nothing 2023-09-30 23:25:15 -04:00
sammyette 4421869b85
fix: check length of args in aliases resolve
this shouldnt be a problem.. but ... what
2023-09-30 23:21:00 -04:00
sammyette 6d07d8db53
chore: merge from upstream 2023-09-30 19:53:38 -04:00
sammyette 483e5f6dbe
fix(hilbish.completions): return prefix on the `call` function (from #263) 2023-09-30 19:52:33 -04:00
sammyette 83492e4e69
docs: fix odd sentence in runner-mode docs 2023-09-27 20:31:27 -04:00
sammyette 4fcd310048
chore: merge from upstream 2023-09-09 13:53:22 -04:00
sammyette db8e87e5dd
fix: compare lower case strings in contains function
this fixes file completion on windows.
2023-09-09 13:53:09 -04:00
sammyette ba04fb2af7
feat: allow numbered substitutions in aliases (#258) 2023-09-09 13:51:58 -04:00
sammyette caff604d95
ci: stop using deprecated method of setting branch name 2023-07-10 22:42:04 -04:00
sammyette 2b9059f726
ci: add missing quote 2023-07-10 22:38:22 -04:00
sammyette 00c967d7c1
ci: fix branch name 2023-07-10 22:38:13 -04:00
TorchedSammy ab26b82a9f docs: [ci] generate new docs 2023-07-11 01:57:00 +00:00
sammyette f540fc2c04
feat: show go version hilbish was compiled with 2023-07-10 21:56:35 -04:00
sammyette c6c81ddece
feat: add notification/message interface (#239)
details in #219
2023-07-10 19:03:30 -04:00
sammyette 3eae0f07be
feat: add fuzzy searching for completion and history search (#247)
* feat: add fuzzy searching for completion and history search

* feat: add fuzzy opt for fuzzy history searching

* chore: add fuzzy opt to changelog
2023-07-10 00:06:29 -04:00
29 changed files with 400 additions and 87 deletions

View File

@ -1,18 +1,39 @@
-- Default Hilbish config -- Default Hilbish config
local hilbish = require 'hilbish'
local lunacolors = require 'lunacolors' local lunacolors = require 'lunacolors'
local bait = require 'bait' local bait = require 'bait'
local ansikit = require 'ansikit' local ansikit = require 'ansikit'
local unreadCount = 0
local running = false
local function doPrompt(fail) local function doPrompt(fail)
hilbish.prompt(lunacolors.format( hilbish.prompt(lunacolors.format(
'{blue}%u {cyan}%d ' .. (fail and '{red}' or '{green}') .. '' '{blue}%u {cyan}%d ' .. (fail and '{red}' or '{green}') .. ''
)) ))
end 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() doPrompt()
bait.catch('command.preexec', function()
running = true
end)
bait.catch('command.exit', function(code) bait.catch('command.exit', function(code)
running = false
doPrompt(code ~= 0) doPrompt(code ~= 0)
doNotifyPrompt()
end) end)
bait.catch('hilbish.vimMode', function(mode) bait.catch('hilbish.vimMode', function(mode)
@ -22,3 +43,7 @@ bait.catch('hilbish.vimMode', function(mode)
ansikit.cursorStyle(ansikit.lineCursor) ansikit.cursorStyle(ansikit.lineCursor)
end end
end) end)
bait.catch('hilbish.notification', function(notif)
doNotifyPrompt()
end)

View File

@ -6,9 +6,25 @@
- `read()` method for retrieving input (so now the `in` sink of commanders is useful) - `read()` method for retrieving input (so now the `in` sink of commanders is useful)
- `flush()` and `autoFlush()` related to flushing outputs - `flush()` and `autoFlush()` related to flushing outputs
- `pipe` property to check if a sink with input is a pipe (like stdin) - `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 - 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) - 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 <file>`
- 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 ### Fixed
- Replaced `sed` in-place editing with `grep` and `mv` for compatibility with BSD utils - Replaced `sed` in-place editing with `grep` and `mv` for compatibility with BSD utils

View File

@ -1,6 +1,8 @@
package main package main
import ( import (
"regexp"
"strconv"
"strings" "strings"
"sync" "sync"
@ -46,9 +48,32 @@ func (a *aliasModule) Resolve(cmdstr string) string {
a.mu.RLock() a.mu.RLock()
defer a.mu.RUnlock() 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]] != "" { for a.aliases[args[0]] != "" {
alias := 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]) cmdstr = alias + strings.TrimPrefix(cmdstr, args[0])
cmdArgs, _ := splitInput(cmdstr) cmdArgs, _ := splitInput(cmdstr)
args = cmdArgs args = cmdArgs

2
api.go
View File

@ -2,6 +2,7 @@
// The Hilbish module includes the core API, containing // The Hilbish module includes the core API, containing
// interfaces and functions which directly relate to shell functionality. // interfaces and functions which directly relate to shell functionality.
// #field ver The version of Hilbish // #field ver The version of Hilbish
// #field goVersion The version of Go that Hilbish was compiled with
// #field user Username of the user // #field user Username of the user
// #field host Hostname of the machine // #field host Hostname of the machine
// #field dataDir Directory for Hilbish data files, including the docs and default modules // #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, "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, "user", rt.StringValue(username))
util.SetFieldProtected(fakeMod, mod, "host", rt.StringValue(host)) util.SetFieldProtected(fakeMod, mod, "host", rt.StringValue(host))
util.SetFieldProtected(fakeMod, mod, "home", rt.StringValue(curuser.HomeDir)) util.SetFieldProtected(fakeMod, mod, "home", rt.StringValue(curuser.HomeDir))

View File

@ -253,15 +253,16 @@ func callLuaCompleter(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
} }
// we must keep the holy 80 cols // we must keep the holy 80 cols
completerReturn, err := rt.Call1(l.MainThread(), cont := c.Next()
rt.FunctionValue(completecb), rt.StringValue(query), err = rt.Call(l.MainThread(), rt.FunctionValue(completecb),
rt.StringValue(ctx), rt.TableValue(fields)) []rt.Value{rt.StringValue(query), rt.StringValue(ctx), rt.TableValue(fields)},
cont)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return c.PushingNext1(t.Runtime, completerReturn), nil return cont, nil
} }
// #interface completions // #interface completions

View File

@ -13,6 +13,7 @@ interfaces and functions which directly relate to shell functionality.
## Interface fields ## Interface fields
- `ver`: The version of Hilbish - `ver`: The version of Hilbish
- `goVersion`: The version of Go that Hilbish was compiled with
- `user`: Username of the user - `user`: Username of the user
- `host`: Hostname of the machine - `host`: Hostname of the machine
- `dataDir`: Directory for Hilbish data files, including the docs and default modules - `dataDir`: Directory for Hilbish data files, including the docs and default modules

View File

@ -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. + `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. `code` is the exit code of the command, and `cmdStr` is the command that was run.

View File

@ -7,3 +7,6 @@
like yanking or pasting text. See `doc vim-mode actions` for more info. 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.cancel` > Sent when the user cancels their input with Ctrl-C.
+ `hilbish.notification` -> message > Sent when a message is
sent.

1
go.mod
View File

@ -8,6 +8,7 @@ require (
github.com/chuckpreslar/emission v0.0.0-20170206194824-a7ddd980baf9 github.com/chuckpreslar/emission v0.0.0-20170206194824-a7ddd980baf9
github.com/maxlandon/readline v0.1.0-beta.0.20211027085530-2b76cabb8036 github.com/maxlandon/readline v0.1.0-beta.0.20211027085530-2b76cabb8036
github.com/pborman/getopt v1.1.0 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/sys v0.0.0-20220520151302-bc2c85ada10a
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 golang.org/x/term v0.0.0-20220411215600-e5f449aeb171
mvdan.cc/sh/v3 v3.5.1 mvdan.cc/sh/v3 v3.5.1

2
go.sum
View File

@ -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.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 h1:d1PiN4RxzIFXCJTvRkvSkKqwtRAl5ZV4lATKtQI0B7I=
github.com/rogpeppe/go-internal v1.8.1-0.20210923151022-86f73c517451/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= 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 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-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4= golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4=

2
lua.go
View File

@ -68,7 +68,7 @@ func luaInit() {
} }
// Add more paths that Lua can require from // 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 { if err != nil {
fmt.Fprintln(os.Stderr, "Could not add Hilbish require paths! Libraries will be missing. This shouldn't happen.") fmt.Fprintln(os.Stderr, "Could not add Hilbish require paths! Libraries will be missing. This shouldn't happen.")
} }

View File

@ -106,7 +106,7 @@ func main() {
} }
if *verflag { if *verflag {
fmt.Printf("Hilbish %s\n", getVersion()) fmt.Printf("Hilbish %s\nCompiled with %s\n", getVersion(), runtime.Version())
os.Exit(0) os.Exit(0)
} }
@ -289,7 +289,7 @@ func removeDupes(slice []string) []string {
func contains(s []string, e string) bool { func contains(s []string, e string) bool {
for _, a := range s { for _, a := range s {
if a == e { if strings.ToLower(a) == strings.ToLower(e) {
return true return true
} }
} }
@ -324,3 +324,7 @@ func getVersion() string {
return v.String() return v.String()
} }
func cut(slice []string, idx int) []string {
return append(slice[:idx], slice[idx + 1:]...)
}

View File

@ -23,12 +23,36 @@ to Hilbish.
Usage: doc <section> [subdoc] Usage: doc <section> [subdoc]
Available sections: ]] .. table.concat(modules, ', ') Available sections: ]] .. table.concat(modules, ', ')
local f
local function handleYamlInfo(d)
local vals = {} local vals = {}
local docs = d
local valsStr = docs:match '%-%-%-\n([^%-%-%-]+)\n'
print(valsStr)
if valsStr then
docs = docs:sub(valsStr:len() + 10, #docs)
-- parse vals
local lines = string.split(valsStr, '\n')
for _, line in ipairs(lines) do
local key = line:match '(%w+): '
local val = line:match '^%w+: (.-)$'
if key then
vals[key] = val
end
end
end
--docs = docs:sub(1, #docs - 1)
return docs, vals
end
if #args > 0 then if #args > 0 then
local mod = args[1] local mod = args[1]
local f = io.open(moddocPath .. mod .. '.md', 'rb') f = io.open(moddocPath .. mod .. '.md', 'rb')
local funcdocs = nil local funcdocs = nil
local subdocName = args[2] local subdocName = args[2]
if not f then if not f then
@ -52,34 +76,13 @@ Available sections: ]] .. table.concat(modules, ', ')
return 1 return 1
end end
end end
funcdocs = f:read '*a':gsub('-([%d]+)', '%1')
end
local moddocs = table.filter(fs.readdir(moddocPath), function(f) return f ~= '_index.md' and f ~= 'index.md' end) local moddocs = table.filter(fs.readdir(moddocPath), function(f) return f ~= '_index.md' and f ~= 'index.md' end)
local subdocs = table.map(moddocs, function(fname) local subdocs = table.map(moddocs, function(fname)
return lunacolors.underline(lunacolors.blue(string.gsub(fname, '.md', ''))) return lunacolors.underline(lunacolors.blue(string.gsub(fname, '.md', '')))
end) end)
if #moddocs ~= 0 then
funcdocs = funcdocs .. '\nSubdocs: ' .. table.concat(subdocs, ', ') .. '\n\n'
end
local valsStr = funcdocs:match '%-%-%-\n([^%-%-%-]+)\n'
if valsStr then
local _, endpos = funcdocs:find('---\n' .. valsStr .. '\n---\n\n', 1, true)
funcdocs = funcdocs:sub(endpos + 1, #funcdocs)
-- parse vals
local lines = string.split(valsStr, '\n')
for _, line in ipairs(lines) do
local key = line:match '(%w+): '
local val = line:match '^%w+: (.-)$'
if key then
vals[key] = val
end
end
end
doc = funcdocs:sub(1, #funcdocs - 1)
f:close()
end
local gh = Greenhouse(sinks.out) local gh = Greenhouse(sinks.out)
function gh:resize() function gh:resize()
@ -102,15 +105,16 @@ Available sections: ]] .. table.concat(modules, ', ')
self.sink:write(ansikit.getCSI(self.region.height + 2 .. ';1', 'H')) self.sink:write(ansikit.getCSI(self.region.height + 2 .. ';1', 'H'))
if not self.isSpecial then if not self.isSpecial then
if args[1] == 'api' then if args[1] == 'api' then
self.sink:writeln(lunacolors.reset(string.format('%s', vals.title))) self.sink:writeln(lunacolors.reset(string.format('%s', workingPage.title)))
self.sink:write(lunacolors.format(string.format('{grayBg} ↳ {white}{italic}%s {reset}', vals.description or 'No description.'))) self.sink:write(lunacolors.format(string.format('{grayBg} ↳ {white}{italic}%s {reset}', workingPage.description or 'No description.')))
else else
self.sink:write(lunacolors.reset(string.format('Viewing doc page %s', moddocPath))) self.sink:write(lunacolors.reset(string.format('Viewing doc page %s', moddocPath)))
end end
end end
end end
local backtickOccurence = 0 local backtickOccurence = 0
local page = Page(nil, lunacolors.format(doc:gsub('`', function() local function formatDocText(d)
return lunacolors.format(d:gsub('`', function()
backtickOccurence = backtickOccurence + 1 backtickOccurence = backtickOccurence + 1
if backtickOccurence % 2 == 0 then if backtickOccurence % 2 == 0 then
return '{reset}' return '{reset}'
@ -120,8 +124,33 @@ Available sections: ]] .. table.concat(modules, ', ')
end):gsub('\n#+.-\n', function(t) end):gsub('\n#+.-\n', function(t)
local signature = t:gsub('<.->(.-)</.->', '{underline}%1'):gsub('\\', '<') local signature = t:gsub('<.->(.-)</.->', '{underline}%1'):gsub('\\', '<')
return '{bold}{yellow}' .. signature .. '{reset}' return '{bold}{yellow}' .. signature .. '{reset}'
end))) end))
end
local doc, vals = handleYamlInfo(#args == 0 and doc or formatDocText(f:read '*a':gsub('-([%d]+)', '%1')))
if #moddocs ~= 0 and f then
doc = doc .. '\nSubdocs: ' .. table.concat(subdocs, ', ') .. '\n\n'
end
if f then f:close() end
local page = Page(vals.title, doc)
page.description = vals.description
gh:addPage(page) gh:addPage(page)
-- add subdoc pages
for _, sdName in ipairs(moddocs) do
local sdFile = fs.join(sdName, '_index.md')
if sdName:match '.md$' then
sdFile = sdName
end
local f = io.open(moddocPath .. sdFile, 'rb')
local doc, vals = handleYamlInfo(f:read '*a':gsub('-([%d]+)', '%1'))
local page = Page(vals.title, formatDocText(doc))
page.description = vals.description
gh:addPage(page)
end
ansikit.hideCursor() ansikit.hideCursor()
gh:initUi() gh:initUi()
end) end)

View File

@ -98,27 +98,27 @@ commander.register('greenhouse', function(args, sinks)
end) end)
if sinks['in'].pipe then if sinks['in'].pipe then
local page = Page('', sinks['in']:readAll()) local page = Page('stdin', sinks['in']:readAll())
gh:addPage(page) gh:addPage(page)
end end
if #args ~= 0 then
for _, name in ipairs(args) do for _, name in ipairs(args) do
local f <close> = io.open(name, 'r') local f <close> = io.open(name, 'r')
if not f then if not f then
sinks.err:writeln(string.format('could not open file %s', name)) sinks.err:writeln(string.format('could not open file %s', name))
end end
local page = Page(name, f:read '*a') local page = Page(name, f:read '*a')
gh:addPage(page) gh:addPage(page)
end end
ansikit.hideCursor()
gh:initUi() if #gh.pages == 0 then
else
sinks.out:writeln [[greenhouse is the Hilbish pager library and command! sinks.out:writeln [[greenhouse is the Hilbish pager library and command!
usage: greenhouse <file>... usage: greenhouse <file>...
example: greenhouse hello.md]] example: greenhouse hello.md]]
return 1
end end
ansikit.hideCursor()
gh:initUi()
end) end)

View File

@ -74,6 +74,10 @@ function Greenhouse:draw()
workingPage = self.specialPage workingPage = self.specialPage
end end
if workingPage.lazy and not workingPage.loaded then
workingPage.initialize()
end
local lines = workingPage.lines local lines = workingPage.lines
self.sink:write(ansikit.getCSI(self.start .. ';1', 'H')) self.sink:write(ansikit.getCSI(self.start .. ';1', 'H'))
self.sink:write(ansikit.getCSI(2, 'J')) self.sink:write(ansikit.getCSI(2, 'J'))

View File

@ -5,6 +5,9 @@ local Page = Object:extend()
function Page:new(title, text) function Page:new(title, text)
self:setText(text) self:setText(text)
self.title = title or 'Page' self.title = title or 'Page'
self.lazy = false
self.loaded = true
self.children = {}
end end
function Page:setText(text) function Page:setText(text)
@ -15,4 +18,15 @@ function Page:setTitle(title)
self.title = title self.title = title
end end
function Page:dynamic(initializer)
self.initializer = initializer
self.lazy = true
self.loaded = false
end
function Page:initialize()
self.initializer()
self.loaded = true
end
return Page return Page

View File

@ -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

View File

@ -11,6 +11,7 @@ require 'nature.completions'
require 'nature.opts' require 'nature.opts'
require 'nature.vim' require 'nature.vim'
require 'nature.runner' require 'nature.runner'
require 'nature.hummingbird'
local shlvl = tonumber(os.getenv 'SHLVL') local shlvl = tonumber(os.getenv 'SHLVL')
if shlvl ~= nil then if shlvl ~= nil then

View File

@ -16,7 +16,7 @@ setmetatable(hilbish.opts, {
local function setupOpt(name, default) local function setupOpt(name, default)
opts[name] = default opts[name] = default
require('nature.opts.' .. name) pcall(require, 'nature.opts.' .. name)
end end
local defaultOpts = { local defaultOpts = {
@ -25,7 +25,9 @@ local defaultOpts = {
greeting = string.format([[Welcome to {magenta}Hilbish{reset}, {cyan}%s{reset}. greeting = string.format([[Welcome to {magenta}Hilbish{reset}, {cyan}%s{reset}.
The nice lil shell for {blue}Lua{reset} fanatics! The nice lil shell for {blue}Lua{reset} fanatics!
]], hilbish.user), ]], hilbish.user),
motd = true motd = true,
fuzzy = false,
notifyJobFinish = true
} }
for optsName, default in pairs(defaultOpts) do for optsName, default in pairs(defaultOpts) do

View File

@ -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)

View File

@ -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. // The rx parameter is passed, as the shell already checked that the search pattern is valid.
func (g *CompletionGroup) updateTabFind(rl *Instance) { 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. // 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 == nil { continue }
if rl.regexSearch.MatchString(g.Suggestions[i]) { if rl.regexSearch.MatchString(g.Suggestions[i]) {
suggs = append(suggs, 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 // this is a list so lets also check the descriptions
suggs = append(suggs, g.Suggestions[i]) suggs = append(suggs, g.Suggestions[i])
} }
} }*/
// We overwrite the group's items, (will be refreshed as soon as something is typed in the search) // We overwrite the group's items, (will be refreshed as soon as something is typed in the search)
g.Suggestions = suggs g.Suggestions = suggs

View File

@ -112,8 +112,10 @@ type Instance struct {
modeAutoFind bool // for when invoked via ^R or ^F outside of [tab] modeAutoFind bool // for when invoked via ^R or ^F outside of [tab]
searchMode FindMode // Used for varying hints, and underlying functions called searchMode FindMode // Used for varying hints, and underlying functions called
regexSearch *regexp.Regexp // Holds the current search regex match regexSearch *regexp.Regexp // Holds the current search regex match
search string
mainHist bool // Which history stdin do we want mainHist bool // Which history stdin do we want
histInfo []rune // We store a piece of hist info, for dual history sources histInfo []rune // We store a piece of hist info, for dual history sources
Searcher func(string, []string) []string
// //
// History ----------------------------------------------------------------------------------- // History -----------------------------------------------------------------------------------
@ -229,6 +231,25 @@ func NewInstance() *Instance {
rl.HintFormatting = "\x1b[2m" rl.HintFormatting = "\x1b[2m"
rl.evtKeyPress = make(map[string]func(string, []rune, int) *EventReturn) rl.evtKeyPress = make(map[string]func(string, []rune, int) *EventReturn)
rl.TempDirectory = os.TempDir() 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 // Registers
rl.initRegisters() rl.initRegisters()

View File

@ -94,7 +94,7 @@ func (rl *Instance) getTabSearchCompletion() {
rl.getCurrentGroup() rl.getCurrentGroup()
// Set the info for this completion mode // 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 { for _, g := range rl.tcGroups {
g.updateTabFind(rl) 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 total number of matches is zero, we directly change the info, and return
if comps, _, _ := rl.getCompletionCount(); comps == 0 { 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)...)
} }
} }

View File

@ -1,9 +1,5 @@
package readline package readline
import (
"regexp"
)
// FindMode defines how the autocomplete suggestions display // FindMode defines how the autocomplete suggestions display
type FindMode int type FindMode int
@ -30,12 +26,7 @@ func (rl *Instance) updateTabFind(r []rune) {
rl.tfLine = append(rl.tfLine, r...) rl.tfLine = append(rl.tfLine, r...)
// The search regex is common to all search modes // The search regex is common to all search modes
var err error rl.search = string(rl.tfLine)
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"))
}
// We update and print // We update and print
rl.clearHelpers() rl.clearHelpers()

View File

@ -14,6 +14,7 @@ var (
// effects // effects
BOLD = "\033[1m" BOLD = "\033[1m"
DIM = "\033[2m" DIM = "\033[2m"
UNDERLINE = "\033[4m"
RESET = "\033[0m" RESET = "\033[0m"
// colors // colors
RED = "\033[31m" RED = "\033[31m"

21
rl.go
View File

@ -7,8 +7,9 @@ import (
"hilbish/util" "hilbish/util"
"github.com/maxlandon/readline"
rt "github.com/arnodel/golua/runtime" rt "github.com/arnodel/golua/runtime"
"github.com/maxlandon/readline"
"github.com/sahilm/fuzzy"
) )
type lineReader struct { type lineReader struct {
@ -24,6 +25,24 @@ func newLineReader(prompt string, noHist bool) *lineReader {
rl: rl, 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, // we don't mind hilbish.read rl instances having completion,
// but it cant have shared history // but it cant have shared history
if !noHist { if !noHist {

View File

@ -26,13 +26,14 @@ func SetFieldProtected(module, realModule *rt.Table, field string, value rt.Valu
} }
// DoString runs the code string in the Lua runtime. // 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("<string>", []byte(code), rt.TableValue(rtm.GlobalEnv())) chunk, err := rtm.CompileAndLoadLuaChunk("<string>", []byte(code), rt.TableValue(rtm.GlobalEnv()))
var ret rt.Value
if chunk != nil { 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. // DoFile runs the contents of the file in the Lua runtime.

View File

@ -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.

View File

@ -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 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 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, Hilbish as a REPL. This also allows users to add alternative languages like
instead of either like Fennel. Fennel as the interactive script runner.
Runner mode can also be used to handle specific kinds of input before 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) evaluating like normal, which is how [Link.hsh](https://github.com/TorchedSammy/Link.hsh)