kaish_tool_api/global_flags.rs
1//! Global flags shared by every builtin via `#[command(flatten)]`.
2//!
3//! Today this is just `--json`. Every builtin flattens `GlobalFlags` into its
4//! own clap struct and calls `parsed.global.apply(ctx)` after parsing; the
5//! kernel reads the output format the flag set (via
6//! [`ToolCtx::set_output_format`](crate::ToolCtx::set_output_format)) after
7//! `execute()` returns and applies it.
8
9use clap::Args;
10
11use kaish_types::{OutputFormat, Value};
12
13use crate::ctx::ToolCtx;
14use kaish_types::ToolArgs;
15
16/// Flags injected into every migrated builtin via `#[command(flatten)] global: GlobalFlags`.
17///
18/// Builtins call `parsed.global.apply(ctx)` after their own argv parse so the
19/// dispatcher can read the output format post-execute and apply it.
20#[derive(Args, Debug, Clone, Default)]
21pub struct GlobalFlags {
22 /// Render structured output as JSON.
23 #[arg(long)]
24 pub json: bool,
25}
26
27impl GlobalFlags {
28 /// Apply the flags to `ctx` so the dispatcher can pick them up after the
29 /// builtin's `execute()` returns.
30 pub fn apply(&self, ctx: &mut dyn ToolCtx) {
31 if self.json {
32 ctx.set_output_format(OutputFormat::Json);
33 }
34 }
35
36 /// Honor `--json` straight off `ToolArgs` before any per-builtin clap parse.
37 ///
38 /// The kernel calls this just before `tool.execute()` so the format is set
39 /// even when a builtin's own `try_parse_from` rejects argv and returns
40 /// before `parsed.global.apply(ctx)` would have run. Idempotent with the
41 /// per-builtin apply: both writing `OutputFormat::Json` yields the same
42 /// state.
43 pub fn apply_from_args(args: &ToolArgs, ctx: &mut dyn ToolCtx) {
44 if args.has_flag("json") || positional_json_flag(args) {
45 ctx.set_output_format(OutputFormat::Json);
46 }
47 }
48}
49
50/// `--json`/`--json=VALUE` surviving as a literal string in `args.positional`
51/// is the `raw_argv` case (GH #198): a `raw_argv` tool's binder deliberately
52/// does not lift ANY flag out of source order (that's the whole point — see
53/// `ToolSchema::raw_argv`), so `--json` lands in `positional` instead of
54/// `flags`. Without this, a raw_argv builtin (`test`, and now `kill`) would
55/// silently ignore `--json` — a real, user-visible regression discovered
56/// while adding `kill`'s signal shorthand.
57///
58/// Stops scanning at a literal `"--"` token: a real end-of-options marker
59/// makes every following token an operand, not a flag — for `raw_argv` tools
60/// that's `kill -- --json foo` (foo is a job/PID literally spelled
61/// `--json`); for a NORMAL (non-raw_argv) tool it's the pre-existing case of
62/// a post-`--` `--json` operand (`echo -- --json`), which the ordinary
63/// binder already relegates to `positional` too (`past_double_dash` in
64/// `bind_tool_args`). Without this boundary, the scan would reinterpret that
65/// literal operand as the global JSON flag for every builtin, not just
66/// raw_argv ones — a real regression this comment exists to prevent
67/// reintroducing.
68///
69/// `--json=VALUE`'s truthiness mirrors `ToolArgs::has_flag`'s String-value
70/// rule exactly (truthy unless empty, `"false"`, or `"0"`), so the two paths
71/// agree on what counts as "on".
72fn positional_json_flag(args: &ToolArgs) -> bool {
73 args.positional
74 .iter()
75 .take_while(|v| !matches!(v, Value::String(s) if s == "--"))
76 .any(|v| {
77 let Value::String(s) = v else { return false };
78 if s == "--json" {
79 return true;
80 }
81 s.strip_prefix("--json=")
82 .is_some_and(|val| !val.is_empty() && val != "false" && val != "0")
83 })
84}