diff --git a/go.mod b/go.mod index cc88c8e..b9899cb 100644 --- a/go.mod +++ b/go.mod @@ -10,10 +10,11 @@ require ( github.com/blackfireio/osinfo v1.0.5 github.com/maxlandon/readline v1.0.14 github.com/pborman/getopt v1.1.0 + github.com/rjeczalik/notify v0.9.3 github.com/sahilm/fuzzy v0.1.1 golang.org/x/sys v0.22.0 golang.org/x/term v0.22.0 - mvdan.cc/sh/v3 v3.8.0 + mvdan.cc/sh/v3 v3.0.0-00010101000000-000000000000 ) require ( diff --git a/go.sum b/go.sum index b4b7a91..b06fa3f 100644 --- a/go.sum +++ b/go.sum @@ -32,12 +32,15 @@ github.com/pborman/getopt v1.1.0 h1:eJ3aFZroQqq0bWmraivjQNt6Dmm5M0h2JcDW38/Azb0= github.com/pborman/getopt v1.1.0/go.mod h1:FxXoW1Re00sQG/+KIkuSqRL/LwQgSkv7uyac+STFsbk= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY= +github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= diff --git a/golibs/fs/fs.go b/golibs/fs/fs.go index 002be90..2933797 100644 --- a/golibs/fs/fs.go +++ b/golibs/fs/fs.go @@ -22,6 +22,7 @@ import ( "mvdan.cc/sh/v3/interp" ) +var watcherMetaKey = rt.StringValue("hshwatcher") type fs struct{ runner *interp.Runner Loader packagelib.Loader @@ -40,6 +41,30 @@ func New(runner *interp.Runner) *fs { } func (f *fs) loaderFunc(rtm *rt.Runtime) (rt.Value, func()) { + watcherMethods := rt.NewTable() + watcherFuncs := map[string]util.LuaExport{ + "start": {watcherStart, 1, false}, + "stop": {watcherStop, 1, false}, + } + util.SetExports(rtm, watcherMethods, watcherFuncs) + + watcherMeta := rt.NewTable() + watcherIndex := func(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + //ti, _ := watcherArg(c, 0) + + arg := c.Arg(1) + val := watcherMethods.Get(arg) + + if val != rt.NilValue { + return c.PushingNext1(t.Runtime, val), nil + } + + return c.PushingNext1(t.Runtime, val), nil + } + + watcherMeta.Set(rt.StringValue("__index"), rt.FunctionValue(rt.NewGoFunction(watcherIndex, "__index", 2, false))) + rtm.SetRegistry(watcherMetaKey, rt.TableValue(watcherMeta)) + exports := map[string]util.LuaExport{ "cd": util.LuaExport{f.fcd, 1, false}, "mkdir": util.LuaExport{f.fmkdir, 2, false}, @@ -51,6 +76,7 @@ func (f *fs) loaderFunc(rtm *rt.Runtime) (rt.Value, func()) { "glob": util.LuaExport{f.fglob, 1, false}, "join": util.LuaExport{f.fjoin, 0, true}, "pipe": util.LuaExport{f.fpipe, 0, false}, + "watch": util.LuaExport{fwatch, 2, false}, } mod := rt.NewTable() util.SetExports(rtm, mod, exports) @@ -334,3 +360,28 @@ func (f *fs) fstat(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.PushingNext1(t.Runtime, rt.TableValue(statTbl)), nil } +// watch(path, callback) +// Watches a path for changes made to it. For example, to monitor +// new files created in a folder. +// The callback passed 2 string arguments, the `event` and the absolute `path` +// #param path string +// #param callback function +func fwatch(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.CheckNArgs(2); err != nil { + return nil, err + } + + dir, err := c.StringArg(0) + if err != nil { + return nil, err + } + + watcher, err := c.ClosureArg(1) + if err != nil { + return nil, err + } + + dw := newWatcher(dir, watcher, t.Runtime) + + return c.PushingNext1(t.Runtime, rt.UserDataValue(dw.ud)), nil +} diff --git a/golibs/fs/watcher.go b/golibs/fs/watcher.go new file mode 100644 index 0000000..b92ba6d --- /dev/null +++ b/golibs/fs/watcher.go @@ -0,0 +1,111 @@ +package fs + +import ( + "fmt" + + "github.com/rjeczalik/notify" + rt "github.com/arnodel/golua/runtime" +) + +// #type +// Watcher type describes a `fs` library file watcher. +type watcher struct{ + path string + callback *rt.Closure + paused bool + started bool + ud *rt.UserData + notifyChan chan notify.EventInfo + rtm *rt.Runtime +} + +func (w *watcher) start() { + if w.callback == nil || w.started { + return + } + + w.started = true + w.notifyChan = make(chan notify.EventInfo) + notify.Watch(w.path, w.notifyChan, notify.All) + + go func() { + for notif := range w.notifyChan { + ev := notif.Event().String() + path := notif.Path() + + _, err := rt.Call1(w.rtm.MainThread(), rt.FunctionValue(w.callback), rt.StringValue(ev), rt.StringValue(path)) + if err != nil { + // TODO: throw error + } + } + }() +} + +func (w *watcher) stop() { + w.started = false + notify.Stop(w.notifyChan) +} + +// #member +// start() +// Start/resume file watching. +func watcherStart(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + pw, err := watcherArg(c, 0) + if err != nil { + return nil, err + } + + pw.start() + + return c.Next(), nil +} + +// #member +// stop() +// Stops watching for changes. Effectively ignores changes. +func watcherStop(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + pw, err := watcherArg(c, 0) + if err != nil { + return nil, err + } + + pw.stop() + + return c.Next(), nil +} + +func newWatcher(path string, callback *rt.Closure, rtm *rt.Runtime) *watcher { + pw := &watcher{ + path: path, + rtm: rtm, + callback: callback, + } + pw.ud = watcherUserData(pw) + pw.start() + + return pw +} + +func watcherArg(c *rt.GoCont, arg int) (*watcher, error) { + j, ok := valueToWatcher(c.Arg(arg)) + if !ok { + return nil, fmt.Errorf("#%d must be a watcher", arg + 1) + } + + return j, nil +} + +func valueToWatcher(val rt.Value) (*watcher, bool) { + u, ok := val.TryUserData() + if !ok { + return nil, false + } + + j, ok := u.Value().(*watcher) + return j, ok +} + +func watcherUserData(j *watcher) *rt.UserData { + watcherMeta := j.rtm.Registry(watcherMetaKey) + return rt.NewUserData(j, watcherMeta.AsTable()) +}