todoke 1.2.0

A rule-driven file and URL dispatcher: hands incoming paths (or URLs) to the right handler based on TOML-defined rules.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
# <img src="assets/icon.png" width="32" align="left" alt="" /> todoke

<p align="center">
  <img src="assets/logo.svg" width="560" alt="todoke — rule-driven file dispatcher" />
</p>

<p align="center">
  <b>A rule-driven file dispatcher that hands incoming paths to the right editor or script — <i>届け</i>.</b>
</p>

<p align="center">
  <a href="https://crates.io/crates/todoke"><img src="https://img.shields.io/crates/v/todoke.svg" alt="crates.io"/></a>
  <a href="https://github.com/yukimemi/todoke/actions"><img src="https://github.com/yukimemi/todoke/actions/workflows/ci.yml/badge.svg" alt="CI"/></a>
  <a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT"/></a>
</p>

`todoke` takes one or more file paths and decides what to do with each of
them — by regex-matching the path against a TOML ruleset. A rule can target
a long-running neovim (reused via msgpack-RPC), any generic CLI editor, or a
raw shell script. Perfect as your OS default program for text files, as
`$EDITOR`, or as a standalone file handler.

## Features

- **Rule-based routing**: regex patterns in TOML decide what handles each
  file. Different paths → different handlers (VSCode for one project, nvim
  for another, a shell script for a third).
- **Single-instance neovim** via named pipes / unix sockets: `todoke`
  connects to a running nvim and sends `:edit` over msgpack-RPC. Works on
  Windows via `\\.\pipe\...` — no Deno, no plugin framework, no cold start.
- **Sync or async** per rule: `sync = true` blocks until the handler exits
  (perfect for `git commit`), `sync = false` fires and forgets (perfect for
  double-clicking files in the OS file explorer).
- **Tera templating** throughout the config: `{{ file_path }}`,
  `{{ env.HOME }}`, `{% if is_windows() %}…{% endif %}`, structural
  conditionals that include whole editor / rule blocks, every Tera filter.
- **Generic CLI support**: any command-line tool works (`code`, `vim`,
  `helix`, `subl`, `emacsclient`, `bat`, `pandoc`, …) without custom code.
- **Fast**: static Rust binary, cold start in milliseconds.

## Install

```sh
cargo install todoke
```

Binary lives at `~/.cargo/bin/todoke`. Make sure that's on your `PATH`.

## Quick start

`todoke` works out of the box with a bundled default config — it routes
everything to a single shared neovim instance, except `$EDITOR`-callback
files (`COMMIT_EDITMSG` etc.) which always get a fresh `sync = true`
instance so `git commit` works.

To customize, drop a file at:

- Linux / macOS / Windows: `~/.config/todoke/todoke.toml`

Minimal example:

