1mod glyph;
2mod traits;
3
4pub use glyph::{Color, Glyph, Presentation};
5pub use traits::{Category, Scope, Tag};
6
7#[cfg(feature = "render")]
8pub mod render;
9
10use std::collections::HashMap;
11use std::io::{self, BufRead, Write};
12
13#[derive(Clone, Copy, Debug)]
14pub struct Example {
15 pub command: &'static str,
16 pub description: &'static str,
17}
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub enum Confirmation {
26 TypeToken {
31 message: &'static str,
32 token: &'static str,
33 },
34 YesNo { message: &'static str },
37}
38
39impl Confirmation {
40 pub fn prompt<R: BufRead, W: Write>(&self, reader: &mut R, writer: &mut W) -> io::Result<bool> {
45 match self {
46 Confirmation::TypeToken { message, token } => {
47 writeln!(writer, "{message}")?;
48 write!(writer, "Type {token} to continue: ")?;
49 writer.flush()?;
50 let mut line = String::new();
51 reader.read_line(&mut line)?;
52 Ok(line.trim() == *token)
53 }
54 Confirmation::YesNo { message } => {
55 write!(writer, "{message} [y/N] ")?;
56 writer.flush()?;
57 let mut line = String::new();
58 reader.read_line(&mut line)?;
59 let answer = line.trim().to_ascii_lowercase();
60 Ok(answer == "y" || answer == "yes")
61 }
62 }
63 }
64
65 pub fn prompt_stdio(&self) -> io::Result<bool> {
67 let mut stdin = io::stdin().lock();
68 let mut stderr = io::stderr();
69 self.prompt(&mut stdin, &mut stderr)
70 }
71}
72
73#[derive(Clone, Copy, Debug)]
79pub struct ApiEndpoint {
80 pub method: &'static str,
81 pub path: &'static str,
82}
83
84#[derive(Clone, Copy, Debug)]
85pub struct CommandDef<C: Category, T: Tag, S: Scope> {
86 pub name: &'static str,
87 pub summary: &'static str,
88 pub category: C,
89 pub tags: &'static [T],
90 pub scope: S,
91 pub examples: &'static [Example],
92 pub see_also: &'static [&'static str],
93 pub long_description: &'static str,
96 pub api: &'static [ApiEndpoint],
98 pub confirmation: Option<Confirmation>,
104}
105
106impl<C: Category, T: Tag, S: Scope> CommandDef<C, T, S> {
107 pub fn requires_confirmation(&self) -> bool {
110 self.confirmation.is_some()
111 }
112
113 pub fn gate(&self) -> io::Result<bool> {
118 match &self.confirmation {
119 None => Ok(true),
120 Some(c) => c.prompt_stdio(),
121 }
122 }
123}
124
125#[derive(Default)]
126pub struct CommandManifest<C: Category, T: Tag, S: Scope> {
127 commands: HashMap<&'static str, CommandDef<C, T, S>>,
128}
129
130impl<C: Category, T: Tag, S: Scope> CommandManifest<C, T, S> {
131 pub fn new() -> Self {
132 Self {
133 commands: HashMap::new(),
134 }
135 }
136
137 pub fn add(&mut self, def: CommandDef<C, T, S>) -> &mut Self {
138 let previous = self.commands.insert(def.name, def);
139 debug_assert!(previous.is_none(), "duplicate command name: {}", def.name);
140 self
141 }
142
143 pub fn get(&self, name: &str) -> Option<&CommandDef<C, T, S>> {
144 self.commands.get(name)
145 }
146
147 pub fn all_sorted(&self) -> Vec<&CommandDef<C, T, S>> {
148 let mut items: Vec<_> = self.commands.values().collect();
149 items.sort_by_key(|def| (def.category.order(), def.name));
150 items
151 }
152
153 pub fn by_category(&self, cat: C) -> Vec<&CommandDef<C, T, S>> {
154 let mut items: Vec<_> = self
155 .commands
156 .values()
157 .filter(|def| def.category == cat)
158 .collect();
159 items.sort_by_key(|def| def.name);
160 items
161 }
162
163 pub fn by_tag(&self, tag: T) -> Vec<&CommandDef<C, T, S>> {
164 let mut items: Vec<_> = self
165 .commands
166 .values()
167 .filter(|def| def.tags.contains(&tag))
168 .collect();
169 items.sort_by_key(|def| def.name);
170 items
171 }
172
173 pub fn by_scope(&self, scope: S) -> Vec<&CommandDef<C, T, S>> {
174 let mut items: Vec<_> = self
175 .commands
176 .values()
177 .filter(|def| def.scope == scope)
178 .collect();
179 items.sort_by_key(|def| def.name);
180 items
181 }
182
183 pub fn categories_in_order(&self) -> Vec<C> {
184 let mut categories = Vec::new();
185 for def in self.commands.values() {
186 if !categories.contains(&def.category) {
187 categories.push(def.category);
188 }
189 }
190 categories.sort_by_key(|cat| cat.order());
191 categories
192 }
193}