bevy_commodore/
builtins.rs1pub mod args {
8 use bevy::prelude::Deref;
9 use internment::Intern;
10
11 use crate::Argument;
12
13 #[repr(transparent)]
18 #[derive(Clone, Copy, PartialEq, Eq, Hash, Deref)]
19 pub struct Identifier(pub Intern<str>);
20
21 impl Argument for Identifier {
22 fn parse(s: &str) -> Result<(&str, Self), crate::ParseError>
23 where
24 Self: Sized,
25 {
26 let mut chars = s.char_indices();
27
28 let first = match chars.next() {
29 Some((_, c)) if c.is_ascii_alphabetic() || c == '_' => c,
30 _ => {
31 return Err(crate::ParseError::new(
32 crate::ParseErrorKind::InvalidSequence,
33 ));
34 }
35 };
36
37 let mut end = first.len_utf8();
38 for (i, c) in chars {
39 if c.is_ascii_alphanumeric() || c == '_' {
40 end = i + c.len_utf8();
41 } else {
42 break;
43 }
44 }
45
46 let ident = &s[..end];
47 let rest = &s[end..];
48
49 Ok((rest, Identifier(ident.into())))
50 }
51
52 fn type_name() -> &'static str
53 where
54 Self: Sized,
55 {
56 "identifier"
57 }
58 }
59
60 impl Argument for String {
61 fn parse(s: &str) -> Result<(&str, Self), crate::ParseError>
62 where
63 Self: Sized,
64 {
65 let s = s.trim_start();
66
67 if s.is_empty() {
69 return Err(crate::ParseError::new(crate::ParseErrorKind::Empty));
70 }
71
72 if let Some(rest) = s.strip_prefix('"') {
74 let mut result = String::new();
75 let mut chars = rest.char_indices();
76 let mut escape = false;
77
78 for (i, c) in &mut chars {
79 if escape {
80 match c {
82 '"' => result.push('"'),
83 '\\' => result.push('\\'),
84 'n' => result.push('\n'),
85 't' => result.push('\t'),
86 other => result.push(other),
87 }
88 escape = false;
89 continue;
90 }
91
92 match c {
93 '\\' => escape = true,
94 '"' => {
95 let rest = &rest[i + 1..];
97 return Ok((rest.trim_start(), result));
98 }
99 _ => result.push(c),
100 }
101 }
102
103 Err(
105 crate::ParseError::new(crate::ParseErrorKind::InvalidSequence)
106 .with_cause("missing closing quote".to_owned()),
107 )
108 } else {
109 let end = s.find(char::is_whitespace).unwrap_or(s.len());
111 let word = &s[..end];
112 let rest = &s[end..];
113 Ok((rest.trim_start(), word.to_string()))
114 }
115 }
116
117 fn type_name() -> &'static str
118 where
119 Self: Sized,
120 {
121 "string"
122 }
123 }
124
125 impl Argument for i32 {
126 fn parse(s: &str) -> Result<(&str, Self), crate::ParseError>
127 where
128 Self: Sized,
129 {
130 let s = s.trim_start();
131 if s.is_empty() {
132 return Err(crate::ParseError::new(crate::ParseErrorKind::Empty));
133 }
134
135 let mut end = 0;
137 let mut chars = s.char_indices();
138
139 if let Some((_, c)) = chars.next() {
141 if c == '-' || c == '+' || c.is_ascii_digit() {
142 end = 1;
143 } else {
144 return Err(crate::ParseError::new(
145 crate::ParseErrorKind::InvalidSequence,
146 ));
147 }
148 }
149
150 for (i, c) in chars {
152 if c.is_ascii_digit() {
153 end = i + c.len_utf8();
154 } else {
155 break;
156 }
157 }
158
159 let number_str = &s[..end];
160 let rest = &s[end..];
161
162 match number_str.parse::<i32>() {
163 Ok(num) => Ok((rest.trim_start(), num)),
164 Err(_) => Err(crate::ParseError::new(
165 crate::ParseErrorKind::InvalidSequence,
166 )),
167 }
168 }
169
170 fn type_name() -> &'static str
171 where
172 Self: Sized,
173 {
174 "integer"
175 }
176 }
177}
178
179pub mod cmd {
181 use bevy::ecs::system::{In, Res};
182 use itertools::Itertools;
183
184 use crate::{
185 ArgumentRegistry, CommandContext, CommandRegistry, CommandResult, RegisteredCommand,
186 RegisteredCommandNode, builtins::args::Identifier,
187 };
188
189 #[expect(clippy::needless_pass_by_value)]
191 #[expect(clippy::must_use_candidate)]
192 pub fn help_cmd_handler<I, O>(
193 In(mut ctx): In<CommandContext>,
194 commands: Res<CommandRegistry<I, O>>,
195 arg_reg: Res<ArgumentRegistry<I, O>>,
196 ) -> CommandResult
197 where
198 I: Send + Sync + 'static,
199 O: Send + Sync + 'static,
200 {
201 use std::fmt::Write;
202
203 if let Some(cmd) = ctx.get_argument::<Identifier>("command") {
204 if let Some(cmd_def) = commands.commands.get(&cmd) {
205 write_cmd_info(&mut ctx, cmd_def, &*arg_reg);
206 } else {
207 _ = writeln!(ctx, "Command \"{}\" does not exist", &**cmd);
208 return CommandResult::Err;
209 }
210 } else {
211 for command in &commands.commands {
212 write_cmd_info(&mut ctx, command.1, &*arg_reg);
213 }
214 }
215
216 CommandResult::Ok
217 }
218
219 fn write_cmd_info<I, O>(
221 ctx: &mut CommandContext,
222 command: &RegisteredCommand,
223 arg_reg: &ArgumentRegistry<I, O>,
224 ) {
225 use std::fmt::Write;
226
227 _ = write!(ctx, "{}", command.name);
228
229 for node in &command.nodes {
230 match node {
231 RegisteredCommandNode::Argument(arg) => {
232 _ = write!(
233 ctx,
234 " <{}: {}>",
235 arg.name,
236 arg_reg.types.get(&arg.type_id).unwrap().type_name
237 );
238 }
239 RegisteredCommandNode::SubcommandGroup(scg) => {
240 _ = write!(ctx, " [{}]", scg.iter().map(|v| v.name).join("|"));
241 }
242 }
243 }
244
245 if let Some(desc) = command.description {
246 _ = writeln!(ctx, ": {desc}");
247 }
248
249 if command
250 .nodes
251 .iter()
252 .any(|v| matches!(v, RegisteredCommandNode::Argument(_)))
253 {
254 _ = writeln!(ctx, "Arguments: ");
255 }
256
257 for args in command.nodes.iter().filter_map(|v| {
258 if let RegisteredCommandNode::Argument(arg) = v {
259 Some(arg)
260 } else {
261 None
262 }
263 }) {
264 _ = writeln!(
265 ctx,
266 "\t- {}: {}",
267 args.name,
268 args.description.map_or("", |v| v.as_ref())
269 );
270 }
271 _ = writeln!(ctx);
272 }
273}