2022-12-15 04:00:54 +00:00
|
|
|
// the event emitter
|
2023-12-26 03:08:29 +00:00
|
|
|
/*
|
|
|
|
Bait is the event emitter for Hilbish. Much like Node.js and
|
|
|
|
its `events` system, many actions in Hilbish emit events.
|
|
|
|
Unlike Node.js, Hilbish events are global. So make sure to
|
|
|
|
pick a unique name!
|
|
|
|
|
|
|
|
Usage of the Bait module consists of userstanding
|
|
|
|
event-driven architecture, but it's pretty simple:
|
|
|
|
If you want to act on a certain event, you can `catch` it.
|
|
|
|
You can act on events via callback functions.
|
|
|
|
|
|
|
|
Examples of this are in the Hilbish default config!
|
|
|
|
Consider this part of it:
|
|
|
|
```lua
|
|
|
|
bait.catch('command.exit', function(code)
|
|
|
|
running = false
|
|
|
|
doPrompt(code ~= 0)
|
|
|
|
doNotifyPrompt()
|
|
|
|
end)
|
|
|
|
```
|
|
|
|
|
|
|
|
What this does is, whenever the `command.exit` event is thrown,
|
|
|
|
this function will set the user prompt.
|
|
|
|
*/
|
2021-03-26 05:12:55 +00:00
|
|
|
package bait
|
|
|
|
|
|
|
|
import (
|
2022-10-11 21:41:13 +00:00
|
|
|
"errors"
|
|
|
|
|
2021-10-16 16:40:53 +00:00
|
|
|
"hilbish/util"
|
|
|
|
|
2022-04-04 10:40:02 +00:00
|
|
|
rt "github.com/arnodel/golua/runtime"
|
|
|
|
"github.com/arnodel/golua/lib/packagelib"
|
2021-03-26 05:12:55 +00:00
|
|
|
)
|
|
|
|
|
2022-08-17 22:01:32 +00:00
|
|
|
type listenerType int
|
|
|
|
const (
|
|
|
|
goListener listenerType = iota
|
|
|
|
luaListener
|
|
|
|
)
|
|
|
|
|
|
|
|
// Recoverer is a function which is called when a panic occurs in an event.
|
|
|
|
type Recoverer func(event string, handler *Listener, err interface{})
|
|
|
|
|
|
|
|
// Listener is a struct that holds the handler for an event.
|
|
|
|
type Listener struct{
|
|
|
|
typ listenerType
|
|
|
|
once bool
|
|
|
|
caller func(...interface{})
|
|
|
|
luaCaller *rt.Closure
|
|
|
|
}
|
|
|
|
|
2021-03-30 23:47:02 +00:00
|
|
|
type Bait struct{
|
2022-04-04 10:40:02 +00:00
|
|
|
Loader packagelib.Loader
|
2022-08-17 22:01:32 +00:00
|
|
|
recoverer Recoverer
|
|
|
|
handlers map[string][]*Listener
|
|
|
|
rtm *rt.Runtime
|
2021-03-30 23:47:02 +00:00
|
|
|
}
|
2021-03-26 05:12:55 +00:00
|
|
|
|
2022-08-17 22:01:32 +00:00
|
|
|
// New creates a new Bait instance.
|
|
|
|
func New(rtm *rt.Runtime) *Bait {
|
|
|
|
b := &Bait{
|
|
|
|
handlers: make(map[string][]*Listener),
|
|
|
|
rtm: rtm,
|
2021-03-30 23:47:02 +00:00
|
|
|
}
|
2022-04-04 10:40:02 +00:00
|
|
|
b.Loader = packagelib.Loader{
|
|
|
|
Load: b.loaderFunc,
|
|
|
|
Name: "bait",
|
|
|
|
}
|
|
|
|
|
|
|
|
return b
|
2021-03-26 05:12:55 +00:00
|
|
|
}
|
|
|
|
|
2022-08-17 22:01:32 +00:00
|
|
|
// Emit throws an event.
|
|
|
|
func (b *Bait) Emit(event string, args ...interface{}) {
|
|
|
|
handles := b.handlers[event]
|
|
|
|
if handles == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
for idx, handle := range handles {
|
|
|
|
defer func() {
|
|
|
|
if err := recover(); err != nil {
|
|
|
|
b.callRecoverer(event, handle, err)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
if handle.typ == luaListener {
|
|
|
|
funcVal := rt.FunctionValue(handle.luaCaller)
|
|
|
|
var luaArgs []rt.Value
|
|
|
|
for _, arg := range args {
|
|
|
|
var luarg rt.Value
|
|
|
|
switch arg.(type) {
|
|
|
|
case rt.Value: luarg = arg.(rt.Value)
|
|
|
|
default: luarg = rt.AsValue(arg)
|
|
|
|
}
|
|
|
|
luaArgs = append(luaArgs, luarg)
|
|
|
|
}
|
|
|
|
_, err := rt.Call1(b.rtm.MainThread(), funcVal, luaArgs...)
|
|
|
|
if err != nil {
|
2022-10-11 21:41:13 +00:00
|
|
|
if event != "error" {
|
|
|
|
b.Emit("error", event, handle.luaCaller, err.Error())
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// if there is an error in an error event handler, panic instead
|
|
|
|
// (calls the go recoverer function)
|
2022-08-17 22:01:32 +00:00
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
handle.caller(args...)
|
|
|
|
}
|
|
|
|
|
|
|
|
if handle.once {
|
|
|
|
b.removeListener(event, idx)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// On adds a Go function handler for an event.
|
|
|
|
func (b *Bait) On(event string, handler func(...interface{})) *Listener {
|
|
|
|
listener := &Listener{
|
|
|
|
typ: goListener,
|
|
|
|
caller: handler,
|
|
|
|
}
|
|
|
|
|
|
|
|
b.addListener(event, listener)
|
|
|
|
return listener
|
|
|
|
}
|
|
|
|
|
|
|
|
// OnLua adds a Lua function handler for an event.
|
|
|
|
func (b *Bait) OnLua(event string, handler *rt.Closure) *Listener {
|
|
|
|
listener :=&Listener{
|
|
|
|
typ: luaListener,
|
|
|
|
luaCaller: handler,
|
|
|
|
}
|
|
|
|
b.addListener(event, listener)
|
|
|
|
|
|
|
|
return listener
|
|
|
|
}
|
|
|
|
|
|
|
|
// Off removes a Go function handler for an event.
|
|
|
|
func (b *Bait) Off(event string, listener *Listener) {
|
|
|
|
handles := b.handlers[event]
|
|
|
|
|
|
|
|
for i, handle := range handles {
|
|
|
|
if handle == listener {
|
|
|
|
b.removeListener(event, i)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// OffLua removes a Lua function handler for an event.
|
|
|
|
func (b *Bait) OffLua(event string, handler *rt.Closure) {
|
|
|
|
handles := b.handlers[event]
|
|
|
|
|
|
|
|
for i, handle := range handles {
|
|
|
|
if handle.luaCaller == handler {
|
|
|
|
b.removeListener(event, i)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Once adds a Go function listener for an event that only runs once.
|
|
|
|
func (b *Bait) Once(event string, handler func(...interface{})) *Listener {
|
|
|
|
listener := &Listener{
|
|
|
|
typ: goListener,
|
|
|
|
once: true,
|
|
|
|
caller: handler,
|
|
|
|
}
|
|
|
|
b.addListener(event, listener)
|
|
|
|
|
|
|
|
return listener
|
|
|
|
}
|
|
|
|
|
|
|
|
// OnceLua adds a Lua function listener for an event that only runs once.
|
|
|
|
func (b *Bait) OnceLua(event string, handler *rt.Closure) *Listener {
|
|
|
|
listener := &Listener{
|
|
|
|
typ: luaListener,
|
|
|
|
once: true,
|
|
|
|
luaCaller: handler,
|
|
|
|
}
|
|
|
|
b.addListener(event, listener)
|
|
|
|
|
|
|
|
return listener
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetRecoverer sets the function to be executed when a panic occurs in an event.
|
|
|
|
func (b *Bait) SetRecoverer(recoverer Recoverer) {
|
|
|
|
b.recoverer = recoverer
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *Bait) addListener(event string, listener *Listener) {
|
|
|
|
if b.handlers[event] == nil {
|
|
|
|
b.handlers[event] = []*Listener{}
|
|
|
|
}
|
|
|
|
|
|
|
|
b.handlers[event] = append(b.handlers[event], listener)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (b *Bait) removeListener(event string, idx int) {
|
|
|
|
b.handlers[event][idx] = b.handlers[event][len(b.handlers[event]) - 1]
|
|
|
|
|
|
|
|
b.handlers[event] = b.handlers[event][:len(b.handlers[event]) - 1]
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *Bait) callRecoverer(event string, handler *Listener, err interface{}) {
|
|
|
|
if b.recoverer == nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
b.recoverer(event, handler, err)
|
|
|
|
}
|
|
|
|
|
2022-04-04 10:40:02 +00:00
|
|
|
func (b *Bait) loaderFunc(rtm *rt.Runtime) (rt.Value, func()) {
|
|
|
|
exports := map[string]util.LuaExport{
|
|
|
|
"catch": util.LuaExport{b.bcatch, 2, false},
|
|
|
|
"catchOnce": util.LuaExport{b.bcatchOnce, 2, false},
|
|
|
|
"throw": util.LuaExport{b.bthrow, 1, true},
|
2022-08-17 22:01:32 +00:00
|
|
|
"release": util.LuaExport{b.brelease, 2, false},
|
2022-10-11 21:41:13 +00:00
|
|
|
"hooks": util.LuaExport{b.bhooks, 1, false},
|
2022-04-04 10:40:02 +00:00
|
|
|
}
|
|
|
|
mod := rt.NewTable()
|
|
|
|
util.SetExports(rtm, mod, exports)
|
2021-11-22 23:58:30 +00:00
|
|
|
|
2022-04-04 10:40:02 +00:00
|
|
|
return rt.TableValue(mod), nil
|
|
|
|
}
|
2021-03-26 05:12:55 +00:00
|
|
|
|
2022-04-04 10:40:02 +00:00
|
|
|
func handleHook(t *rt.Thread, c *rt.GoCont, name string, catcher *rt.Closure, args ...interface{}) {
|
|
|
|
funcVal := rt.FunctionValue(catcher)
|
|
|
|
var luaArgs []rt.Value
|
|
|
|
for _, arg := range args {
|
|
|
|
var luarg rt.Value
|
|
|
|
switch arg.(type) {
|
|
|
|
case rt.Value: luarg = arg.(rt.Value)
|
|
|
|
default: luarg = rt.AsValue(arg)
|
|
|
|
}
|
|
|
|
luaArgs = append(luaArgs, luarg)
|
|
|
|
}
|
|
|
|
_, err := rt.Call1(t, funcVal, luaArgs...)
|
|
|
|
if err != nil {
|
|
|
|
e := rt.NewError(rt.StringValue(err.Error()))
|
|
|
|
e = e.AddContext(c.Next(), 1)
|
|
|
|
// panicking here won't actually cause hilbish to panic and instead will
|
|
|
|
// print the error and remove the hook (look at emission recover from above)
|
|
|
|
panic(e)
|
|
|
|
}
|
2021-03-26 05:12:55 +00:00
|
|
|
}
|
|
|
|
|
2021-10-16 14:21:05 +00:00
|
|
|
// catch(name, cb)
|
2023-12-26 03:08:29 +00:00
|
|
|
// Catches an event. This function can be used to act on events.
|
|
|
|
// #param name string The name of the hook.
|
|
|
|
// #param cb function The function that will be called when the hook is thrown.
|
|
|
|
/*
|
|
|
|
#example
|
|
|
|
bait.catch('hilbish.exit', function()
|
|
|
|
print 'Goodbye Hilbish!'
|
|
|
|
end)
|
|
|
|
#example
|
|
|
|
*/
|
2022-04-04 10:40:02 +00:00
|
|
|
func (b *Bait) bcatch(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
|
|
|
name, catcher, err := util.HandleStrCallback(t, c)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-08-17 22:01:32 +00:00
|
|
|
b.OnLua(name, catcher)
|
2022-04-04 10:40:02 +00:00
|
|
|
|
|
|
|
return c.Next(), nil
|
2021-03-30 23:47:02 +00:00
|
|
|
}
|
2021-12-04 21:59:19 +00:00
|
|
|
|
|
|
|
// catchOnce(name, cb)
|
2023-12-26 03:08:29 +00:00
|
|
|
// Catches an event, but only once. This will remove the hook immediately after it runs for the first time.
|
|
|
|
// #param name string The name of the event
|
|
|
|
// #param cb function The function that will be called when the event is thrown.
|
2022-04-04 10:40:02 +00:00
|
|
|
func (b *Bait) bcatchOnce(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
|
|
|
name, catcher, err := util.HandleStrCallback(t, c)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-08-17 22:01:32 +00:00
|
|
|
b.OnceLua(name, catcher)
|
|
|
|
|
|
|
|
return c.Next(), nil
|
|
|
|
}
|
|
|
|
|
2023-02-07 22:18:03 +00:00
|
|
|
// hooks(name) -> table
|
2023-12-26 03:08:29 +00:00
|
|
|
// Returns a table of functions that are hooked on an event with the corresponding `name`.
|
|
|
|
// #param name string The name of the hook
|
|
|
|
// #returns table<function>
|
2022-10-11 21:41:13 +00:00
|
|
|
func (b *Bait) bhooks(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
|
|
|
if err := c.Check1Arg(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
evName, err := c.StringArg(0)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
noHooks := errors.New("no hooks for event " + evName)
|
|
|
|
|
|
|
|
handlers := b.handlers[evName]
|
|
|
|
if handlers == nil {
|
|
|
|
return nil, noHooks
|
|
|
|
}
|
|
|
|
|
|
|
|
luaHandlers := rt.NewTable()
|
|
|
|
for _, handler := range handlers {
|
|
|
|
if handler.typ != luaListener { continue }
|
|
|
|
luaHandlers.Set(rt.IntValue(luaHandlers.Len() + 1), rt.FunctionValue(handler.luaCaller))
|
|
|
|
}
|
|
|
|
|
|
|
|
if luaHandlers.Len() == 0 {
|
|
|
|
return nil, noHooks
|
|
|
|
}
|
|
|
|
|
|
|
|
return c.PushingNext1(t.Runtime, rt.TableValue(luaHandlers)), nil
|
|
|
|
}
|
2023-12-26 03:08:29 +00:00
|
|
|
|
|
|
|
// release(name, catcher)
|
|
|
|
// Removes the `catcher` for the event with `name`.
|
|
|
|
// For this to work, `catcher` has to be the same function used to catch
|
|
|
|
// an event, like one saved to a variable.
|
|
|
|
// #param name string Name of the event the hook is on
|
|
|
|
// #param catcher function Hook function to remove
|
|
|
|
/*
|
|
|
|
#example
|
|
|
|
local hookCallback = function() print 'hi' end
|
|
|
|
|
|
|
|
bait.catch('event', hookCallback)
|
|
|
|
|
|
|
|
-- a little while later....
|
|
|
|
bait.release('event', hookCallback)
|
|
|
|
-- and now hookCallback will no longer be ran for the event.
|
|
|
|
#example
|
|
|
|
*/
|
|
|
|
func (b *Bait) brelease(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
|
|
|
name, catcher, err := util.HandleStrCallback(t, c)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
b.OffLua(name, catcher)
|
|
|
|
|
|
|
|
return c.Next(), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// throw(name, ...args)
|
|
|
|
// #param name string The name of the hook.
|
|
|
|
// #param args ...any The arguments to pass to the hook.
|
|
|
|
// Throws a hook with `name` with the provided `args`.
|
|
|
|
/*
|
|
|
|
#example
|
|
|
|
bait.throw('greeting', 'world')
|
|
|
|
|
|
|
|
-- This can then be listened to via
|
|
|
|
bait.catch('gretting', function(greetTo)
|
|
|
|
print('Hello ' .. greetTo)
|
|
|
|
end)
|
|
|
|
#example
|
|
|
|
*/
|
|
|
|
func (b *Bait) bthrow(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) {
|
|
|
|
if err := c.Check1Arg(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
name, err := c.StringArg(0)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
ifaceSlice := make([]interface{}, len(c.Etc()))
|
|
|
|
for i, v := range c.Etc() {
|
|
|
|
ifaceSlice[i] = v
|
|
|
|
}
|
|
|
|
b.Emit(name, ifaceSlice...)
|
|
|
|
|
|
|
|
return c.Next(), nil
|
|
|
|
}
|