{"id":22350936,"url":"https://github.com/johnae/spook","last_synced_at":"2025-07-30T07:31:35.741Z","repository":{"id":33882089,"uuid":"37593119","full_name":"johnae/spook","owner":"johnae","description":"Lightweight programmable evented utility based on LuaJIT and ljsyscall","archived":false,"fork":false,"pushed_at":"2021-05-10T21:06:41.000Z","size":1382,"stargazers_count":35,"open_issues_count":0,"forks_count":0,"subscribers_count":7,"default_branch":"master","last_synced_at":"2024-04-24T03:38:11.526Z","etag":null,"topics":["epoll","event-driven","event-loop","filewatcher","kqueue","lua","luajit","moonscript","watcher"],"latest_commit_sha":null,"homepage":"","language":"MoonScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/johnae.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-06-17T12:19:38.000Z","updated_at":"2024-03-30T02:57:24.000Z","dependencies_parsed_at":"2022-08-17T23:55:30.593Z","dependency_job_id":null,"html_url":"https://github.com/johnae/spook","commit_stats":null,"previous_names":[],"tags_count":64,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johnae%2Fspook","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johnae%2Fspook/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johnae%2Fspook/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johnae%2Fspook/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/johnae","download_url":"https://codeload.github.com/johnae/spook/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":228106596,"owners_count":17870437,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["epoll","event-driven","event-loop","filewatcher","kqueue","lua","luajit","moonscript","watcher"],"created_at":"2024-12-04T12:11:49.966Z","updated_at":"2024-12-04T12:11:50.538Z","avatar_url":"https://github.com/johnae.png","language":"MoonScript","readme":"[![Build Status](https://badge.buildkite.com/41c70ee26c601a323ea26103e67674cae9bdcbaa8fc17786ae.svg)](https://buildkite.com/insane/spook)\n\n## Spook - react to change\n\nSpook started out as a light weight replacement for [guard](https://github.com/guard/guard) but has become more than that over time. It is mostly written in [MoonScript](https://github.com/leafo/moonscript), a language that compiles to [Lua](http://www.lua.org) - with a sprinkle of C. It's built as a single binary. The ridiculously fast [LuaJIT VM](http://luajit.org/) is embedded and compiled with Lua 5.2 compatibility. Extensions are easily written in [MoonScript](https://github.com/leafo/moonscript), which is also part of the compiled binary.\n\nWhile spook may seem to be geared towards running tests in a feedback loop, there are many other potential uses. For some inspiration, check out my i3bar implementation [moonbar](https://github.com/johnae/moonbar) for the [i3 window manager](https://i3wm.org/) which is also using a Spookfile but is doing something very different. Otherwise the Spookfile in this repo and the examples in the readme should point you in the right direction if you're just looking for a lightweight test feedback loop runner.\n\nSpook was also inspired by the [entrproject](http://entrproject.org/) and it's simplicity (eg. the lightweight \"feel\" of entr). The goal of spook was always broader and more general. Still, entr is a very nice tool which is why spook has (since version 0.8.1) gained some of the functionality entr provides. More specifically it can read a list of files on stdin and run a command when any of them changes or even be part of a longer unix pipeline. See further down for some examples of this.\n\nBuilding spook requires the usual tools (eg. make and gcc/clang), so you may need to install some things before building it. Otherwise it should be as straightforward as:\n\n```sh\nmake\n```\n\nAfter that you should have an executable called spook. It's known to build and work well on Linux and Mac OS X. It's also verified to work on FreeBSD. On FreeBSD, you need to install gmake, like this:\n\n```sh\nsudo pkg install gmake\ngmake\n```\n\nEverything in the lib directory and top level is part of spook itself, anything in vendor and deps is other peoples work. See [LICENSE](LICENSE.md) for more.\n\n\nInstallation is as straightforward as:\n\n```sh\nmake install PREFIX=/usr/local\n```\n\nOr gmake on FreeBSD for example.\n\n### Changelog\n\nThere's a [CHANGELOG](CHANGELOG.md) which may be useful when learning about any breaking changes, new features or other improvements. Please consult it when upgrading.\n\n### Running it\n\nFor some basic help on command line usage, please run:\n\n```sh\nspook --help\n```\n\nCurrently that would output something like:\n\n```\nUsage: spook [-v] [-i] [-l \u003cl\u003e] [-c \u003cc\u003e] [-w \u003cw\u003e] [-f \u003cf\u003e] [-s] [-o]\n       [-r \u003cr\u003e] [-p \u003cp\u003e] [--] [-h]\n\nWatches for changes and runs functions (and commands) in response, based on a config file (eg. Spookfile) or watches any files it is given on stdin (similar to the entrproject).\n\nOptions:\n   -v                    Show the Spook version you're running and exit.\n   -i                    Initialize an example Spookfile in the current dir.\n   -l \u003cl\u003e                Log level either ERR, WARN, INFO or DEBUG.\n   -c \u003cc\u003e                Expects the path to a Spook config file (eg. Spookfile) - overrides the default of loading a Spookfile from cwd.\n   -w \u003cw\u003e                Expects the path to working directory - overrides the default of using wherever spook was launched.\n   -f \u003cf\u003e                Expects a path to a MoonScript or Lua file - runs the script within the context of spook, skipping the default behavior completely.\n                         Any arguments following the path to the file will be given to the file itself.\n   -s                    Stdin mode only: start the given utility immediately without waiting for changes first.\n                         The utility to run should be given as the last arg(s) on the commandline. Without a utility spook will output the changed file path.\n   -o                    Stdin mode only: exit immediately after running utility (or receiving an event basically).\n                         The utility to run should be given as the last arg(s) on the commandline. Without a utility spook will output the changed file path.\n   -r \u003cr\u003e                Wait this many seconds for data on stdin before bailing (0 means don't wait for any data at all). (default: 2)\n   -p \u003cp\u003e                Write the pid of the running spook process to given file path.\n   --                    Disable argument parsing from here.\n   -h, --help            Show this help message and exit.\n\nFor more see https://github.com/johnae/spook\n```\n\n### MacOS\n\nCheck your ulimits (max open files seem to be set to 256 by default on MacOS). It will likely make spook crash if watching more than a few hundred files. Setting it higher looks something like:\n\n```sh\nulimit -n 4096\n```\n\nHere's a guide on how to permanently set your ulimits on MacOS: [ulimit-shenanigans-on-osx-el-capitan](https://blog.dekstroza.io/ulimit-shenanigans-on-osx-el-capitan/)\n\n### The Spookfile\n\nFor alot of things it is useful to create a Spookfile in a directory (probably your project). The Spookfile enables you to control spook in a rather fine grained fashion - you could build almost anything out of spook this way:\n\n```sh\ncd /to/your/project\nspook -i\n```\n\nin your project directory to create an example Spookfile. Then tailor it to your needs. After that you just run spook without arguments in that directory. The default Spookfile is a basic example that might work for a Rails app.\n\nThe Spookfile should be written in [MoonScript](https://github.com/leafo/moonscript). It comes with a simple DSL as well as just straight MoonScript for just about anything you can do in Lua and/or MoonScript. Hooking in to the notifications api is easy and it's also easy to implement your own notifiers.\n\nThis is the Spookfile used to test spook itself:\n\n```moonscript\n-- How much log output can you handle? (ERR, WARN, INFO, DEBUG)\nlog_level \"INFO\"\n\n-- If the spookfile is reloaded we just ensure we reload\n-- the other stuff too.\npackage.loaded['moonscript.cmd.lint'] = nil\nmoonlint = require(\"moonscript.cmd.lint\").lint_file\npackage.loaded.lint_config = nil\npackage.loaded.lint_config = pcall -\u003e loadfile('lint_config')!\n\n-- Require some things that come with spook\ncolors = require \"ansicolors\"\nfs = require 'fs'\n\n-- Adds the built-in terminal_notifier - this notifies of success/fail\n-- in the terminal.\nnotify.add 'terminal_notifier'\n\n-- If we find 'notifier' in the path, let's\n-- add that notifier also. We fail silently otherwise.\npcall notify.add, 'notifier'\n\n-- Yet another simple way of including a notifier would\n-- be to define it right here - like this:\nnotify.add {\n  start: (msg, info) -\u003e\n    print \"Start, yay\"\n  success: (msg, info) -\u003e\n    print \"Success, yay!\"\n  fail: (msg, info) -\u003e\n    print \"Fail, nay!\"\n}\n\n-- spookfile_helpers is included inside the spook binary,\n-- it's some helpers mainly for using spook in a similar fashion\n-- to guard.\n{\n  :until_success\n  :command\n  :task_filter\n  :notifies\n} = require 'spookfile_helpers'\n\n-- we use this for notifications, by filtering out\n-- the commands not runnable (because the mapped files\n-- aren't present), we don't unnecessarily notify on\n-- start / fail / success when nothing can actually\n-- happen. For a spec runner, this makes sense.\ntask_list = task_filter fs.is_present\nspec = command \"./tools/luajit/bin/luajit spec/support/run_busted.lua\"\nexec = command \"./tools/luajit/bin/luajit run.lua\"\n\nlint = (file) -\u003e\n  notify.info \"LINTING #{file}\"\n  result, err = moonlint file\n  success = if result\n    io.stdout\\write colors(\"\\n[ %{red}LINT error ]\\n%{white}#{result}\\n\\n\")\n    false\n  elseif err\n    io.stdout\\write colors(\"\\n[ %{red}LINT error ]\\n#%{white}{file}\\n#{err}\\n\\n\")\n    false\n  else\n    true\n  if success\n    io.stdout\\write colors(\"\\n[ %{green}LINT: %{white}All good ]\\n\\n\")\n  assert success == true, \"lint #{file}\"\n\n-- Watching for changes underneath . and matching them to handlers using\n-- lua patterns (see: http://lua-users.org/wiki/PatternsTutorial for example).\nwatch \".\", -\u003e\n\n  on_changed \"^spec/spec_helper%.moon\", (event) -\u003e\n    until_success -\u003e\n      notifies event.path, event,\n        task_list(\n          lint, \"spec/spec_helper.moon\"\n          spec, \"spec\"\n        )\n\n  on_changed \"^spec/(.*)%.moon\", (event, name) -\u003e\n    until_success -\u003e\n      notifies event.path, event,\n        task_list(\n          lint, \"spec/#{name}.moon\"\n          spec, \"spec/#{name}.moon\"\n        )\n\n  on_changed \"^lib/(.*)/event_loop%.moon\", (event, name) -\u003e\n    until_success -\u003e\n      notifies event.path, event,\n        task_list(\n          lint, \"lib/#{name}/event_loop.moon\"\n          spec, \"spec/event_loop_spec.moon\"\n        )\n\n  on_changed \"^lib/(.*)%.moon\", (event, name) -\u003e\n    until_success -\u003e\n      notifies event.path, event,\n        task_list(\n          lint, \"lib/#{name}.moon\"\n          spec, \"spec/#{name}_spec.moon\"\n        )\n\n  on_changed \"^shpec/(.*)%.sh\", (event, name) -\u003e\n    return unless os.getenv('SPOOK_INTEGRATION') == 'yes'\n    until_success -\u003e\n      notifies event.path, event,\n        task_list(\n          shpec, \"shpec/#{name}.sh\"\n        )\n\n  on_changed \"^playground/(.*)%.moon\", (event, name) -\u003e\n    exec \"playground/#{name}.moon\"\n\n  on_changed \"^playground/(.*)%.lua\", (event, name) -\u003e\n    exec \"playground/#{name}.lua\"\n\n  on_changed \"^Spookfile$\", (event) -\u003e\n    notify.info \"Re-executing spook...\"\n    reload_spook!\n\n  on_changed \"^lint_config%.lua$\", (event) -\u003e\n    notify.info \"Re-executing spook...\"\n    reload_spook!\n```\n\nSo as you can see, some things were defined in a helper file (`until_success`, `notifies` etc functions) which was built in to spook. Some others (eg. the `notifier`) was required from somewhere on the package path (eg. from disk).\n\nOf note is that while it's possible to define several watch statements with different directories, as soon as you want to watch something in PWD (that goes for watch_file statements as well even though non-obvious) it's better to just watch '.' and define on_changed handlers (or on_deleted, on_attrib, on_created etc.) to match on them.\n\nThe reason for this is that the matchers are all in the same \"bucket\" and it's more straightforward to ensure no collisions eg. something unexpected matches before the match you expected - spook ONLY executes the handler for the first match by default.\n\nBasically instead of this:\n\n```moonscript\nwatch 'lib', 'spec', -\u003e\n  on_changed '^lib/(.*)%.moon', (event, name) -\u003e\n    run_spec \"spec/#{name}_spec.moon\"\n\n  on_changed '^spec/(.*)%.moon', (event, name) -\u003e\n    run_spec \"spec/#{name}.moon\"\n\n-- anything we may easily assume\n  on_changed '.*', (event) -\u003e\n    print \"something changed\"\n\n-- this will never run because the above catch-all will be matched -\n-- at the moment all matchers are in the same \"bucket\".\nwatch_file 'spookfile', -\u003e\n  on_changed (event) -\u003e\n    notify.info \"re-executing spook...\"\n    reload_spook!\n```\n\nDo this:\n\n```moonscript\nwatch '.', -\u003e\n  on_changed '^lib/(.*)%.moon', (event, name) -\u003e\n    run_spec \"spec/#{name}_spec.moon\"\n\n  on_changed '^spec/(.*)%.moon', (event, name) -\u003e\n    run_spec \"spec/#{name}.moon\"\n\n  on_changed '^Spookfile$', (event) -\u003e\n    notify.info \"re-executing spook...\"\n    reload_spook!\n\n-- anything else - this would actually work as expected\n  on_changed '.*', (event) -\u003e\n    print \"something changed\"\n```\n\nIgnoring certain paths is supported as of spook 0.9.6. You would just give a list of patterns as the second argument to `watch`, like this:\n\n```moonscript\nwatch '.', {'^%.git$', '%.env.*'}, -\u003e\n  on_changed '^lib/(.*)%.moon', (event, name) -\u003e\n    run_spec \"spec/#{name}_spec.moon\"\n```\n\nThe above would ignore any file or directory called exactly '.git' and any file or directory having '.env' in it's name. This is applied recursively.\n\n\n### Pipelining with spook (eg. watch files given on stdin)\n\nAs mentioned up top, spook (since version 0.8.1) has gained the basic functionality of [entr](http://entrproject.org/). Using it in this mode is as simple as:\n\n```sh\nfind . -type f | spook echo file changed: {file}\n```\n\nOr\n\n```sh\nls *.moon | spook echo file changed: {file}\n```\n\nSince all commands in this scenario are passed to /bin/sh, this is also possible:\n\n```sh\nls *.moon | spook \"echo file changed: {file} \u0026\u0026 echo something else\"\n```\n\nPerhaps a more relevant example of that would be something like:\n\n```sh\nag -l | spook \"make test \u0026\u0026 make\"\n```\n\nBasically if tests pass, run the build.\n\nOr keeping a log of changes like so:\n\n```sh\nfind . -type f | spook \"echo \\$(date): {file} \u003e\u003e /tmp/changelog.txt\"\n```\n\nThe \"restart server on changes\" works something like:\n\n```sh\nfind . -type f -name \"*.go\" | spook -s go run server.go\n```\n\nThe above would run the server until a file in the given list of files changed at which time spook would restart the server. Using the \"-s\" switch means that the given utility to run is started immediately, not after a change is detected.\n\nThere's also a oneshot option, -o, which executes the given utility just once then exits when a watched file changes:\n\n```sh\nfind . -type f -name \"*.jpg\" | spook -o convert {file} -50% {filenoext}.small.jpg\n```\n\nTogether, the oneshot option and the \"server\" option results in the given command being started immediately and terminated on the first change detected. Perhaps something like:\n\n```sh\nwhile true; do find . -type f -name \"*.go\" | spook -o -s go run server.go; done\n```\n\nThe above might be useful when you'd want to find new files between each restart (eg. the find would be executed again in this scenario).\n\nThese are exactly the kinds of things entr was made to do in a very simple and unsurprising fashion.\n\nThat last {file} \"thing\" by the way is a replacement string which will actually contain the file that changed. Two other variants of that are [file] and \u0026lt;file\u0026gt;. There's also {filenoext} which will be the filename without extension (with the path), there's {basename} which is the filename without the path and finally {basenamenoext} which is the filename without path and extension.\n\nHere's another example one might modify to do more interesting things:\n\n```sh\nfind . -type f -name \"*.txt\" | spook | grep \"secrets\"\n```\n\nSay we're in $HOME, the above would watch ALL files (ending in .txt) underneath $HOME (whatever find returns basically) and then we grep the changes for files called \"secret\" so we're notified if they change.\n\nIf it so happens that the command you want to run has the same switches as spook does, command line argument parsing can be disabled like this:\n\n```sh\nfind . -type f -name \"*.pdf\" | spook -- ls -o {file}\n```\n\nSo far I've implemented the features of entr most useful to me. If more advanced features of are desired I'd suggest using spook with a Spookfile since that gives you almost unlimited flexibility. Or use the real entr - it is a very useful tool.\n\n\n### Adding a simple REPL\n\nAs of Spook 0.8.4 there is a basic implementation of a REPL that can also be extended quite easily. To use the repl you would do something like this in the Spookfile:\n\n```moonscript\n-- the function given to the shell below is the prompt, it should be a function\n-- it is called on every screen update.\n:repl = require('shell') -\u003e getcwd! .. ' spook% '\nS = require 'syscall'\non_read S.stdin, repl\n```\n\nPress enter and the repl will present itself. Type \"help\" for a list of default commands. Defining more commands work like this:\n\n```moonscript\n:repl, :cmdline = require('shell') -\u003e getcwd! .. ' spook% '\nS = require 'syscall'\n\n-- the first argument is the command name, second the help text\ncmdline\\cmd \"date\", \"Show the current date\", (screen) -\u003e\n  print os.date!\n\n-- the arguments given to the function (last arg) are first the\n-- screen object which may or may not be very interesting. The\n-- following arguments are whatever is given after the name of\n-- the command tokenized using space as delimiter.\ncmdline\\cmd \"date\", \"Show the current date\", (screen) -\u003e\n  print os.date!\n\n:concat = table\ncmdline\\cmd \"echo\", \"Echo whatever you want\", (screen, ...) -\u003e\n  args = {...}\n  str = concat args, '#'\n  print str\n-- examples of the output of above:\n-- echo one two three\n-- one#two#three\n\n-- it's possible to define a dynamic handler that would be a catchall for\n-- anything not defined, like this:\ncmdline\\dynamic (c, key, value) -\u003e\n  (screen, ...) -\u003e\n    args = {key}\n    insert args, arg for arg in *{...}\n    os.execute concat(args, ' ')\n-- above would try to execute anything not already defined\n-- as a program on the PATH\n\non_read S.stdin, repl\n```\n\n### Timers, Signals and Readers\n\nNow for something completely different and slightly more experimental still. Perhaps you're not interested in file system events or perhaps you're interested in combining those events with other events on the system. Whatever you want, this is how you'd define a timer in the Spookfile:\n\n```moonscript\nafter 5.0, (t) -\u003e\n  print \"yay I was called!\"\n  t\\again! -- this would be a somewhat inefficient way of creating a recurring timer (needs a syscall)\n```\n\nAs mentioned above, recurring timers using \"again\" are somewhat inefficient. It's probably better to use the \"every\" function instead in that case:\n\n```moonscript\nevery 5.0, (t) -\u003e\n  print \"this will print every 5 seconds\"\n```\n\nThere is also the old function \"timer\" which behaves exactly like \"after\" above.\n\nAnd signal handlers are defined like this:\n\n```moonscript\non_signal \"int\", (receiver) -\u003e\n  print \"Why? Please don't interrupt me!\"\n  os.exit(1) -- you should probably deal with this in a sane way\n```\n\nFinally, reading from something else (like a socket) - please see the specs here [spec/event_loop_spec.moon](spec/event_loop_spec.moon). From the spookfile you'd do something like:\n\n```moonscript\nS = require 'syscall'\nstdin = S.stdin\non_read stdin, (reader, fd) -\u003e\n  data = fd\\read!\n  print \"Got some data: #{data}\"\n```\n\nThese functions, eg. on_read, on_signal etc are actually methods on the global spook object. So, if you want to use them from a file you require you can do so like this instead:\n\n```moonscript\nS = require 'syscall'\nstdin = S.stdin\n-- stdin = Types.fd(0) - if it's some other fd you MUST wrap it (S.stdin etc are already wrapped) or it gets GC:ed and weird things happen, see the ljsyscall project\n-- it's really _G.spook by the way, eg. it's a global object\nspook\\on_read stdin, (reader, fd) -\u003e\n  data = fd\\read!\n  print \"Got some data: #{data}\"\n```\n\n### Coroutines\n\nSpook, since release 0.8.0, wraps all event handlers in coroutines. This means that it is quite easy to use the asynchrony in a serial fashion rather than in a callback fashion. I don't believe this is especially relevant to the original use case of spook (eg. as a test feedback loop). However, since I've been using spook in other ways too I've found that a coroutine based flow can be quite helpful.\n\nSo, here's a brief example of Spook without and Spook with coroutines, first without:\n\n```moonscript\nevery 1.0, (t) -\u003e\n  print \"1 sec passed again\"\n\nevery 5.0, (t) -\u003e\n  _, _, status = os.execute \"sleep 2\"\n  print \"sleep status: #{status}\"\n```\n\nAbove, the function given to every will have been wrapped in a coroutine. However, since nothing in that function actually yields (coroutine.yield) or resumes (coroutine.resume), it will just work the way spook always did - in the above case it will even \"freeze\" spook completely for 2 seconds waiting for sleep to exit (second every function). So the first every function that should execute once per second will skip a second.\n\nThere is a process helper that has, among other things, an os.execute api compatible implementation that is coroutine based. Using that to implement the same code as above would look like this:\n\n```moonscript\n:execute = require 'process'\n\nevery 1.0, (t) -\u003e\n  print \"1 sec passed again\"\n\nevery 5.0, (t) -\u003e\n  _, _, status = execute \"sleep 2\"\n  print \"sleep status: #{status}\"\n```\n\nThere's not much difference but you will see that there is no pausing of the 1 sec timer. This is a trivial example of course. For more interesting examples, see [moonbar](https://github.com/johnae/moonbar).\n\nWhile you certainly CAN use os.execute as mentioned, I would recommend that you use the execute that comes with spook instead for job control (regardless of whether you care about coroutines). Like this:\n\n```moonscript\nexecute = require('process').execute\nevery 5.0, (t) -\u003e\n  _, _, status = execute \"sleep 2\"\n  print \"sleep status: #{status}\"\n```\n\nor, if you're using third party stuff, you might consider doing this (spooks own Spookfile does actually):\n\n```moonscript\nexecute = require('process').execute\nos.execute = execute\nevery 5.0, (t) -\u003e\n  _, _, status = os.execute \"sleep 2\"\n  print \"sleep status: #{status}\"\n```\n\nObviously above it won't make much difference to override the default os.execute but with third party code or code you don't want to change it may be extremely handy.\n\n*NOTE:* you should probably prefer using the execute that comes with spook rather than os.execute. If only for the ability to actually interrupt whatever spook is running using CTRL-C (another CTRL-C would kill spook itself). Unless you have some specific reason to use os.execute of course.\n\n### Notifications\n\nThis is how a simple notifier might look (load it using notify.add):\n\n```moonscript\ngetcwd = _G.getcwd\nproject_name = -\u003e\n  cwd = getcwd!\\split '/'\n  cwd[#cwd]\n\nmoon = require \"moon\"\n\n-- info is a table\nstart = (msg, info) -\u003e\n  print \"#{project_name!} starting: #{msg}\"\n  moon.p info -- debug\n\nsuccess = (msg, info) -\u003e\n  print \"#{project_name!} success: #{msg}\"\n  moon.p info -- debug\n\nfail = (msg, info) -\u003e\n  print \"#{project_name!} fail: #{msg}\"\n  moon.p info -- debug\n\n-- Finally those are exported in usual moonscript style\n:start, :success, :fail\n```\n\nA notifier can use ANY arbitrary names for the functions handling the notifications. Just know that generally start, success and fail will be called. Whatever else you do is completely up to you. And you don't have to use any notifiers at all.\n\nAs is mentioned further down, one place to put notifiers might be in $HOME/.spook/lib since that is already on the package.path. For example, different team members might agree that a good place to put the notifier could be in \"$HOME/.spook/lib/notifier.moon\". Everyone's notifier can be different but is still referred to by the same name. Or some code might be written where any and all notifiers under a certain directory get loaded. There's no restrictions really.\n\nA slightly more complex notification example for tmux might look like this:\n\n```moonscript\ngetcwd = _G.getcwd\nround = math.round\nproject_name = -\u003e\n  cwd = getcwd!\\split '/'\n  cwd[#cwd]\n\ntime_calc = (start, finish) -\u003e\n  round finish - start, 3\n\ntmux_set_status = (status) -\u003e\n  os.execute \"tmux set status-left '#{status}' \u003e /dev/null\"\n\ntmux_default_status = '#[fg=colour16,bg=colour254,bold]'\n\ntmux_fail_status = (info) -\u003e\n  tmux_default_status .. '#[fg=white,bg=red] FAIL: ' .. project_name! .. \" (#{time_calc(info.start_at, info.fail_at)} s) \" .. '#[fg=red,bg=colour234,nobold]'\n\ntmux_pass_status = (info) -\u003e\n  tmux_default_status .. '#[fg=white,bg=green] PASS: ' .. project_name! .. \" (#{time_calc(info.start_at, info.success_at)} s) \" .. '#[fg=green,bg=colour234,nobold]'\n\ntmux_test_status = (info) -\u003e\n  tmux_default_status .. '#[fg=white,bg=cyan] TEST: ' .. project_name! .. ' #[fg=cyan,bg=colour234,nobold]'\n\nspook = _G.spook\n\ntimer = nil\nstart = (msg, event) -\u003e\n  tmux_set_status tmux_test_status(event)\n  timer\\stop! if timer\n\nsuccess = (msg, info) -\u003e\n  tmux_set_status tmux_pass_status(info)\n  timer\\stop! if timer\n  timer = spook\\timer 7.0, (t) -\u003e tmux_set_status tmux_default_status\n  timer\\start!\n\nfail = (msg, info) -\u003e\n  tmux_set_status tmux_fail_status(info)\n  timer\\stop! if timer\n  timer = spook\\timer 7.0, (t) -\u003e tmux_set_status tmux_default_status\n  timer\\start!\n\nspook\\on_signal 'int', (s) -\u003e\n  tmux_set_status tmux_default_status\n  os.exit(1)\n\n:start, :success, :fail\n```\n\nOr another example that I'm currently using on Linux (you'll have to tweak it slightly to use your own icons):\n\n```moonscript\nsuccess_icon = \"#{os.getenv('HOME')}/Pictures/icons/essential/success.svg\"\nfail_icon = \"#{os.getenv('HOME')}/Pictures/icons/essential/error.svg\"\n\nnotify_send = (success, project, msg) -\u003e\n  cmd = if success\n    \"notify-send -i #{success_icon} -a 'Spook' -u normal '#{project}: SUCCESS' '#{msg}'\"\n  else\n    \"notify-send -i #{fail_icon} -a 'Spook' -u critical '#{project}: FAIL' '#{msg}'\"\n  os.execute cmd\n\ngetcwd = _G.getcwd\nround = math.round\nproject_name = -\u003e\n  cwd = getcwd!\\split '/'\n  cwd[#cwd]\n\ntime_calc = (start, finish) -\u003e\n  round finish - start, 3\n\n{\n  success: (msg, info) -\u003e\n    :start_at, success_at: end_at = info\n    msg = \"tests passed in #{time_calc(start_at, end_at)}s\"\n    notify_send true, project_name!\\upper!, msg\n\n  fail: (msg, info) -\u003e\n    :start_at, fail_at: end_at = info\n    msg = \"tests failed in #{time_calc(start_at, end_at)}s\"\n    notify_send false, project_name!\\upper!, msg\n}\n```\n\n### Extending Spook\n\nThere's a package.path pointing to $HOME/.spook/lib as well as PROJECT_DIR/.spook/lib which means you can put any extensions in there (written in MoonScript or Lua) and load them easily from your Spookfile. This means you could extend functionality in infinite ways. This is really just convenience since you could just as easily add your own package paths directly to the Spookfile. However, to me it seems $HOME/.spook is a reasonable place to put such things as well PROJECT_DIR/.spook.\n\nBasically, let's say you've got some code in $HOME/.spook/lib/utils/boom.moon that you'd like to use in the Spookfile. This is how you'd do that:\n\n```moonscript\nboom = require \"utils.boom\"\n\nboom.blow_up!\n```\n\nThat _may_ be overridden by a local file in PROJECT_DIR/.spook/lib which takes precedence (eg. named the same as the one in the global search path).\n\n\n### Additional functions available in the global scope\n\nThese can be used in the notifier and any other code running in the context of spook (like stuff in $HOME/.spook/lib or code in the Spookfile):\n\n```moonscript\ngetcwd\n```\n\nChange the working directory.\n\n```moonscript\nchdir(\"/some/dir\")\n```\n\nThis returns the current working directory (where you run spook, probably your git checkout of your app).\n\n### License\n\nSpook is released under the MIT license (see [LICENSE.md](LICENSE.md) for details).\n\n### Contribute\n\nAnything is welcome. Bug reports and pull requests most of all.\n\nUse the [Github issue tracker](https://github.com/johnae/spook/issues) for bug reports please.\nI can be reached directly at \\\u003cjohn at insane.se\\\u003e as well as through github.\n\n### In closing\n\nAnything you can do with LuaJIT (FFI for example) you can do with Spook. Either in the Spookfile or files that you require (like the notifier). MoonScript and Lua are really powerful and fun and, coupled with LuaJIT, they're ridiculously fast too compared to basically all other dynamic languages and runtimes. They're not used often enough in my opinion. You should really give them a try - they deserve it, regardless of whether you like Spook or not.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjohnae%2Fspook","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjohnae%2Fspook","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjohnae%2Fspook/lists"}