```toml
# ~/.config/todoke/todoke.toml

# kind = "neovim" opts into msgpack-RPC reuse; "exec" (default) just spawns.
[todoke.nvim]
kind = "neovim"
command = "nvim"
listen = '{% if is_windows() %}\\.\pipe\nvim-todoke-{{ group }}{% else %}/tmp/nvim-todoke-{{ group }}.sock{% endif %}'

[todoke.code]
command = "code"
[todoke.code.args]
remote = ["--reuse-window"]
new    = ["--new-window"]

[todoke.firefox]
command = "firefox"
# gui = true skips `cmd /c start` on Windows, so no cmd window flashes
# before firefox. Unix: no-op.
gui = true

# A second firefox target specifically for issue: inputs — the URL is
# constructed from the capture group, so append_inputs = false tells the
# exec backend not to tack the raw "issue:42" onto the command line as a
# second positional.
[todoke.gh-issue]
command = "firefox"
gui = true
append_inputs = false
args.default = ["https://github.com/yukimemi/todoke/issues/{{ cap.1 }}"]

# Git-ref target: opens the GitHub tree browser at a branch / tag / sha.
[todoke.gh-ref]
command = "firefox"
gui = true
append_inputs = false
args.default = ["https://github.com/yukimemi/todoke/tree/{{ input }}"]

# git commit, rebase, etc. — always a blocking fresh nvim.
[[rules]]
name = "editor-callback"
match = '(?i)/(COMMIT_EDITMSG|MERGE_MSG|git-rebase-todo)$'
to = "nvim"
mode = "new"
sync = true

# GitHub URLs → firefox (URL is auto-appended by the exec backend)
[[rules]]
name = "gh"
match = '^https?://(www\.)?github\.com/'
to = "firefox"

# Route files under ~/src/company/ to VSCode.
[[rules]]
name = "work"
match = '/src/company/'
to = "code"
mode = "remote"

# Raw strings — custom-scheme bare ids like `issue:42` auto-detect as Raw
# so this rule fires without `--todoke-as`. Capture groups are available to the
# handler as `{{ cap.1 }}` / `{{ cap.name }}`.
[[rules]]
name = "gh-issue"
match = '^issue:(\d+)$'
to = "gh-issue"

# Git refs — branch names, tags, short SHAs, etc. `input_type = "raw"`
# pins this rule to `--todoke-as raw` so that bare words like `HEAD` / `main`,
# which auto-detect as File, don't accidentally trigger the GitHub URL
# handler when you meant to open a local file by that name.
[[rules]]
name = "gh-ref"
match = '^(HEAD|main|master|develop|v?\d+\.\d+\.\d+|[0-9a-f]{7,40})$'
to = "gh-ref"
input_type = "raw"

# URL fallback: any other URL → browser. Without this, non-GitHub URLs
# would fall through to the file default (nvim) and get dropped by the
# neovim backend (which only accepts files).
[[rules]]
name = "url-default"
match = '^https?://'
input_type = "url"
to = "firefox"

# Default: everything else (file inputs, mostly) goes to the shared nvim.
[[rules]]
name = "default"
match = '.*'
to = "nvim"
group = "default"
mode = "remote"
```

Then:

```sh
# Open any file in the right handler
todoke notes.md

# URLs work too — same rule engine routes them to a browser, a browser
# profile, or any CLI that accepts URLs.
todoke https://github.com/yukimemi/todoke  # → gh rule → firefox
todoke https://example.com                  # → url-default rule → firefox

# Raw strings match rules too. `<scheme>:<body>` bare ids auto-detect as
# Raw so gh-issue fires without `--todoke-as`. Captures are available as
# `{{ cap.N }}`.
todoke issue:42      # → firefox opens issues/42

# Bare words like `HEAD` or `Makefile` auto-detect as File (so
# `$EDITOR=todoke Makefile` Just Works — see the $EDITOR section below).
# When you want `HEAD` routed as a git ref instead, pass `--todoke-as raw`
# and wire the matching rule with `input_type = "raw"`:
todoke --todoke-as raw HEAD # → firefox opens the repo tree at HEAD

# See which rule would match, without actually dispatching
todoke check notes.md https://example.com issue:42

# Same dispatch logic, don't execute
todoke --todoke-dry-run notes.md

# Lint the config for common footguns
todoke doctor
```

### Recipe: one target, many variants

Neovim has several front-ends — `nvim` itself, `neovide`, `nvim-qt`, … —
and you'll probably want to swap between them without rewriting rules.
Because the whole config is pre-rendered through Tera, a list in `[vars]`
plus a single conditional covers every combination:

```toml
[vars]
# Swap this line to switch front-ends.
gui = "neovide"
# Wrappers that forward CLI args to an embedded nvim only after `--`.
# Raw `nvim` is not in this list because it would treat args after `--`
# as filenames.
wrapper_guis = ["neovide", "nvim-qt"]

[todoke.gui]
kind = "neovim"
command = "{{ vars.gui }}"
listen = '{% if is_windows() %}\\.\pipe\nvim-todoke-{{ group }}{% else %}/tmp/nvim-todoke-{{ group }}.sock{% endif %}'
# gui = true suppresses the transient cmd window on Windows when the
# handler is a GUI front-end (neovide / nvim-qt). Skip when using plain
# `nvim` in a separate terminal, which needs the console that the
# `cmd /c start` wrapper allocates.
gui = {{ vars.gui in vars.wrapper_guis }}

{% if vars.gui in vars.wrapper_guis %}
[todoke.gui.args]
remote = ["--"]
{% endif %}

[[rules]]
match = '.*'
to = "gui"
mode = "remote"
```

