Skip to content

Commit e27929e

Browse files
committed
Execute Lua hooks using the HsLua embedded interpreter
1 parent 09565db commit e27929e

6 files changed

Lines changed: 380 additions & 32 deletions

File tree

docs-source/cli/hooks/examples.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,42 @@ fn main() {
276276
```
277277

278278

279+
## Lua Scripts
280+
281+
Save the examples in the hooks directory as `post-add.lua`, etc.
282+
Lua is the recommended language because TaskLite includes an embedded
283+
Lua interpreter with the `tl` namespace for utilities like JSON parsing.
284+
285+
286+
### Post Add
287+
288+
This example automatically adds a `music` tag when a task has the `jazz` tag:
289+
290+
```lua
291+
stdin = io.read("*all")
292+
data = tl.json.decode(stdin)
293+
294+
if data.taskAdded.tags == tl.json.null then
295+
return
296+
end
297+
298+
for _, tag in ipairs(data.taskAdded.tags) do
299+
if tag == "jazz" then
300+
local handle = io.popen(
301+
"tasklite tag music " .. data.taskAdded.ulid,
302+
"r"
303+
)
304+
if handle then
305+
handle:read("*a")
306+
handle:close()
307+
end
308+
print(tl.json.encode({message = "Added music tag"}))
309+
return
310+
end
311+
end
312+
```
313+
314+
279315
## Shell Scripts
280316

281317
Save the examples in the hooks directory as `pre-launch.sh`, etc.

docs-source/cli/hooks/main.md

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,47 @@ Otherwise, the file will be executed as a shell script.
1717

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

28-
Another good alternative is [Lua](https://www.lua.org/)
29-
as it is simple, lightweight, and fast.
30-
Furthermore, future versions of TaskLite will include the Lua interpreter
31-
to make it independent of the system's installed Lua interpreter.
23+
**Lua is the recommended language** for writing hooks because TaskLite
24+
includes an embedded Lua interpreter, making hooks portable and independent
25+
of system installations. Lua hooks also have access to the `tl` namespace
26+
which provides useful utilities (see below).
27+
28+
Alternatively, you can use the [V programming language][vlang]
29+
for high performance (scripts are compiled on first execution).
30+
The compiled versions will be cached as
31+
`_v_executable_.<name-of-script>` in the hooks directory.
32+
33+
34+
## The `tl` Namespace (Lua Only)
35+
36+
Lua hooks have access to a global `tl` namespace that provides
37+
TaskLite-specific utilities. Currently available:
38+
39+
40+
### `tl.json`
41+
42+
A JSON encoding/decoding module:
43+
44+
- `tl.json.decode(string)` - Parse a JSON string into a Lua table
45+
- `tl.json.encode(table)` - Convert a Lua table to a JSON string
46+
- `tl.json.null` - A value representing JSON null (for comparisons)
47+
48+
Example:
49+
50+
```lua
51+
stdin = io.read("*all")
52+
data = tl.json.decode(stdin)
53+
54+
if data.taskAdded.tags == tl.json.null then
55+
return
56+
end
57+
58+
-- Process the task...
59+
print(tl.json.encode({message = "Hook completed"}))
60+
```
3261

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

tasklite-core/package.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ library:
5959
- ansi-terminal >= 0.11 && < 1.2
6060
- bytestring >= 0.11 && < 0.13
6161
- cassava >= 0.5.2 && < 0.6
62+
- hslua >= 2.3 && < 2.4
63+
- hslua-aeson >= 2.3 && < 2.4
6264
- containers >= 0.6 && < 0.8
6365
- colour >= 2.3 && < 2.4
6466
- directory >= 1.3 && < 1.4

tasklite-core/source/Hooks.hs

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ import Protolude qualified as P
1818

1919
import Control.Arrow ((>>>))
2020
import Data.Aeson qualified as Aeson
21+
import Data.ByteString.Lazy qualified as BL
2122
import Data.Text (Text)
2223
import Data.Text qualified as T
24+
import Data.Text.Encoding qualified as TE
2325
import Options.Applicative.Arrows (left)
2426
import Prettyprinter (Doc, annotate, pretty)
2527
import Prettyprinter.Render.Terminal (AnsiStyle, Color (Red, Yellow))
@@ -28,6 +30,7 @@ import System.Process (readProcess)
2830

2931
import Config (Config, Hook (body, filePath, interpreter))
3032
import ImportTask (ImportTask)
33+
import LuaRunner (runLuaHook)
3134
import Utils (colr, (<!!>))
3235

3336

@@ -91,8 +94,6 @@ executeHooks stdinText hooks = do
9194
if
9295
| s `P.elem` ["javascript", "js", "node", "node.js"] ->
9396
("node", ["-e"], ExecStdin)
94-
| s `P.elem` ["lua"] ->
95-
("lua", ["-e"], ExecStdin)
9697
| s `P.elem` ["python", "python3", "py"] ->
9798
("python3", ["-c"], ExecStdin)
9899
| s `P.elem` ["ruby", "rb"] ->
@@ -103,6 +104,24 @@ executeHooks stdinText hooks = do
103104
| otherwise ->
104105
("", [""], ExecFile)
105106

107+
-- Execute a Lua hook using the embedded interpreter
108+
runEmbeddedLua :: Text -> IO String
109+
runEmbeddedLua luaCode = do
110+
result <- runLuaHook luaCode stdinText
111+
case result of
112+
P.Right output -> pure $ T.unpack output
113+
P.Left err ->
114+
pure $
115+
T.unpack $
116+
TE.decodeUtf8 $
117+
BL.toStrict $
118+
Aeson.encode $
119+
Aeson.object ["error" Aeson..= err]
120+
121+
-- Check if a file extension or interpreter name indicates Lua
122+
isLua :: String -> P.Bool
123+
isLua s = s `P.elem` ["lua"]
124+
106125
hookToResult <-
107126
P.sequence $
108127
hooks <&> \hook -> do
@@ -112,29 +131,38 @@ executeHooks stdinText hooks = do
112131
"" ->
113132
-- Is executed with shell
114133
readProcess fPath [] stdinStr
115-
ext -> do
116-
let (interpreter, cliFlags, execMode) = getInterpreter ext
117-
case execMode of
118-
ExecStdin -> do
134+
ext
135+
-- Use embedded Lua interpreter for .lua files
136+
| isLua ext -> do
119137
fileContent <- P.readFile fPath
120-
readProcess
121-
interpreter
122-
(P.concat [cliFlags, [T.unpack fileContent]])
123-
stdinStr
124-
ExecFile -> do
125-
readProcess
126-
interpreter
127-
(P.concat [cliFlags, [fPath]])
128-
stdinStr
138+
runEmbeddedLua fileContent
139+
| otherwise -> do
140+
let (interpreterCmd, cliFlags, execMode) = getInterpreter ext
141+
case execMode of
142+
ExecStdin -> do
143+
fileContent <- P.readFile fPath
144+
readProcess
145+
interpreterCmd
146+
(P.concat [cliFlags, [T.unpack fileContent]])
147+
stdinStr
148+
ExecFile -> do
149+
readProcess
150+
interpreterCmd
151+
(P.concat [cliFlags, [fPath]])
152+
stdinStr
129153
---
130154
Nothing -> do
131-
let
132-
(interpreter, cliFlags, _) =
133-
getInterpreter (T.unpack hook.interpreter)
134-
readProcess
135-
interpreter
136-
(P.concat [cliFlags, [T.unpack hook.body]])
137-
stdinStr
155+
-- Use embedded Lua interpreter for inline Lua hooks
156+
if isLua (T.unpack hook.interpreter)
157+
then runEmbeddedLua hook.body
158+
else do
159+
let
160+
(interpreterCmd, cliFlags, _) =
161+
getInterpreter (T.unpack hook.interpreter)
162+
readProcess
163+
interpreterCmd
164+
(P.concat [cliFlags, [T.unpack hook.body]])
165+
stdinStr
138166

139167
let parsedHookResults :: [P.Either Text HookResult] =
140168
hookToResult

0 commit comments

Comments
 (0)