fig-sys 5.0.0

FFI bindings and native library for fig (the comment-preserving JSON/YAML/TOML/… config engine). Used by the `fig` crate.
Documentation
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
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
//! `--help`/usage text for the `fig` CLI, one function per action plus the
//! top-level `general` summary. Pure output — no parsing or state.
//!
//! The two lists that drifted per action — the format names `--input`/
//! `--output` take and the `--embed` archetypes — are derived or shared
//! (`formatList`, `embed_archetypes`), so every action's help says what the
//! argument parser accepts.
const std = @import("std");
const fig = @import("fig");
const build_options = @import("build_options");
const types = @import("types.zig");
const Io = std.Io;

/// Whether the compiled `Format` member `name` is in this build: a registry
/// entry whose language was not compiled out, `canonical` when
/// `-Dcanonical=true`, and `gron`, which the CLI always carries.
fn compiledIn(comptime name: []const u8) bool {
    if (comptime std.mem.eql(u8, name, "gron")) return true;
    if (comptime std.mem.eql(u8, name, "canonical")) return build_options.lang_canonical;
    return fig.Language.entryFor(name).Lang != void;
}

/// The format names `parseFormatName` accepts in this build — every compiled
/// `Format` member, less `except` (gron for an action that writes a stored
/// document) — comma-separated and wrapped under `indent` to fit 72 columns.
/// Derived from the enum, so a new format is listed everywhere at once.
fn formatList(comptime indent: []const u8, comptime except: []const []const u8) []const u8 {
    comptime {
        @setEvalBranchQuota(50_000);
        var out: []const u8 = indent;
        var col = indent.len;
        var first = true;
        fields: for (@typeInfo(types.Format).@"enum".fields) |f| {
            for (except) |e| if (std.mem.eql(u8, e, f.name)) continue :fields;
            if (!compiledIn(f.name)) continue;
            const word = f.name;
            if (!first) {
                if (col + 2 + word.len > 72) {
                    out = out ++ ",\n" ++ indent;
                    col = indent.len;
                } else {
                    out = out ++ ", ";
                    col += 2;
                }
            }
            out = out ++ word;
            col += word.len;
            first = false;
        }
        return out ++ "\n";
    }
}

/// The languages an embed archetype's `<lang>` takes — the registry's
/// embeddable formats, the same set `args.embedTypeFromName` loops over.
const embed_langs = blk: {
    var out: []const u8 = "";
    for (@typeInfo(fig.Embed.InnerFormat).@"enum".fields, 0..) |f, i|
        out = out ++ (if (i == 0) "" else ", ") ++ f.name;
    break :blk out;
};

/// The `--embed <archetype>` vocabulary, indented to sit under the flag's own
/// line — the one text every action that takes `--embed` prints, matching
/// `args.embedTypeFromName`.
pub const embed_archetypes =
    "    Parametric families take a <lang> — " ++ embed_langs ++ ":\n" ++
    \\    `md-<lang>` (---<lang> frontmatter; bare `frontmatter` is ---/YAML,
    \\    so there is no `md-yaml`), `fenced-<lang>` (```lang code block),
    \\    `html-script[-<lang>]` (<script type="application/<lang>"> data
    \\    island) and `html-code[-<lang>]` (<pre><code class="language-<lang>">
    \\    visible block); a bare html-script/html-code is fig. Plus the fixed
    \\    presets `semicolons` (;;; JSON), `plus` (+++ TOML), and
    \\    `endmatter` (trailing ```endmatter block). Also accepted:
    \\    `frontmatter-yaml` and `endmatter-yaml`.
    \\
    ;

/// How `replace`/`set`/`insert` read a value argument (`value_arg.zig`).
pub const value_reading =
    \\  <value> is read as a fig value and written in the file's own syntax,
    \\    so it means the same thing in every format: 5, 2.5, true, null and
    \\    2026-09-22 are typed; hello, hello world, Yes and 007 are strings;
    \\    [1, 2] and {{a = 1, b = [x]}} are a sequence and a mapping; '"5"' is
    \\    the string 5. An empty value, one with a line break or with space
    \\    at either end, and one whose # would start a comment are strings
    \\    as written.
    \\  --string: take <value> as a string, whatever it looks like
    \\    (`set f.json version --string 1.10`)
    \\  --raw: splice <value> verbatim as source text in the file's format,
    \\    for what only it can spell (a YAML anchor, a TOML local datetime)
    \\
;