- `vars.gui = "nvim"``nvim FILE --listen PIPE`
- `vars.gui = "neovide"``neovide FILE -- --listen PIPE`
- `vars.gui = "nvim-qt"``nvim-qt FILE -- --listen PIPE`

One target definition, three valid command lines. Adding a new wrapper in
the future is one entry in `wrapper_guis`.

### Recipe: categorized `match` patterns

`match` accepts either a single regex string or an array. The array form
is OR-matched (hit any → rule fires) and is the right shape when a rule's
intent spans several unrelated sources — `$EDITOR`-callback files are a
classic example because every tool sprinkles its own filename convention:

```toml
[[rules]]
name = "editor-callback"
match = [
  # git
  '(?i)/(COMMIT_EDITMSG|MERGE_MSG|TAG_EDITMSG|EDIT_DESCRIPTION|git-rebase-todo|NOTES_EDITMSG|\.gitmessage)$',
  # svn / hg
  '(?i)/svn-commit\.tmp$',
  # Claude Code prompt temp files
  '(?i)/claude-prompt-.*$',
]
to = "nvim-term"
mode = "new"
sync = true
```

Each bucket is its own readable regex; extending for a new tool is
appending one line with a `# new-tool` comment instead of threading
another alternation into a long single-string pattern.

### Recipe: editor-flag passthrough (`+42 file.txt`)

Some `$EDITOR` callers (vim-aware Git frontends, `sudo -e`, etc.) pass
`nvim`-style flags ahead of the file — e.g. `+42 file.txt` to jump to
line 42. todoke's auto-detection would otherwise absolutize `+42` into
a file path. Two ways to handle it:

**Option A — `passthrough`** (simple; good for individual flag classes):

```toml
[[rules]]
name = "nvim-flag"
match = '^[-+]'          # matches against the RAW argv, pre auto-detect
to = "nvim-term"
sync = true
passthrough = true       # forward as-is to nvim's start-up argv

[[rules]]
name = "nvim-file"
match = '.*'
to = "nvim-term"
sync = true
```

`todoke +42 foo.txt bar.txt` now spawns `nvim +42 foo.txt bar.txt`
(multi-file still works, `+42` rides along as a flag). For spaced
values like `-c :set ft=md` where the flag and its value are separate
argv items, use `consumes` to pull the next argv along:

```toml
[[rules]]
name = "nvim-c"
match = '^-c$'
to = "nvim-term"
sync = true
passthrough = true
consumes = 1       # `-c` + next argv both forwarded as passthrough
```

For open-ended multi-value flags like `nvim -p a.txt b.txt c.txt` (tab
open) or `-o` / `-O` (splits), use `consumes_until`:

```toml
[[rules]]
name = "nvim-p"
match = '^-[pOo]$'
to = "nvim-term"
sync = true
passthrough = true
consumes_until = '^[-+]'    # keep eating argv until the next flag
```

And for the GNU-style `--` separator that means "everything after me is
for the target", use `consumes_rest`:

```toml
[[rules]]
name = "nvim-passthrough-rest"
match = '^--$'
to = "nvim-term"
sync = true
passthrough = true
consumes_rest = true
```

Exactly one of `consumes` / `consumes_until` / `consumes_rest` may be
set per rule (compile-time error otherwise).

Passthrough inputs are merged into the **normal rule's batch** that
shares the same `(target, group)` — so a passthrough rule's `mode` /
`sync` are only used when no normal rule routes to the same
target+group. On a merge the normal rule's values win and a runtime
warn is emitted if they differ (doctor can't catch it because
`group` / `to` are Tera templates that only resolve at dispatch).

**Option B — `joined`** (flexible; one rule captures the whole argv):

```toml
[[rules]]
name = "nvim-with-line"
match = '^(?P<pre>\+\d+ )?(?P<input>\S+)$'
to = "nvim-term"
sync = true
joined = true

[todoke.nvim-term.args]
default = ["{{ cap.pre | default(value='') | trim }}"]
# append_inputs = true is still default so {{ cap.input }} is opened
# by the handler after args; the captured flag rides in the args list.
```

`joined` matches once against the space-joined argv. The named capture
`input` is re-classified (so a nonexistent `foo.txt` still becomes a
`File` and `:edit`-able), and `cap.pre` ends up in the args. Use
`joined` when you want a single regex describing the full invocation
shape; use `passthrough` when each flag has its own rule.

