Skip to content

Commit

Permalink
Improve API of hooks and work on implementation, extend docs for hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
ad-si committed Sep 16, 2024
1 parent 73c183e commit 8ee1c51
Show file tree
Hide file tree
Showing 7 changed files with 265 additions and 130 deletions.
107 changes: 87 additions & 20 deletions docs-source/cli/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,40 @@ Hooks can either be specified via the config file or via hook files.
If the files have an extension (e.g. `post-add.lua`) the corresponding
interpreter will be used.
Otherwise the file will be executed as a shell script.
Currently supported interpreters are: `lua`, `python3`, `ruby`, `node`.

Currently supported interpreters are:
`lua`, `python3`, `ruby`, `node`, and [`v`][vlang].
It's recommended to write you scripts in the [V programming language][vlang]
due to its high performance (script is compiled on first execution),
its great ergonomics, and its comprehensive standard library.
The compiled versions of the script will be cached as
`_v_executable_.<name-of-script>` in the hooks directory.
?
[vlang]: https://vlang.io

Another good alternative is [Lua](https://www.lua.org/)
as it is simple, lightweight, and fast.
Futhermore, future versions of TaskLite will include the Lua interpreter
to make it independent of the system's installed Lua interpreter.

If the hook files are shell scripts, they must be executable (`chmod +x`).
Otherwise they can't be executed directly by TaskLite.

It's recommended to use [Lua](https://www.lua.org/) for hooks
as it's simple, lightweight, and has the best performance.
Futhermore, future versions of TaskLite will include a Lua interpreter
for even better performance.
The file names must start with the stage they are for (`pre` or `post`),
followed by a dash and the stage name (e.g. `pre-add`), and optionally
followed by an underscore and a description.
E.g `pre-add_validate_task.v` or `pre-exit_sync.v`.

They are executed in alphabetical order.
If you want to ensure a specific order,
you can include a number in the description.
E.g. `pre-add_01_x.v`, `pre-add_02_y.v`, ….

> [!WARNING]
> Multiple `pre-add` hooks are not supported yet,
> but will be in the future.
To ignore a hook, you can prefix the file name with an underscore (`_`).


## Stages
Expand All @@ -44,8 +69,7 @@ Following stages are available:
Can be used to prevent modification of task.
- `post-modify` - After task was modified.
- Exit
- `pre-exit` - Pre printing results
- `post-exit` - Last thing before program termination
- `pre-exit` - Last thing before program termination

The hooks receive JSON data from TaskLite via stdin.
We're using JSON5 here for better readability.
Expand Down Expand Up @@ -117,7 +141,7 @@ with improved formatting and coloring.
</pre></td>
<td><pre>
{
taskToAdd: {},
task: {},
message: "…",
…,
}
Expand All @@ -136,11 +160,7 @@ with improved formatting and coloring.
}
</pre></td>
<td><pre>
{
taskAdded: {},
message: "…",
}
{ message: "…", … }
</pre></td>
<td>
<pre>{ message: "…", … }</pre>
Expand All @@ -152,12 +172,12 @@ with improved formatting and coloring.
<td><pre>
{
arguments: [],
taskOriginal: {}
taskToModify: {}
}
</pre></td>
<td><pre>
{
taskToModify: {},
task: {},
message: "…",
}
Expand All @@ -177,11 +197,7 @@ with improved formatting and coloring.
}
</pre></td>
<td><pre>
{
taskModified: {},
message: "…",
}
{ message: "…", … }
</pre></td>
<td>
<pre>{ message: "…", … }</pre>
Expand Down Expand Up @@ -211,6 +227,57 @@ tl ndjson | grep $ULID_OF_TASK | head -n 1 | jq
```


## Config

You can add a `hooks` field to your config file like this:

```yaml
hooks:
directory: /Users/adrian/Dropbox/TaskLite/hooks
launch:
post: []
pre: []
add:
post: []
pre:
-
interpreter: v
body: |
import json
struct Task {
mut:
body string
}
struct TaskData {
mut:
task_to_add Task @[json: 'taskToAdd']
message string @[omitempty]
warning string @[omitempty]
}
fn main() {
stdin := os.get_raw_lines_joined()
mut task_data := json.decode(TaskData, stdin)!
task_body := task_data.task_to_add.body
if task_body.contains('coke') {
task_data.warning = 'Coke? Rather have some milk!'
task_data.task_to_add.body = task_body.replace('coke', 'milk')
}
println(json.encode(task_data))
}
modify:
post: []
pre: []
exit:
post: []
pre: []
```
## Examples
### Shell
Expand Down
45 changes: 32 additions & 13 deletions tasklite-core/source/Cli.hs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ import Config (
HooksConfig (..),
addHookFilesToConfig,
)
import Hooks (HookResult (message), executeHooks)
import Control.Arrow ((>>>))
import Hooks (executeHooks, formatHookResult)
import ImportExport (
backupDatabase,
dumpCsv,
Expand Down Expand Up @@ -228,7 +229,7 @@ import Utils (
TagText,
parseUtc,
ulidText2utc,
(<$$>),
(<!!>),
)


Expand Down Expand Up @@ -1339,15 +1340,24 @@ printOutput appName argsMb config = do
fileContent <- readFile filePath
pure (filePath, perm, fileContent)
)
let (configNorm, errors) =
addHookFilesToConfig configNormHookDir hookFilesPermContent
P.when (not $ null errors) $
["WARNING:\n"]
<> errors
& P.traverse_
( pretty
>>> annotate (color Yellow)
>>> hPutDoc P.stderr
)

let configNorm = addHookFilesToConfig configNormHookDir hookFilesPermContent

-- Run pre-launch hooks
preLaunchResults <- executeHooks "" configNorm.hooks.launch.pre
let preLaunchHookMsg =
preLaunchResults
<&> \case
Left error -> pretty error
Right hookResult -> pretty hookResult.message
Right hookResult -> formatHookResult hookResult
& P.fold

connection <- setupConnection configNorm
Expand All @@ -1356,11 +1366,8 @@ printOutput appName argsMb config = do
-- SQLite.setTrace connection $ Just P.putStrLn

migrationsStatus <- runMigrations configNorm connection
nowElapsed <- timeCurrentP

let
now = timeFromElapsedP nowElapsed :: DateTime

-- Run post-launch hooks
progName <- getProgName
args <- case argsMb of
Just args -> pure args
Expand All @@ -1378,20 +1385,32 @@ printOutput appName argsMb config = do
postLaunchResults
<&> \case
Left error -> pretty error
Right hookResult -> pretty hookResult.message
Right hookResult -> formatHookResult hookResult
& P.fold

nowElapsed <- timeCurrentP
let now = timeFromElapsedP nowElapsed :: DateTime
doc <- executeCLiCommand configNorm now connection progName args

-- TODO: Use withConnection instead
SQLite.close connection

-- Run pre-exit hooks
preExitResults <- executeHooks "" configNorm.hooks.exit.pre
let preExitHookMsg =
preExitResults
<&> \case
Left error -> pretty error
Right hookResult -> formatHookResult hookResult
& P.fold

-- TODO: Remove color when piping into other command
putDoc $
preLaunchHookMsg
<$$> migrationsStatus
<> doc
<$$> postLaunchHookMsg
<!!> migrationsStatus
<!!> postLaunchHookMsg
<!!> doc
<!!> preExitHookMsg


exampleConfig :: Text
Expand Down
71 changes: 48 additions & 23 deletions tasklite-core/source/Config.hs
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,8 @@ import Data.Aeson (
(.:?),
)
import Data.Hourglass (TimeFormat (toFormat), TimeFormatString)
import Data.Text as T (
dropEnd,
isInfixOf,
pack,
replace,
split,
strip,
stripPrefix,
)
import Data.Text (dropEnd, pack, split, stripPrefix)
import Data.Text qualified as T
import Data.Yaml (encode)
import Prettyprinter.Internal (Pretty (pretty))
import Prettyprinter.Render.Terminal (
Expand Down Expand Up @@ -142,8 +135,8 @@ defaultHooksConfig =
}


addHookFilesToConfig :: Config -> [(FilePath, b, Text)] -> Config
addHookFilesToConfig = do
addHookFilesToConfig :: Config -> [(FilePath, b, Text)] -> (Config, [Text])
addHookFilesToConfig config = do
let
buildHook :: FilePath -> Text -> Hook
buildHook filePath content =
Expand Down Expand Up @@ -190,18 +183,50 @@ addHookFilesToConfig = do
}
_ -> hConfig

P.foldl $ \conf (filePath, _, fileContent) ->
case split (== '-') $ pack $ takeBaseName filePath of
[stage, event] ->
conf
{ hooks =
addToHooksConfig
event
stage
(buildHook filePath fileContent)
conf.hooks
}
_ -> conf
getStageAndEvent :: FilePath -> [Text]
getStageAndEvent filePath = do
let fileName =
filePath
& takeBaseName
& pack

-- Prefix with "_" to ignore files
if "_" `T.isPrefixOf` fileName
then []
else
fileName
& split (== '_')
& P.headMay
& fromMaybe ""
& split (== '-')

P.foldl'
( \(conf, errors) (filePath, _, fileContent) ->
case getStageAndEvent filePath of
[stage, event] ->
( conf
{ hooks =
addToHooksConfig
event
stage
(buildHook filePath fileContent)
conf.hooks
}
, errors <> []
)
[] -> (conf, errors)
filenameParts ->
( conf
, errors
<> [ ("`" <> (filenameParts & T.intercalate "-") <> "` ")
<> "is not a correct hook name.\n"
<> "Hook file names must be in the format: "
<> "<stage>-<event>_<description>.<ext>\n"
<> "E.g `post-launch.v`, or `pre-exit.lua`.\n"
]
)
)
(config, [])


data Config = Config
Expand Down
Loading

0 comments on commit 8ee1c51

Please sign in to comment.