Skip to main content

coding_tools/
completion.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Jonathan Shook
3
4//! Dynamic shell completion for the `ct` umbrella, via `veks-completion`.
5//!
6//! The command tree is derived from the lib-hosted clap grammar
7//! ([`crate::cli`]) so it can never drift from the actual CLI: every leaf tool
8//! becomes a `ct <name>` subcommand whose value/boolean flags come straight
9//! from the introspected grammar, and a value_enum flag's variants become its
10//! completion set the same way. On top of that, a few flags get **runtime**
11//! value providers that read the live rule store — ids, tags, and def names —
12//! which a static `clap_complete` script cannot do. The `ct` binary drives this
13//! tree through [`veks_completion::handle_complete_env`].
14
15use std::sync::Arc;
16
17use veks_completion::{CommandTree, Node, ValueProvider};
18
19use crate::{cli, rules};
20
21/// Which rule-store field a dynamic provider serves.
22#[derive(Clone, Copy)]
23enum Field {
24    Id,
25    Tag,
26    Def,
27}
28
29/// Read the nearest rule store (`.ct/rules.jsonc`, discovered upward from the
30/// cwd) and return its ids / tags / def names. Empty on any failure —
31/// completion offers nothing rather than erroring.
32fn store_values(field: Field) -> Vec<String> {
33    let Ok(cwd) = std::env::current_dir() else {
34        return Vec::new();
35    };
36    let Some(root) = rules::discover_root(&cwd) else {
37        return Vec::new();
38    };
39    let Ok(text) = std::fs::read_to_string(rules::store_path(&root)) else {
40        return Vec::new();
41    };
42    let Ok(store) = rules::parse_store(&text) else {
43        return Vec::new();
44    };
45    match field {
46        Field::Id => store.rules.iter().map(|r| r.id.clone()).collect(),
47        Field::Tag => {
48            let mut tags: Vec<String> = store
49                .rules
50                .iter()
51                .flat_map(|r| r.tags.iter().cloned())
52                .collect();
53            tags.sort();
54            tags.dedup();
55            tags
56        }
57        Field::Def => store.defs.keys().cloned().collect(),
58    }
59}
60
61/// A provider over a live store field, filtered by the partial word.
62fn store_provider(field: Field) -> ValueProvider {
63    Arc::new(move |partial: &str, _: &[&str]| {
64        store_values(field)
65            .into_iter()
66            .filter(|v| v.starts_with(partial))
67            .collect()
68    })
69}
70
71/// A provider over a fixed value set (a value_enum's variants).
72fn set_provider(values: Vec<String>) -> ValueProvider {
73    Arc::new(move |partial: &str, _: &[&str]| {
74        values
75            .iter()
76            .filter(|v| v.starts_with(partial))
77            .cloned()
78            .collect()
79    })
80}
81
82/// Attach the store-backed dynamic providers for a subcommand that selects by
83/// id/tag/def (`ct check`, `ct rules`).
84fn with_store_providers(sub: &str, node: Node) -> Node {
85    match sub {
86        "check" => node
87            .with_value_provider("--id", store_provider(Field::Id))
88            .with_value_provider("--tag", store_provider(Field::Tag)),
89        "rules" => node
90            .with_value_provider("--promote", store_provider(Field::Id))
91            .with_value_provider("--remove", store_provider(Field::Id))
92            .with_value_provider("--tag", store_provider(Field::Tag))
93            .with_value_provider("--def", store_provider(Field::Def)),
94        _ => node,
95    }
96}
97
98/// Build the `ct` completion tree from the live clap grammar: one subcommand
99/// per leaf tool (its `ct-` prefix dropped), flags split into value/boolean by
100/// the introspected kind, enum value-sets and store providers attached.
101pub fn command_tree() -> CommandTree {
102    let mut tree = CommandTree::new("ct");
103    for (name, grammar) in cli::grammars() {
104        let sub = name.strip_prefix("ct-").unwrap_or(name);
105        let value_flags: Vec<String> = grammar
106            .flags
107            .iter()
108            .filter(|f| f.kind != "boolean")
109            .map(|f| format!("--{}", f.name))
110            .collect();
111        let bool_flags: Vec<String> = grammar
112            .flags
113            .iter()
114            .filter(|f| f.kind == "boolean")
115            .map(|f| format!("--{}", f.name))
116            .collect();
117        let vf: Vec<&str> = value_flags.iter().map(String::as_str).collect();
118        let bf: Vec<&str> = bool_flags.iter().map(String::as_str).collect();
119
120        let mut node = Node::leaf_with_flags(&vf, &bf);
121        for f in grammar
122            .flags
123            .iter()
124            .filter(|f| f.kind != "boolean" && !f.values.is_empty())
125        {
126            node =
127                node.with_value_provider(&format!("--{}", f.name), set_provider(f.values.clone()));
128        }
129        node = with_store_providers(sub, node);
130        tree = tree.command(sub, node);
131    }
132    // The meta-command that prints the shell registration script; its single
133    // positional is the shell name.
134    let shells = ["bash", "zsh", "fish"]
135        .iter()
136        .map(|s| s.to_string())
137        .collect();
138    tree = tree.command(
139        "completions",
140        Node::leaf(&[]).with_positional_provider(set_provider(shells)),
141    );
142    tree
143}