### Recipe: gvim server reuse with flag passthrough

gvim has built-in `--servername` / `--remote-silent` — the vim-era
cousin of neovim's `--listen`/msgpack-RPC. A single `kind = "exec"`
target can re-use a gvim server per group and place passthrough flags
**before** the `--remote-silent <file>` chunk so gvim doesn't treat
them as extra filenames:

```toml
[todoke.gvim]
command = "gvim"
gui = true
[todoke.gvim.args]
default = [
  "--servername", "{{ group | upper }}",
  "{{ passthrough }}",                      # ← expanded inline, one argv per entry
  "--remote-silent", "{{ input }}",
]

[[rules]]
name = "vim-flag"
match = '^[-+]'
to = "gvim"
passthrough = true

[[rules]]
name = "default"
match = '.*'
to = "gvim"
```

An args element that is *exactly* `{{ passthrough }}` (with optional
surrounding whitespace / strip marks) is **expanded inline** — one
argv per passthrough string. So `[-c, :set ft=md]` stays two argv,
and an empty passthrough list contributes zero args (no literal `""`
floating around). `{{ input }}` is also referenced, so `append_inputs`
auto-suppresses the trailing append. Result: `gvim --servername
DEFAULT -c :set ft=md --remote-silent foo.txt` — exactly what gvim
expects, no double-paste, no empty-argv cruft.

(If you specifically want a joined string you can still write
`"{{ passthrough | join(sep=' ') }}"` — that path goes through the
normal single-argv render. Use the bare `{{ passthrough }}` element
when you want proper argv expansion, which is what gvim et al. need.)

### As `$EDITOR`

```sh
export EDITOR=todoke
git commit      # → todoke routes COMMIT_EDITMSG to nvim mode=new sync=true
```

The bundled default config is compatible with every `$EDITOR=…` caller I
know of (git, crontab, visudo, fc, mutt, …).

Any arg that isn't a URL (`foo://…`) or a custom-scheme bare id
(`issue:42`) auto-detects as a **file** — including extension-less
names like `Makefile`, `Dockerfile`, `Rakefile` and not-yet-existing
paths like `newfile.txt` or `/tmp/new.md`. So `todoke Makefile` and
`todoke newfile.txt` behave just like `vim Makefile` / `vim newfile.txt`
— rules match against the absolute path and the editor creates the
file on write.

### As OS default program (Windows)

Right-click a `.txt` → Open with → Choose another app → Browse → point at
`todoke.exe`. `todoke` honors the rules and opens the file in the correct
handler, spawning a new console if the target is a TUI.

## Configuration reference

### `[vars]`

User-defined variables available as `{{ vars.NAME }}` in every other
template:

```toml
[vars]
proj_root = "/home/me/src"
```

### `[todoke.<name>]`

