bevy_commodore/
builtins.rs

1//! Built-in argument types and commands.
2//!
3//! The [`args`] module provides an implementation of the [`Argument`](crate::Argument) trait for a few, commonly used argument types, such as [`f32`], [`String`], [`Identifier`](args::Identifier), and [`i32`]. \
4//! The [`cmd`] module provides implementations of command handlers for a few built-in commands.
5
6/// Built-in argument types
7pub mod args {
8    use bevy::prelude::Deref;
9    use internment::Intern;
10
11    use crate::Argument;
12
13    /// An identifier. This is an unquoted string that matches the regex:
14    /// ```regex
15    /// ^[a-zA-Z_][a-zA-Z0-9_]*
16    /// ```
17    #[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            // Empty input?
68            if s.is_empty() {
69                return Err(crate::ParseError::new(crate::ParseErrorKind::Empty));
70            }
71
72            // Quoted string case
73            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                        // Handle escaped characters
81                        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                            // End of quoted string
96                            let rest = &rest[i + 1..];
97                            return Ok((rest.trim_start(), result));
98                        }
99                        _ => result.push(c),
100                    }
101                }
102
103                // If we reach here, the closing quote was missing
104                Err(
105                    crate::ParseError::new(crate::ParseErrorKind::InvalidSequence)
106                        .with_cause("missing closing quote".to_owned()),
107                )
108            } else {
109                // Unquoted string: read until next whitespace
110                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            // Find where the integer token ends
136            let mut end = 0;
137            let mut chars = s.char_indices();
138
139            // Optional leading sign
140            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            // Consume all following digits
151            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
179/// Implementations for default commands.
180pub 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    /// The command handler for the built-in `help` function.
190    #[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    /// Write info about a command to `ctx`. This is used for the `help`-command handler.
220    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}