/// The <path> syntax and the Markdown-host note `replace` and `rename` share.
const path_and_markdown =
    \\  path format: dot syntax for keys, bracket syntax for indices
    \\    a key holding a . or [ is quoted or escaped: a."b.c", a.'b.c',
    \\    a["b.c"] (as `-o gron` prints it), or a.b\.c
    \\    example: school.class[0].student[3]
    \\  .md/.markdown files: edits the frontmatter/endmatter in place —
    \\    its archetype (YAML/JSON/TOML/fig frontmatter, fenced ```lang
    \\    frontmatter, YAML endmatter) is sniffed from the file,
    \\    defaulting to YAML when none is found
    \\
;

pub const title_string = "\n=========\n   FIG\n=========\n\n";

pub const Help = struct {
    pub fn general(term: *Io.Terminal, binary_name: []const u8) !void {
        try term.writer.print(
            \\Usage:
            \\  {s} <action> [action options] --[flags]
            \\Possible actions:
            \\  help: prints this text (default action)
            \\  version: prints version number
            \\  replace: replace the value at a path
            \\  rename: rename the key at a path
            \\  set: upsert a value (create the key, embed block, or file, if absent)
            \\  insert: add a new key or list item to a file
            \\  delete: remove a key or list item from a file
            \\  get: print a file or a specific part of a file to stdout
            \\  comment: add or edit a comment on part of a file
            \\  check: validate that one or more files parse cleanly
            \\  fmt: reformat a file in place (house style; gofmt-style)
            \\  convert: convert a file (or a host document's embedded region)
            \\    from one format/archetype to another; prints to stdout, or
            \\    writes the file in place with --write
            \\  patch: merge one document into another, in place and losslessly
            \\  lang: list the languages fig knows, or check a configured one
            \\
            \\Every action takes --lang <name> to read and write a file through
            \\a language configured in languages.figl rather than by extension
            \\(`{s} lang --help`).
            \\
            \\Arguments are read strictly: a word beginning with - that is not one
            \\of the action's flags, or one more argument than the action takes,
            \\is a usage error (exit 2). `--` ends the flags, so a file or value
            \\that begins with - goes after it (`{s} get -- -x.yaml`); `-` alone is
            \\stdin, and a negative number (-5, -2.5) is a value wherever it
            \\stands. fig has no bare -inf or nan (they are strings), so
            \\-inf reads as a flag.
            \\
            \\Exit status, for every action:
            \\  0  done (for `fmt --dry-run`/`--diff` and `check`: nothing to
            \\     change, everything parses)
            \\  1  the operation failed on the document: it does not parse, a
            \\     path or file is missing, an edit was refused, `fmt --dry-run`
            \\     found a change, `--strict` found a warning
            \\  2  the command line is wrong: an unknown action or flag, a missing
            \\     or surplus argument, a path or value that does not parse
            \\
            \\Any other action is handed to a `fig-<action>` program on your PATH,
            \\the way git does: `{s} schema lint f.json` runs `fig-schema lint f.json`
            \\with every argument after `schema` passed through untouched.
            \\
            \\Colour: stdout and stderr are each coloured when they are a
            \\terminal. NO_COLOR (set, to anything) turns colour off;
            \\CLICOLOR_FORCE turns it on where the stream is not a terminal.
            \\NO_COLOR wins when both are set.
            \\
            \\For information on action options, pass --help or -h
            \\to the action you would like to learn about.
            \\
        , .{ binary_name, binary_name, binary_name, binary_name });
        try term.writer.flush();
    }

    pub fn replace(term: *Io.Terminal, binary_name: []const u8) !void {
        try term.writer.print(
            \\Usage: {s} replace [--string | --raw] <file> <path> <value>
            \\  Replaces the value at <path>, which must already exist (`set`
            \\    creates it when absent). To rename the key there, use
            \\    `{s} rename`.
            \\
        ++ value_reading ++ path_and_markdown, .{ binary_name, binary_name });
        try term.writer.flush();
    }

    pub fn version(term: *Io.Terminal, binary_name: []const u8) !void {
        try term.writer.print(
            \\Usage: {s} version
            \\  Prints fig's version and its epoch, a name with no compatibility
            \\    meaning: `fig 5.0.0 "Texas Everbearing"`. Every artifact — the
            \\    CLI, the core, the Rust crates, the npm packages — carries the
            \\    one version. `--version` and `-v` are the same action. It takes
            \\    no arguments.
            \\
        , .{binary_name});
        try term.writer.flush();
    }

    pub fn rename(term: *Io.Terminal, binary_name: []const u8) !void {
        try term.writer.print(
            \\Usage: {s} rename <file> <path> <name>
            \\  Renames the key at <path> to <name>, keeping its value and
            \\    comments. <name> is the new key's name, which fig spells as the
            \\    file's format spells a key, quoting it where it must. A <path>
            \\    that ends in an index has no key, and a <name> the mapping
            \\    already holds would be a second key of that name: both are
            \\    refused, exit 1. To replace the value there, use `{s} replace`.
            \\
        ++ path_and_markdown, .{ binary_name, binary_name });
        try term.writer.flush();
    }

    pub fn set(term: *Io.Terminal, binary_name: []const u8) !void {
        try term.writer.print(
            \\Usage: {s} set [--embed <archetype>] [--string | --raw] <file> <path> <value>
            \\       {s} set [--embed <archetype>] [--string | --raw] --seq <file> <path> <item>...
            \\  Upsert: replace the value at <path>, or create it when absent —
            \\    one verb for `replace`+`insert`. Missing parent maps along <path>
            \\    are auto-created (`mkdir -p`); a segment that is an existing
            \\    non-map scalar is a type error and left untouched.
            \\  When <file> itself does not exist, it is CREATED and seeded with
            \\    <path>: <value>; `fig get <file>` then prints what was written.
            \\    The format comes from the extension, so a new file needs a known
            \\    one — .figl/.json/.jsonc/.yaml/.yml/.toml (or a .md host, via
            \\    --embed). .zon/.json5 have no from-scratch seed and must already
            \\    exist.
            \\  --seq: reconcile the sequence at <path> to exactly <item>..., keeping
            \\    the comments on items that survive (only new items are inserted,
            \\    only dropped ones removed; result order matches the arguments).
            \\  --embed <archetype>: target an embedded region of a host file.
            \\
        ++ embed_archetypes ++
            \\    When the host has no such block, it is CREATED (frontmatter at
            \\    the top, endmatter at the bottom) and seeded with <path>:
            \\    <value> — unless one would go at the top of a host that already
            \\    has frontmatter of another kind, which is refused (`{s} convert
            \\    --to-embed` changes it).
            \\
        ++ value_reading ++
            \\    A created key is written in the file's syntax too, so new keys
            \\    work for strict JSON. Each --seq <item> is read the same way.
            \\  path format: dot syntax for keys, bracket syntax for indices
            \\    a key holding a . or [ is quoted or escaped: a."b.c", a.'b.c',
            \\    a["b.c"] (as `-o gron` prints it), or a.b\.c
            \\    example: school.class[0].student[3]
            \\  .md/.markdown files: upserts the frontmatter/endmatter, creating
            \\    it (as YAML) if absent — the archetype is otherwise sniffed
            \\    from the file, not assumed from the extension.
            \\
        , .{ binary_name, binary_name, binary_name });
        try term.writer.flush();
    }

    pub fn insert(term: *Io.Terminal, binary_name: []const u8) !void {
        try term.writer.print(
            \\Usage: {s} insert [--string | --raw] <file> <path> <value>
            \\  Adds a new entry. The last path segment names the slot to create:
            \\    a.b.newkey   -> insert key `newkey` into the mapping at a.b
            \\    a.list[0]    -> prepend <value> as the first item of a.list
            \\    a.list[-]    -> append <value> as the last item ([$] also works)
            \\  An empty parent targets the root container, so the document's own
            \\    root (mapping vs list) decides which form applies — not the format.
            \\  There is no insert in the middle of a sequence (list[2]): `set --seq`
            \\    rewrites the whole sequence.
            \\
        ++ value_reading ++
            \\  path format: dot syntax for keys, bracket syntax for indices.
            \\    a key holding a . or [ is quoted or escaped: a."b.c", a.'b.c',
            \\    a["b.c"] (as `-o gron` prints it), or a.b\.c
            \\  .md/.markdown files: edits the frontmatter/endmatter in place —
            \\    its archetype is sniffed from the file (YAML by default).
            \\
        , .{binary_name});
        try term.writer.flush();
    }

    pub fn delete(term: *Io.Terminal, binary_name: []const u8) !void {
        try term.writer.print(
            \\Usage: {s} delete <file> <path>
            \\  Removes the entry the path points at. The last path segment decides:
            \\    a.b.key      -> delete that mapping entry (with its own comments)
            \\    a.list[2]    -> remove item 2 from the sequence a.list
            \\    a.list[-]    -> remove the last item of the sequence a.list
            \\  A path naming a whole [table]/[section]/block container — or one
            \\  [[array-of-tables]] element — removes all of it, header, entries
            \\  and every place it is reopened further down the file.
            \\  path format: dot syntax for keys, bracket syntax for indices
            \\    a key holding a . or [ is quoted or escaped: a."b.c", a.'b.c',
            \\    a["b.c"] (as `-o gron` prints it), or a.b\.c
            \\    example: school.class[0].student[3]
            \\    [-] or [$] in place of an index means "the last item"
            \\  .md/.markdown files: edits the frontmatter/endmatter in place —
            \\    its archetype is sniffed from the file (YAML by default).
            \\
        , .{binary_name});
        try term.writer.flush();
    }

    pub fn comment(term: *Io.Terminal, binary_name: []const u8) !void {
        try term.writer.print(
            \\Usage: {s} comment [--inline] [--delete | --get] <file> <path> [<text>]
            \\  default: add an own-line comment ABOVE the node at <path>
            \\  --inline: target the same-line trailing comment on the value at
            \\    <path> instead (set replaces any existing one on that line)
            \\  --delete: remove the targeted comment instead of adding it; <text>
            \\    is then omitted (a no-op when there is no such comment)
            \\  --get: print the targeted comment to stdout (markers stripped) and
            \\    make no change; <text> is then omitted (exits 1, printing
            \\    nothing, when there is no such comment)
            \\  the comment marker is added for you: # for YAML/TOML, // for
            \\    JSONC/JSON5/ZON. Strict JSON has no comments (rejected).
            \\  <text> may span multiple lines (leading only): one comment line each.
            \\  path format: dot syntax for keys, bracket syntax for indices
            \\    a key holding a . or [ is quoted or escaped: a."b.c", a.'b.c',
            \\    a["b.c"] (as `-o gron` prints it), or a.b\.c
            \\    example: school.class[0].student[3]
            \\  .md/.markdown files: comments the frontmatter/endmatter in
            \\    place — its archetype is sniffed from the file (YAML by
            \\    default).
            \\
        , .{binary_name});
        try term.writer.flush();
    }

    pub fn get(term: *Io.Terminal, binary_name: []const u8) !void {
        try term.writer.print(
            \\Usage: {s} get [--input <format>] [--output <format>] <file> [path]
            \\  -i, --input: input format of file (defaults to the file extension,
            \\    then to sniffing the file's contents if the extension is unknown)
            \\  -o, --output:   output format (defaults to the input format)
            \\  [path]: a scalar there prints as its text and a newline — a string
            \\    unquoted, anything else as fig spells it (42, true, null) — in
            \\    every format, so $(fig get f name) is the value. A container
            \\    prints as a document. With -o, the fragment prints in that
            \\    format's own spelling (-o json: "hi").
            \\  <format> is one of these, or a language configured in
            \\    languages.figl (`{s} lang list`):
            \\
        ++ formatList("    ", &.{}) ++
            \\  canonical: the AST's 1:1 oracle text encoding; usable as input or
            \\    output, e.g. to inspect how any document parses. (Owns no file
            \\    extension — select it explicitly.) Compiled in only with
            \\    `-Dcanonical=true` (opt-in, off by default — it is a
            \\    test/debugging oracle, not exposed through the C ABI or bindings).
            \\  fig: the human-facing authoring dialect (`.figl`); lossy at the
            \\    edges (non-string keys, YAML refs) —
            \\    use `canonical`/`--lossless` for those. `-o fig` prints in
            \\    house style; use `fig fmt` to
            \\    rewrite a file in place instead of printing to stdout.
            \\  ini: `[section]` headers + `key = value` lines, `;`/`#` full-line
            \\    comments; every value is plain text (no typed scalars). Holds a
            \\    root mapping and one level of section nesting only — a value
            \\    nested any deeper, or an array anywhere, has no INI spelling.
            \\    Editable in place, except that a `[section]` is a name rather
            \\    than a value — `replace`/`delete` at a section's own path refuse,
            \\    since a section owns no contiguous text but its header and may
            \\    be reopened further down the file. Edit the keys inside it.
            \\  dotenv (.env): flat `KEY=value` only, no sections/nesting; keys
            \\    are bash identifiers, an optional `export ` prefix is accepted
            \\    and discarded, and `"`/`'` quoting is real (escapes, multi-line
            \\    values) — no `$VAR` interpolation is performed.
            \\  properties (Java .properties): flat `key=value` only (also
            \\    accepts `key: value`/`key value`); backslash escapes on both
            \\    key and value (`\t \n \r \f \\ \uXXXX`, plus `\` at end-of-line
            \\    as a line continuation); `#`/`!` full-line comments.
            \\  nestedtext (.nt, nestedtext.org): indentation-nested `key: value`/
            \\    `- item`/`> multiline string` lines, arbitrary nesting depth;
            \\    every value is plain text (no typed scalars, like ini). `#`
            \\    full-line comments. Detected from content only as a last
            \\    resort (after every other format, including yaml, since plain
            \\    `key: value`/`- item` text is valid in both) — select it with
            \\    `-i nestedtext` or a `.nt` extension.
            \\  gron: a line-oriented `path = value;` projection (greppable, and
            \\    reversible with `-i gron`); must be selected explicitly, never
            \\    sniffed. Fidelity matches JSON (drops comments/anchors).
            \\  --gron-root NAME: root identifier for `-o gron` (default "json").
            \\  --gron-sep STR: key/value separator for `-o gron` (default " = ").
            \\    Print-only: ungron always splits on " = ", so a custom separator
            \\    is one-way unless it matches the default.
            \\  --gron-term STR: per-line terminator for `-o gron` (default ";");
            \\    pass "" to drop it. ungron strips an optional ";" regardless.
            \\  --compact: single-line output with minimal whitespace (JSON, JSON5, ZON).
            \\  --pretty: multi-line, indented output (the default).
            \\  --indent N: spaces per indent level for pretty JSON, and for TOML's
            \\    wrapped arrays (default 2).
            \\  --width N: TOML column budget (default 80); a mapping/array that fits
            \\    stays inline, a wider one expands to a [section] / wrapped array.
            \\  --strip-comments: drop comments instead of carrying them across formats.
            \\  --lossless: preserve values the target can't represent natively
            \\    (e.g. a null in TOML, a TOML datetime in JSON) via a $fig
            \\    envelope, and reconstruct any such envelope in the input.
            \\    --lossy (the default) emits clean, idiomatic output instead.
            \\  --lax-tags: drop unknown/custom YAML tags instead of erroring, when
            \\    converting away from YAML.
            \\  -q, --quiet, --no-warnings: suppress warnings on stderr — lossy
            \\    conversions, and fig authoring lints (`Yes`-style strings, a
            \\    likely missing comma in a flow value, indent/marker-count
            \\    disagreement, ...).
            \\  --strict: treat any warning as an error (exit 1).
            \\  --embed <archetype>: read an embedded region of a host file.
            \\    Without this flag, a `.md`/`.markdown` file has its archetype
            \\    sniffed from the content (falling back to `frontmatter`/YAML
            \\    when none is found).
            \\
        ++ embed_archetypes ++
            \\  --body: print the host prose OUTSIDE the fences (the body span) instead
            \\    of the embed content; the whole file when there is no such region.
            \\  path format: dot syntax for keys, bracket syntax for indices
            \\    a key holding a . or [ is quoted or escaped: a."b.c", a.'b.c',
            \\    a["b.c"] (as `-o gron` prints it), or a.b\.c
            \\    example: school.class[0].student[3]
            \\  .md/.markdown files: reads the frontmatter/endmatter, whichever
            \\    archetype it turns out to be
            \\
        , .{ binary_name, binary_name });
        try term.writer.flush();
    }

    pub fn check(term: *Io.Terminal, binary_name: []const u8) !void {
        try term.writer.print(
            \\Usage: {s} check [--input <format>] [-q|--quiet] [--strict] <file>...
            \\  Validate that each file parses cleanly as its format. Prints an
            \\  `ok` line per file and exits 0 when all parse; prints an error
            \\  line to stderr for each failing file and exits 1 if any fail.
            \\  -i, --input: parse every file as this format — one of these, or a
            \\    language configured in languages.figl (`{s} lang list`):
            \\
        ++ formatList("    ", &.{}) ++
            \\    Default: infer from each file's extension, then by sniffing
            \\    its contents.
            \\  -s, --spec: validate against a specific language version, where one
            \\    is selectable: TOML `1.0`/`1.1` (default 1.1), YAML `1.2.2`/`1.1`
            \\    (default 1.2.2).
            \\    JSON strictness is the format itself (json vs jsonc vs json5).
            \\  -q, --quiet, --no-warnings: suppress the per-file `ok` lines and
            \\    fig authoring warnings; errors still print.
            \\  --strict: a file that parses with warnings fails too (exit 1).
            \\  reads stdin when <file> is `-`.
            \\  .md/.markdown files: validates the frontmatter/endmatter,
            \\    whichever archetype it turns out to be (YAML by default).
            \\
        , .{ binary_name, binary_name });
        try term.writer.flush();
    }

    pub fn fmt(term: *Io.Terminal, binary_name: []const u8) !void {
        try term.writer.print(
            \\Usage: {s} fmt [--input <format>] [--dry-run | --diff] <file>
            \\  Reformat a file in place: parse then re-emit in the format's house
            \\  style — `.figl`'s printer applies the style DESIGN.md describes
            \\  (spaced marker runs, `[]`/`+` list sigils, ...); other formats get
            \\  their own printer's canonical layout. Unlike `get`, the output
            \\  format always matches the input — reformatting never converts.
            \\  A file already in house style is left byte-identical (no-op write).
            \\  --dry-run: print the reformatted result to stdout instead of
            \\    writing it back; exit 1 if reformatting would change the file,
            \\    0 if it's already clean — a check gate for pre-commit/CI.
            \\  --diff: like --dry-run, but print a unified diff of the change to
            \\    stdout instead of the whole reformatted file; nothing is printed
            \\    (and exit is 0) when the file is already clean.
            \\  -i, --input: input format (defaults to the file extension, then
            \\    to sniffing the file's contents if the extension is unknown) —
            \\    one of these, or a language configured in languages.figl:
            \\
        ++ formatList("    ", &.{"gron"}) ++
            \\  --compact: single-line output with minimal whitespace (JSON, JSON5, ZON).
            \\  --pretty: multi-line, indented output (the default).
            \\  --indent N: spaces per indent level for pretty JSON, and for TOML's
            \\    wrapped arrays (default 2).
            \\  --width N: TOML column budget (default 80); a mapping/array that fits
            \\    stays inline, a wider one expands to a [section] / wrapped array.
            \\  --strip-comments: drop comments instead of re-emitting them.
            \\  -q, --quiet, --no-warnings: suppress warnings on stderr.
            \\  --strict: treat any warning as an error (exit 1, no write).
            \\  --embed <archetype>: reformat an embedded region of a host file
            \\    instead of the whole file. Without this flag, a `.md`/`.markdown`
            \\    file has its archetype sniffed from the content (falling back to
            \\    `frontmatter`/YAML when none is found).
            \\
        ++ embed_archetypes ++
            \\  reads stdin when <file> is `-`, but only with --dry-run/--diff:
            \\    there is nowhere to write an in-place result back to.
            \\  .md/.markdown files: reformats the frontmatter/endmatter in
            \\    place, whichever archetype it turns out to be.
            \\
        , .{binary_name});
        try term.writer.flush();
    }

    pub fn convert(term: *Io.Terminal, binary_name: []const u8) !void {
        try term.writer.print(
            \\Usage: {s} convert --output <format> [--input <format>] [--write | --diff] <file>
            \\       {s} convert --to-embed <archetype> [--embed <archetype>] [--write | --diff] <file>
            \\  Convert a file — `fmt`'s twin for when the target format differs
            \\  from the source. Exactly one of --output/--to-embed picks the
            \\  target; the other flag group is unused (rejected together).
            \\  Like `get`, it prints the converted result to stdout by default;
            \\  pass --write to write it back to <file> in place instead.
            \\
            \\  Whole-file mode (--output): parse the whole file as --input (else the
            \\    extension, else sniffed from its contents) and re-emit it as
            \\    --output, in the target format's house style. A host document
            \\    whose extension implies an embedded region (`.md`/`.markdown`) is
            \\    rejected here — use embed-archetype mode, or pass --input to force
            \\    whole-file conversion anyway.
            \\  -i, --input, -o, --output: one of these, or a language configured
            \\    in languages.figl:
            \\
        ++ formatList("    ", &.{"gron"}) ++
            \\
            \\  Embed-archetype mode (--to-embed): rehouse a host document's
            \\    embedded region from one archetype's fence-and-content convention
            \\    to another's — e.g. turn YAML frontmatter into JSON frontmatter —
            \\    splicing the new fences and re-serialized content in place while
            \\    leaving the surrounding prose byte-identical. The source archetype
            \\    is --embed, else sniffed from the file's own fences (falling back
            \\    to frontmatter/YAML when none is found).
            \\  --embed, --to-embed <archetype>:
            \\
        ++ embed_archetypes ++
            \\
            \\  -w, --write: write the converted result back to <file> in place
            \\    (skipped if it's already byte-identical) instead of printing it.
            \\  --diff: print a unified diff of the change instead of the whole
            \\    converted file. Combine with --write to write AND see what changed.
            \\  --compact / --pretty: single-line vs multi-line output (default pretty).
            \\  --indent N / --width N: as in `get`/`fmt`.
            \\  --strip-comments: drop comments instead of carrying them across formats.
            \\  --lossless / --lossy: preserve values the target can't represent
            \\    natively via a $fig envelope (default --lossy).
            \\  --lax-tags: drop unknown/custom YAML tags instead of erroring, when
            \\    converting away from YAML.
            \\  -q, --quiet, --no-warnings: suppress warnings on stderr.
            \\  --strict: treat any warning as an error (exit 1, no write).
            \\  reads stdin when <file> is `-`, but only without --write.
            \\
        , .{ binary_name, binary_name });
        try term.writer.flush();
    }

    pub fn patch(term: *Io.Terminal, binary_name: []const u8) !void {
        try term.writer.print(
            \\Usage: {s} patch [flags] <file> <patch-file>
            \\  Merge <patch-file> into <file>, in place. Every byte of <file>
            \\  outside the paths the patch actually names is left untouched —
            \\  comments, key order, quoting style and all. The two files need not
            \\  share a format: a TOML patch merges into a YAML target, rendered in
            \\  the target's syntax on the way in.
            \\
            \\  The merge rule, per path:
            \\    absent in <file>          -> created, with the patch's own comments
            \\    mapping on both sides     -> merged key by key, recursively
            \\    sequence on both sides    -> --seq decides
            \\    anything else             -> the patch's value wins
            \\  A value the two files already agree on is not rewritten at all, so
            \\  re-running a patch is a no-op and the diff is only what changed.
            \\  A container the patch DOES change is re-rendered by the target
            \\  format's printer, so it comes back in that format's house style
            \\  (in YAML, a block collection) rather than the patch's spelling.
            \\
            \\  <file> must already exist — `patch` merges into a document, it does
            \\  not create one; use `set` for that. `-` reads stdin for either file
            \\  (not both), and stdin as <file> needs --dry-run or --diff.
            \\
            \\  --at <path>: merge into that path in <file> instead of its root.
            \\  --from <path>: take only that subtree of <patch-file>.
            \\    Together they move one part of one file into another part of
            \\    another: `fig patch app.yaml defaults.toml --from db --at service.db`
            \\  --delete <path>: remove a path from <file> after the merge; repeatable.
            \\    A path that isn't there is not an error.
            \\  --seq replace|append|union: what to do when both files hold a
            \\    sequence at the same path. `replace` (default) takes the patch's;
            \\    `append` adds every item; `union` adds only the items <file> does
            \\    not already hold, compared by value rather than by source text.
            \\  --comments ours|theirs|none: whose comment wins where both files
            \\    carry one in the same position. `ours` (default) keeps <file>'s and
            \\    contributes the patch's only where <file> has none; `theirs` lets
            \\    the patch overwrite; `none` carries no comment from the patch at
            \\    all. Comments nested inside a subtree the patch contributes whole
            \\    always ride along with it unless `none`.
            \\  --strip-comments: the same as `--comments none`, in `get`'s word.
            \\  -i, --input <format>: the format of <file> (else its extension, else
            \\    sniffed). --patch-input does the same for <patch-file>.
            \\  --embed <archetype>: patch the embedded region of a host <file>
            \\    (e.g. a markdown post's frontmatter), splicing the result back
            \\    between its fences and leaving the prose byte-identical.
            \\    --patch-embed does the same for <patch-file>. A `.md`/`.markdown`
            \\    extension implies the sniff on either side without the flag.
            \\  --lossless / --lossy: preserve values the target format can't hold
            \\    natively via a $fig envelope (default --lossy).
            \\  --indent N / --width N / --compact / --pretty: style knobs for the
            \\    values the patch contributes, as in `get`/`convert`.
            \\  --dry-run: print the patched document to stdout; write nothing.
            \\  --diff: print a unified diff of the change; write nothing. Neither
            \\    preview mode sets a non-zero exit status — a patch is expected to
            \\    change the file.
            \\  -q, --quiet, --no-warnings: suppress the patch file's warnings and
            \\    the dropped-comment note.
            \\  --strict: treat any warning as an error (exit 1, no write).
            \\
            \\  Refused rather than guessed at: merging into a YAML `*alias` (the
            \\  reference belongs to whatever defined the anchor), and replacing the
            \\  whole document root with a non-mapping (that is a copy, not a patch).
            \\
        , .{binary_name});
        try term.writer.flush();
    }
    pub fn lang(term: *Io.Terminal, binary_name: []const u8) !void {
        try term.writer.print(
            \\Usage: {s} lang list
            \\       {s} lang check <name> [--against <format>] [files...]
            \\       {s} lang table <file> [-i <format>] [--spec <version>]
            \\  Languages fig did not compile in. One is a helper program that
            \\  speaks the wire in fig's Rust crate (`fig::helper`), configured in
            \\  a `languages.figl`:
            \\
            \\    language[]
            \\    > name = lua-dotenv
            \\    > extensions = [env]
            \\    > command = [fig-lua, ~/.config/fig/languages/dotenv.lua]
            \\
            \\  found, earlier file winning a name, at $FIG_LANGUAGES (a file path),
            \\  .fig/languages.figl in the working directory or any ancestor, and
            \\  $XDG_CONFIG_HOME/fig/languages.figl (~/.config/fig/languages.figl).
            \\  `name` is what --input, --output and --lang accept; `extensions`
            \\  resolves a file to it, but a compiled format's extension always
            \\  wins, so a twin of a compiled language is reached with --lang.
            \\  A language named after a format this build left out (`list` says
            \\  "compiled out") stands in for it: the format's name, extension and
            \\  embedded spellings (markdown frontmatter, fenced blocks) reach it.
            \\
            \\  Nothing is spawned until a name or extension the CLI cannot resolve
            \\  itself is asked for; the helper is then started once and asked to
            \\  parse its own samples, print and reparse them, and take a no-op
            \\  edit, and is refused with the reason if any of that fails.
            \\
            \\  list: every compiled format, and every configured language with
            \\    what it can do (read, edit, serialize), its extensions and the
            \\    file that configured it — or why it was refused.
            \\  check <name>: load the language and report what the harness found.
            \\    --against <format>: also hold it to a compiled format — parse each
            \\    of the language's samples and each file given with both, and
            \\    compare the tables row for row, the way a reimplementation of a
            \\    compiled language is proven (`{s} lang check lua-dotenv --against
            \\    dotenv secrets.env`). Exits 1 on the first difference.
            \\  table <file>: parse the file (as -i <format>, else by its extension,
            \\    else by its contents) and print its node table as the JSON a
            \\    helper would answer `parse` with — what a twin has to produce,
            \\    row for row, to pass `check --against`. --spec selects a version
            \\    of the format where one is selectable, as `check`'s does.
            \\
            \\  --lang <name>, on any action, names the language a file is read and
            \\  written in, whatever its extension: `{s} get secrets.env --lang lua-dotenv`.
            \\
        , .{ binary_name, binary_name, binary_name, binary_name, binary_name });
        try term.writer.flush();
    }
};

test "every format the help lists is one the argument parser accepts" {
    const args = @import("args.zig");
    inline for (comptime .{ formatList("", &.{}), formatList("", &.{"gron"}) }) |list| {
        var it = std.mem.tokenizeAny(u8, list, ", \n");
        while (it.next()) |name| try std.testing.expect(args.parseFormatName(name) != null);
    }
}

test "every --embed archetype the help names is one the argument parser accepts" {
    const args = @import("args.zig");
    var langs = std.mem.tokenizeAny(u8, embed_langs, ", ");
    while (langs.next()) |l| {
        var buf: [64]u8 = undefined;
        for ([_][]const u8{ "md-", "fenced-", "html-script-", "html-code-" }) |family| {
            const name = try std.fmt.bufPrint(&buf, "{s}{s}", .{ family, l });
            // `md-yaml` is deliberately not a spelling, and the help says so.
            if (std.mem.eql(u8, name, "md-yaml")) {
                try std.testing.expect(args.embedTypeFromName(name) == null);
                continue;
            }
            try std.testing.expect(args.embedTypeFromName(name) != null);
        }
    }
    for ([_][]const u8{
        "frontmatter", "semicolons", "plus",             "endmatter",
        "html-script", "html-code",  "frontmatter-yaml", "endmatter-yaml",
    }) |name| try std.testing.expect(args.embedTypeFromName(name) != null);
}