A delivery target (the value behind a rule's `to = "<name>"`).

| field      | type                                | required | meaning                                                         |
| ---------- | ----------------------------------- | -------- | --------------------------------------------------------------- |
| `kind`     | `"exec"` / `"neovim"`               | no (default `"exec"`) | `"exec"` spawns the command; `"neovim"` reuses a running nvim via msgpack-RPC |
| `command`  | string                              | yes      | the handler binary (PATH-resolved)                              |
| `listen`   | string                              | neovim   | socket / named pipe path for RPC                                |
| `args`     | table of `<mode>``array<string>` | no       | args injected based on `rule.mode`; `args.default` is the fallback when no key matches |
| `append_inputs` | bool (optional)                | **auto** | `exec` kind only. `None` / omitted = **auto**: append each input's display string to the end of argv unless any `args` template references `{{ input }}` / `{{ file_* }}` / `{{ url_* }}` (cap is intentionally ignored). `true` = force append. `false` = force skip. |
| `append_passthrough` | bool (optional)           | **auto** | `exec` kind only. Same auto / true / false semantics as `append_inputs`, but keyed on `{{ passthrough }}` references in `args`. When you reference `{{ passthrough \| join(sep=' ') }}` to place flag-argv in a specific spot, the auto-append is suppressed so the values aren't pasted twice. |
| `env`      | table                               | no       | env vars passed to the spawned handler                          |
| `gui`      | bool                                | `false`  | Windows only (no-op on Unix): when `true`, detached spawns use `CREATE_NO_WINDOW + DETACHED_PROCESS` instead of `cmd /c start`, so no transient cmd window flashes before the GUI appears. Set to `true` for GUI handlers (`neovide`, `nvim-qt`, `code`, `firefox`, …) and leave `false` for terminal / TUI handlers that need a fresh console (`nvim` in a new window, `helix`, …). |

### `[[rules]]`

| field     | type                      | default      | meaning                                      |
| --------- | ------------------------- | ------------ | -------------------------------------------- |
| `name`    | string                    | `rule[N]`    | human-readable label (shown in `check`)      |
| `match`   | regex string or `[regex]` | required     | pattern(s) matched against a string derived from the input: **file** = canonicalized absolute path with `/` separators (`\\?\` verbatim prefix stripped), **url** = the URL string as-is, **raw** = the argument string as-is. Anchors like `^foo$` only fire for the URL/raw cases unless you design the regex for absolute paths. |
| `exclude` | regex string or `[regex]` | none         | when any `exclude` hits, the rule is skipped even if `match` hits — todoke falls through to the next rule |
| `to`      | string (Tera-templated)   | required     | key into `[todoke.*]`                        |
| `group`   | string                    | `"default"`  | instance identity (one nvim per group)       |
| `mode`    | string                    | `"remote"`   | free-form; `"remote"` / `"new"` are reserved for neovim behavior, otherwise used only to pick `args.<mode>` |
| `sync`    | bool                      | `false`      | `true` = block until handler exits           |
| `input_type` | `"file" \| "url" \| "raw"` or array | all kinds | restrict which input kinds this rule applies to. Example: `input_type = "raw"` makes the rule fire only for `--todoke-as raw` / auto-detected Raw inputs — useful for git-ref style patterns (`^HEAD$`, `^main$`) that must not shadow a local file of the same name. |
| `joined`   | bool                       | `false`     | match against the full argv-join (all positional args concatenated with spaces, **pre auto-detect**) instead of each input individually. On a hit, the named capture `input` is re-classified via `Input::from_arg` and becomes the batch's sole input; other captures ride along in `{{ cap.<name> }}` for the target's args templates. Designed for `$EDITOR=todoke +42 file.txt` style calls. Mutually exclusive with `passthrough`. |
| `passthrough` | bool                    | `false`     | match against the **raw argv** (pre auto-detect) per input. On a hit, the raw string is forwarded to the target's start-up argv instead of being opened/edited. Use for editor flags like `+42` / `-c :set ft=...`. Mutually exclusive with `joined`. |
| `consumes` | non-negative int           | `0`         | only valid with `passthrough = true`. When the rule matches, also forward the next **N** argv items as part of the same passthrough sequence. Designed for spaced-value flags like `-c :set ft=md` where the value is its own argv — a `consumes = 1` on `match = '^-c$'` keeps the flag and its value together. |
| `consumes_until` | regex string          | none        | only valid with `passthrough = true`. On match, keep absorbing argv until one matches this regex (or argv ends). The stopper argv itself is NOT consumed. Typical values: `'^[-+]'` (stop at next flag), `'^--$'` (stop at GNU separator). Designed for multi-value flags like `nvim -p a.txt b.txt c.txt`. |
| `consumes_rest` | bool                   | `false`     | only valid with `passthrough = true`. Consume every remaining argv as part of this passthrough. For "trailing args all go to this target" patterns, often paired with `match = '^--$'`. |

### Template context

Available in `rule.group`, `rule.to`, `todoke.*.command`, `todoke.*.listen`,
`todoke.*.args.*`:

| variable        | example                             | populated for |
| --------------- | ----------------------------------- | ------------- |
| `input`         | `/tmp/foo.md` or `https://…`        | always        |
| `input_type`    | `"file"` / `"url"` / `"raw"`         | always        |
| `file_path`     | `C:/Users/you/notes/todo.md`        | file inputs   |
| `file_dir`      | `C:/Users/you/notes`                | file inputs   |
| `file_name`     | `todo.md`                           | file inputs   |
| `file_stem`     | `todo`                              | file inputs   |
| `file_ext`      | `md` (no leading dot)               | file inputs   |
| `url_scheme`    | `https`                             | URL inputs    |
| `url_host`      | `github.com`                        | URL inputs    |
| `url_port`      | `443` or empty                      | URL inputs    |
| `url_path`      | `/yukimemi/todoke`                  | URL inputs    |
| `url_query`     | `tab=rs` or empty                   | URL inputs    |
| `url_fragment`  | `top` or empty                      | URL inputs    |
| `command_*`     | same five fields for the target command | always    |
| `cwd`           | current working directory           | always        |
| `group`         | resolved group                      | phase 3       |
| `rule`          | resolved rule name                  | phase 3       |
| `cap.0`         | full match of the `match` regex     | when a rule matched |
| `cap.1` / `cap.2` / … | numbered capture groups       | when defined        |
| `cap.<name>`    | named capture groups `(?P<name>…)`  | when defined        |
| `passthrough`   | array of raw argv strings from passthrough rules in the batch (`["+42", "-c", ":set ft=md"]`). Render with `{{ passthrough \| join(sep=' ') }}`, iterate via `{% for p in passthrough %}{{ p }}{% endfor %}`. Auto-suppresses the trailing append when referenced (see `append_passthrough`). | always (empty array when no passthrough) |
| `vars.<key>`    | your `[vars]` entries               | always        |
| `env.<KEY>`     | process env at todoke invocation    | always        |

`kind = "neovim"` targets accept **file inputs only** — URLs and raw
strings routed to a neovim target are logged and skipped. Route those to
a `kind = "exec"` target (e.g. a browser for URLs, any CLI that consumes
the raw string for `"raw"`).

And these todoke-specific Tera functions:

- `is_windows()`, `is_linux()`, `is_mac()` — booleans for OS branching.

Plus everything Tera ships — `replace`, `split`, `join`, `length`, `now()`,
structural `{% if %}` / `{% elif %}` / `{% else %}` blocks around editor
and rule sections, and all other stock [Tera features][tera].

## CLI reference

```
todoke [FILES]...            # dispatch files per rules (default action)
todoke check <FILES>...      # dry-run: show matched rule per file
todoke doctor                # lint the config for common footguns
todoke completion <shell>    # emit shell completion script
todoke --help
todoke --version

# v0.2+:
todoke list                    # list alive handler instances
todoke kill <group> | --all    # terminate instances
todoke config path | edit | validate | show
```

Flags (all long-only and `--todoke-` prefixed so they don't collide with
flags the downstream tool expects):

- `--todoke-config <PATH>` — override config path
- `--todoke-editor <NAME>` — bypass rule, force handler
- `--todoke-group <NAME>`  — bypass rule, force group
- `--todoke-as <KIND>`     — force input classification (`file` / `url` / `raw`)
- `--todoke-dry-run`       — print the resolved plan without executing
- `--todoke-verbose`       — repeat for more verbosity (info / debug / trace)

Positional args are collected with `trailing_var_arg = true` +
`allow_hyphen_values = true`, so `-c :set ft=md` / `+42` / `-abc` flow
straight through to whichever passthrough / normal rule matches — no
`--` separator required. Trade-off: **todoke's own flags must precede
the inputs** (e.g. `todoke --todoke-dry-run +42 foo.txt`); flags
written after the first input get absorbed as positional. That's the
right shape for `$EDITOR` callers, who never inject todoke flags
after inputs.

clap still consumes the `--` end-of-options marker itself, so if a
downstream tool *requires* a literal `--` in its argv, pass it some
other way — e.g. a `consumes_rest` rule keyed on a non-`--` sentinel.

Logging is also controllable via `RUST_LOG`.

## Roadmap

- **v0.1** *(this release)*: core dispatch, neovim + generic backends,
  `check`, `doctor`, `completion`, default config, `$EDITOR`
  compatibility, colored output.
- **v0.2**: `list` / `kill` / `config edit|validate|show`, `open` / `send`,
  neovim `remote + sync` via `nvim_buf_attach`.
- **v0.3**: `script` editor kind — run arbitrary shell commands as a
  handler, turning todoke into a general "open with rules" tool for any
  file type (previewer, formatter, pipeline, …).

## License

[MIT](./LICENSE) — © 2026 yukimemi.

[tera]: https://keats.github.io/tera/docs/#built-ins