Skip to main content

proof_engine/editor/
console.rs

1//! Developer console — command dispatcher, log ring-buffer, expression
2//! evaluator, auto-complete and history.
3
4use std::collections::HashMap;
5use std::time::{Duration, Instant};
6
7// ─────────────────────────────────────────────────────────────────────────────
8// LogLevel
9// ─────────────────────────────────────────────────────────────────────────────
10
11/// Severity level for console log entries.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub enum LogLevel {
14    Trace,
15    Debug,
16    Info,
17    Warn,
18    Error,
19    Fatal,
20}
21
22impl LogLevel {
23    pub fn label(self) -> &'static str {
24        match self {
25            LogLevel::Trace => "TRACE",
26            LogLevel::Debug => "DEBUG",
27            LogLevel::Info  => "INFO ",
28            LogLevel::Warn  => "WARN ",
29            LogLevel::Error => "ERROR",
30            LogLevel::Fatal => "FATAL",
31        }
32    }
33    /// ASCII colour prefix character for terminal-style rendering.
34    pub fn prefix_char(self) -> char {
35        match self {
36            LogLevel::Trace => '.',
37            LogLevel::Debug => 'd',
38            LogLevel::Info  => 'i',
39            LogLevel::Warn  => '!',
40            LogLevel::Error => 'E',
41            LogLevel::Fatal => 'X',
42        }
43    }
44}
45
46impl std::fmt::Display for LogLevel {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        write!(f, "{}", self.label())
49    }
50}
51
52// ─────────────────────────────────────────────────────────────────────────────
53// ConsoleLine
54// ─────────────────────────────────────────────────────────────────────────────
55
56/// A single log entry.
57#[derive(Debug, Clone)]
58pub struct ConsoleLine {
59    pub text:      String,
60    pub level:     LogLevel,
61    /// Wall-clock time when this entry was recorded (relative to engine start).
62    pub timestamp: Duration,
63    /// How many times this exact message was repeated (for deduplication).
64    pub count:     u32,
65    /// Source location tag (e.g. "render::pipeline").
66    pub source:    Option<String>,
67}
68
69impl ConsoleLine {
70    pub fn new(text: impl Into<String>, level: LogLevel, timestamp: Duration) -> Self {
71        Self { text: text.into(), level, timestamp, count: 1, source: None }
72    }
73
74    pub fn with_source(mut self, src: impl Into<String>) -> Self {
75        self.source = Some(src.into());
76        self
77    }
78
79    /// Render as a single text line.
80    pub fn render(&self) -> String {
81        let secs = self.timestamp.as_secs_f64();
82        let repeat = if self.count > 1 { format!(" (x{})", self.count) } else { String::new() };
83        let src = self.source.as_deref().map(|s| format!("[{}] ", s)).unwrap_or_default();
84        format!(
85            "[{:8.3}] {} {}{}{}",
86            secs,
87            self.level.label(),
88            src,
89            self.text,
90            repeat,
91        )
92    }
93}
94
95// ─────────────────────────────────────────────────────────────────────────────
96// ConsoleFilter
97// ─────────────────────────────────────────────────────────────────────────────
98
99/// Controls which log entries are visible.
100#[derive(Debug, Clone)]
101pub struct ConsoleFilter {
102    pub min_level:    LogLevel,
103    pub search:       String,
104    pub show_trace:   bool,
105    pub show_debug:   bool,
106    pub show_info:    bool,
107    pub show_warn:    bool,
108    pub show_error:   bool,
109    pub show_fatal:   bool,
110}
111
112impl Default for ConsoleFilter {
113    fn default() -> Self {
114        Self {
115            min_level: LogLevel::Debug,
116            search:    String::new(),
117            show_trace: false,
118            show_debug: true,
119            show_info:  true,
120            show_warn:  true,
121            show_error: true,
122            show_fatal: true,
123        }
124    }
125}
126
127impl ConsoleFilter {
128    pub fn new() -> Self { Self::default() }
129
130    pub fn allows(&self, line: &ConsoleLine) -> bool {
131        let level_ok = match line.level {
132            LogLevel::Trace => self.show_trace,
133            LogLevel::Debug => self.show_debug,
134            LogLevel::Info  => self.show_info,
135            LogLevel::Warn  => self.show_warn,
136            LogLevel::Error => self.show_error,
137            LogLevel::Fatal => self.show_fatal,
138        } && line.level >= self.min_level;
139        let search_ok = self.search.is_empty()
140            || line.text.to_lowercase().contains(&self.search.to_lowercase());
141        level_ok && search_ok
142    }
143
144    pub fn set_search(&mut self, q: impl Into<String>) {
145        self.search = q.into();
146    }
147
148    pub fn show_all(&mut self) {
149        self.show_trace = true;
150        self.show_debug = true;
151        self.show_info  = true;
152        self.show_warn  = true;
153        self.show_error = true;
154        self.show_fatal = true;
155        self.min_level  = LogLevel::Trace;
156    }
157
158    pub fn show_errors_only(&mut self) {
159        self.show_trace = false;
160        self.show_debug = false;
161        self.show_info  = false;
162        self.show_warn  = false;
163        self.show_error = true;
164        self.show_fatal = true;
165        self.min_level  = LogLevel::Error;
166    }
167}
168
169// ─────────────────────────────────────────────────────────────────────────────
170// Ring buffer for log lines
171// ─────────────────────────────────────────────────────────────────────────────
172
173const RING_SIZE: usize = 10_000;
174
175struct RingBuffer {
176    buf:  Vec<ConsoleLine>,
177    head: usize,
178    len:  usize,
179}
180
181impl RingBuffer {
182    fn new() -> Self {
183        Self { buf: Vec::with_capacity(RING_SIZE), head: 0, len: 0 }
184    }
185
186    fn push(&mut self, line: ConsoleLine) {
187        if self.buf.len() < RING_SIZE {
188            self.buf.push(line);
189            self.len += 1;
190        } else {
191            self.buf[self.head] = line;
192            self.head = (self.head + 1) % RING_SIZE;
193        }
194    }
195
196    fn iter(&self) -> impl Iterator<Item = &ConsoleLine> {
197        let (right, left) = self.buf.split_at(self.head);
198        left.iter().chain(right.iter())
199    }
200
201    fn len(&self) -> usize {
202        self.len.min(self.buf.len())
203    }
204
205    fn clear(&mut self) {
206        self.buf.clear();
207        self.head = 0;
208        self.len = 0;
209    }
210}
211
212// ─────────────────────────────────────────────────────────────────────────────
213// CommandHistory
214// ─────────────────────────────────────────────────────────────────────────────
215
216/// Stores the last N typed commands for up/down arrow recall.
217#[derive(Debug, Clone, Default)]
218pub struct CommandHistory {
219    entries:  Vec<String>,
220    cursor:   Option<usize>,
221    max_size: usize,
222}
223
224impl CommandHistory {
225    pub fn new(max_size: usize) -> Self {
226        Self { entries: Vec::new(), cursor: None, max_size }
227    }
228
229    pub fn push(&mut self, cmd: impl Into<String>) {
230        let s: String = cmd.into();
231        if s.trim().is_empty() { return; }
232        // Avoid duplicate consecutive entries.
233        if self.entries.last().map(|e| e == &s).unwrap_or(false) { return; }
234        if self.entries.len() >= self.max_size {
235            self.entries.remove(0);
236        }
237        self.entries.push(s);
238        self.cursor = None;
239    }
240
241    pub fn navigate_up(&mut self) -> Option<&str> {
242        if self.entries.is_empty() { return None; }
243        let next = match self.cursor {
244            None => self.entries.len() - 1,
245            Some(0) => 0,
246            Some(c) => c - 1,
247        };
248        self.cursor = Some(next);
249        self.entries.get(next).map(|s| s.as_str())
250    }
251
252    pub fn navigate_down(&mut self) -> Option<&str> {
253        match self.cursor {
254            None => None,
255            Some(c) if c + 1 >= self.entries.len() => {
256                self.cursor = None;
257                None
258            }
259            Some(c) => {
260                self.cursor = Some(c + 1);
261                self.entries.get(c + 1).map(|s| s.as_str())
262            }
263        }
264    }
265
266    pub fn reset_cursor(&mut self) {
267        self.cursor = None;
268    }
269
270    pub fn len(&self) -> usize {
271        self.entries.len()
272    }
273
274    pub fn is_empty(&self) -> bool {
275        self.entries.is_empty()
276    }
277}
278
279// ─────────────────────────────────────────────────────────────────────────────
280// CommandAutoComplete
281// ─────────────────────────────────────────────────────────────────────────────
282
283/// Tab-completion engine: prefix-matches command names.
284#[derive(Debug, Clone, Default)]
285pub struct CommandAutoComplete {
286    completions: Vec<String>,
287    index:       usize,
288    last_prefix: String,
289}
290
291impl CommandAutoComplete {
292    pub fn new() -> Self { Self::default() }
293
294    /// Compute completions for a given prefix against a list of command names.
295    pub fn compute(&mut self, prefix: &str, all_names: &[&str]) {
296        if prefix == self.last_prefix && !self.completions.is_empty() { return; }
297        self.completions = all_names.iter()
298            .filter(|&&n| n.starts_with(prefix))
299            .map(|&s| s.to_string())
300            .collect();
301        self.completions.sort();
302        self.index = 0;
303        self.last_prefix = prefix.to_string();
304    }
305
306    /// Return the next completion in the cycle.
307    pub fn next(&mut self) -> Option<&str> {
308        if self.completions.is_empty() { return None; }
309        let s = &self.completions[self.index];
310        self.index = (self.index + 1) % self.completions.len();
311        Some(s)
312    }
313
314    pub fn clear(&mut self) {
315        self.completions.clear();
316        self.index = 0;
317        self.last_prefix.clear();
318    }
319
320    pub fn suggestions(&self) -> &[String] {
321        &self.completions
322    }
323
324    pub fn has_completions(&self) -> bool {
325        !self.completions.is_empty()
326    }
327}
328
329// ─────────────────────────────────────────────────────────────────────────────
330// CommandOutput
331// ─────────────────────────────────────────────────────────────────────────────
332
333/// The result returned by a command handler.
334#[derive(Debug, Clone)]
335pub struct CommandOutput {
336    pub text:    String,
337    pub level:   LogLevel,
338    pub success: bool,
339}
340
341impl CommandOutput {
342    pub fn ok(text: impl Into<String>) -> Self {
343        Self { text: text.into(), level: LogLevel::Info, success: true }
344    }
345    pub fn err(text: impl Into<String>) -> Self {
346        Self { text: text.into(), level: LogLevel::Error, success: false }
347    }
348    pub fn warn(text: impl Into<String>) -> Self {
349        Self { text: text.into(), level: LogLevel::Warn, success: true }
350    }
351}
352
353// ─────────────────────────────────────────────────────────────────────────────
354// CommandRegistration
355// ─────────────────────────────────────────────────────────────────────────────
356
357/// Metadata and handler for a registered command.
358pub struct CommandRegistration {
359    pub name:      String,
360    pub aliases:   Vec<String>,
361    pub help:      String,
362    pub usage:     String,
363    pub handler:   Box<dyn Fn(&[&str], &mut ConsoleState) -> CommandOutput + Send + Sync>,
364}
365
366impl std::fmt::Debug for CommandRegistration {
367    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
368        f.debug_struct("CommandRegistration")
369            .field("name", &self.name)
370            .finish()
371    }
372}
373
374// ─────────────────────────────────────────────────────────────────────────────
375// ConsoleState — mutable scene-side state that commands can modify
376// ─────────────────────────────────────────────────────────────────────────────
377
378/// The mutable game/engine state that console commands can inspect or modify.
379/// In a real engine this would hold references; here we use owned copies so
380/// the struct is `'static` and thread-safe.
381#[derive(Debug, Clone, Default)]
382pub struct ConsoleState {
383    pub time_scale:  f32,
384    pub fps:         f32,
385    pub frame_index: u64,
386    pub memory_mb:   f32,
387    pub entity_count: u32,
388    pub particle_count: u32,
389    pub field_count: u32,
390    /// Simple key-value store for command-side property overrides.
391    pub properties: HashMap<String, String>,
392    pub quit_requested: bool,
393    pub screenshot_path: Option<String>,
394    pub profiler_running: bool,
395    pub profiler_report: Option<String>,
396    pub log_output: Vec<String>,
397}
398
399impl ConsoleState {
400    pub fn new() -> Self {
401        Self { time_scale: 1.0, ..Default::default() }
402    }
403
404    pub fn set_property(&mut self, key: impl Into<String>, val: impl Into<String>) {
405        self.properties.insert(key.into(), val.into());
406    }
407
408    pub fn get_property(&self, key: &str) -> Option<&str> {
409        self.properties.get(key).map(|s| s.as_str())
410    }
411}
412
413// ─────────────────────────────────────────────────────────────────────────────
414// CommandRegistry
415// ─────────────────────────────────────────────────────────────────────────────
416
417/// Holds all registered commands and dispatches them.
418pub struct CommandRegistry {
419    commands: HashMap<String, CommandRegistration>,
420}
421
422impl CommandRegistry {
423    pub fn new() -> Self {
424        let mut reg = Self { commands: HashMap::new() };
425        reg.register_builtins();
426        reg
427    }
428
429    pub fn register(
430        &mut self,
431        name:    impl Into<String>,
432        help:    impl Into<String>,
433        usage:   impl Into<String>,
434        handler: impl Fn(&[&str], &mut ConsoleState) -> CommandOutput + Send + Sync + 'static,
435    ) {
436        let name: String = name.into();
437        self.commands.insert(name.clone(), CommandRegistration {
438            name,
439            aliases: Vec::new(),
440            help: help.into(),
441            usage: usage.into(),
442            handler: Box::new(handler),
443        });
444    }
445
446    pub fn register_alias(&mut self, alias: impl Into<String>, canonical: impl Into<String>) {
447        let alias: String = alias.into();
448        let canonical: String = canonical.into();
449        if let Some(reg) = self.commands.get_mut(&canonical) {
450            reg.aliases.push(alias.clone());
451        }
452        // Also insert duplicate entry pointing to same canonical by cloning help.
453        let (help, usage) = if let Some(r) = self.commands.get(&canonical) {
454            (r.help.clone(), r.usage.clone())
455        } else {
456            (String::new(), String::new())
457        };
458        // For alias we just re-register with a wrapper that looks up the canonical.
459        let canonical_clone = canonical.clone();
460        let alias_name = alias.clone();
461        self.commands.insert(alias, CommandRegistration {
462            name: alias_name,
463            aliases: vec![canonical_clone],
464            help,
465            usage,
466            handler: Box::new(move |args, state| {
467                // Forward to canonical — but we can't call self here, so just
468                // record the alias and return an ok with guidance.
469                let _ = (args, state, canonical.as_str());
470                CommandOutput::ok("(alias)")
471            }),
472        });
473    }
474
475    pub fn dispatch(&self, input: &str, state: &mut ConsoleState) -> CommandOutput {
476        let input = input.trim();
477        if input.is_empty() {
478            return CommandOutput::ok("");
479        }
480        let parts: Vec<&str> = input.split_whitespace().collect();
481        let cmd_name = parts[0].to_lowercase();
482        let args = &parts[1..];
483
484        if let Some(reg) = self.commands.get(&cmd_name) {
485            (reg.handler)(args, state)
486        } else {
487            CommandOutput::err(format!("Unknown command '{}'. Type 'help' for a list.", cmd_name))
488        }
489    }
490
491    pub fn names(&self) -> Vec<&str> {
492        let mut names: Vec<&str> = self.commands.keys().map(|s| s.as_str()).collect();
493        names.sort();
494        names
495    }
496
497    pub fn get(&self, name: &str) -> Option<&CommandRegistration> {
498        self.commands.get(name)
499    }
500
501    fn register_builtins(&mut self) {
502        // help
503        self.register(
504            "help",
505            "List all commands or describe a specific command.",
506            "help [command]",
507            |args, _state| {
508                if args.is_empty() {
509                    CommandOutput::ok(
510                        "Available: help spawn despawn set get teleport timescale fps mem \
511                         fields particle script reload screenshot profile quit eval clear version"
512                    )
513                } else {
514                    CommandOutput::ok(format!("Help for '{}': see source docs.", args[0]))
515                }
516            },
517        );
518
519        // spawn
520        self.register(
521            "spawn",
522            "Spawn an entity of a given type at optional position.",
523            "spawn <entity_type> [x y z]",
524            |args, state| {
525                if args.is_empty() {
526                    return CommandOutput::err("Usage: spawn <entity_type> [x y z]");
527                }
528                let kind = args[0];
529                let x = args.get(1).and_then(|s| s.parse::<f32>().ok()).unwrap_or(0.0);
530                let y = args.get(2).and_then(|s| s.parse::<f32>().ok()).unwrap_or(0.0);
531                let z = args.get(3).and_then(|s| s.parse::<f32>().ok()).unwrap_or(0.0);
532                state.entity_count += 1;
533                state.log_output.push(format!("Spawned {} at ({},{},{})", kind, x, y, z));
534                CommandOutput::ok(format!("Spawned {} at ({:.2},{:.2},{:.2})", kind, x, y, z))
535            },
536        );
537
538        // despawn
539        self.register(
540            "despawn",
541            "Remove an entity by id.",
542            "despawn <id>",
543            |args, state| {
544                let id: u32 = match args.first().and_then(|s| s.parse().ok()) {
545                    Some(v) => v,
546                    None => return CommandOutput::err("Usage: despawn <id>"),
547                };
548                if state.entity_count > 0 { state.entity_count -= 1; }
549                CommandOutput::ok(format!("Despawned entity {}", id))
550            },
551        );
552
553        // set
554        self.register(
555            "set",
556            "Set a property on an entity.",
557            "set <entity_id> <property> <value>",
558            |args, state| {
559                if args.len() < 3 {
560                    return CommandOutput::err("Usage: set <entity_id> <property> <value>");
561                }
562                let key = format!("{}.{}", args[0], args[1]);
563                let val = args[2..].join(" ");
564                state.set_property(key.clone(), val.clone());
565                CommandOutput::ok(format!("Set {}: {}", key, val))
566            },
567        );
568
569        // get
570        self.register(
571            "get",
572            "Get a property value from an entity.",
573            "get <entity_id> <property>",
574            |args, state| {
575                if args.len() < 2 {
576                    return CommandOutput::err("Usage: get <entity_id> <property>");
577                }
578                let key = format!("{}.{}", args[0], args[1]);
579                match state.get_property(&key) {
580                    Some(v) => CommandOutput::ok(format!("{} = {}", key, v)),
581                    None => CommandOutput::warn(format!("{} not set", key)),
582                }
583            },
584        );
585
586        // teleport
587        self.register(
588            "teleport",
589            "Move the editor camera to (x, y, z).",
590            "teleport <x> <y> <z>",
591            |args, state| {
592                if args.len() < 3 {
593                    return CommandOutput::err("Usage: teleport <x> <y> <z>");
594                }
595                let x = args[0].parse::<f32>().unwrap_or(0.0);
596                let y = args[1].parse::<f32>().unwrap_or(0.0);
597                let z = args[2].parse::<f32>().unwrap_or(0.0);
598                state.set_property("camera.x", x.to_string());
599                state.set_property("camera.y", y.to_string());
600                state.set_property("camera.z", z.to_string());
601                CommandOutput::ok(format!("Camera teleported to ({:.2},{:.2},{:.2})", x, y, z))
602            },
603        );
604
605        // timescale
606        self.register(
607            "timescale",
608            "Set simulation time scale (1.0 = normal, 0.5 = half speed).",
609            "timescale <factor>",
610            |args, state| {
611                let factor: f32 = match args.first().and_then(|s| s.parse().ok()) {
612                    Some(f) => f,
613                    None => return CommandOutput::err("Usage: timescale <factor>"),
614                };
615                if factor < 0.0 {
616                    return CommandOutput::err("Time scale cannot be negative.");
617                }
618                state.time_scale = factor;
619                CommandOutput::ok(format!("Time scale set to {:.3}", factor))
620            },
621        );
622
623        // fps
624        self.register(
625            "fps",
626            "Display current frames per second.",
627            "fps",
628            |_args, state| {
629                CommandOutput::ok(format!("FPS: {:.1}", state.fps))
630            },
631        );
632
633        // mem
634        self.register(
635            "mem",
636            "Display current memory usage.",
637            "mem",
638            |_args, state| {
639                CommandOutput::ok(format!("Memory: {:.1} MB", state.memory_mb))
640            },
641        );
642
643        // fields
644        self.register(
645            "fields",
646            "Manage force fields. Sub-commands: list | clear | add <type>",
647            "fields [list|clear|add <type>]",
648            |args, state| {
649                match args.first().copied() {
650                    None | Some("list") => {
651                        CommandOutput::ok(format!("{} force field(s) active", state.field_count))
652                    }
653                    Some("clear") => {
654                        state.field_count = 0;
655                        CommandOutput::ok("All force fields removed.")
656                    }
657                    Some("add") => {
658                        let kind = args.get(1).copied().unwrap_or("gravity");
659                        state.field_count += 1;
660                        CommandOutput::ok(format!("Added {} force field (total: {})", kind, state.field_count))
661                    }
662                    Some(sub) => CommandOutput::err(format!("Unknown sub-command: {}", sub)),
663                }
664            },
665        );
666
667        // particle
668        self.register(
669            "particle",
670            "Emit a particle burst at position.",
671            "particle <preset> <x> <y> <z>",
672            |args, state| {
673                if args.is_empty() {
674                    return CommandOutput::err("Usage: particle <preset> <x> <y> <z>");
675                }
676                let preset = args[0];
677                let x = args.get(1).and_then(|s| s.parse::<f32>().ok()).unwrap_or(0.0);
678                let y = args.get(2).and_then(|s| s.parse::<f32>().ok()).unwrap_or(0.0);
679                let z = args.get(3).and_then(|s| s.parse::<f32>().ok()).unwrap_or(0.0);
680                state.particle_count += 64;
681                CommandOutput::ok(format!("Emitted '{}' particles at ({:.1},{:.1},{:.1})", preset, x, y, z))
682            },
683        );
684
685        // script
686        self.register(
687            "script",
688            "Execute a script snippet inline.",
689            "script <source>",
690            |args, state| {
691                if args.is_empty() {
692                    return CommandOutput::err("Usage: script <source>");
693                }
694                let source = args.join(" ");
695                state.log_output.push(format!("Script executed: {}", source));
696                CommandOutput::ok(format!("Executed: {}", source))
697            },
698        );
699
700        // reload
701        self.register(
702            "reload",
703            "Hot-reload scripts and assets.",
704            "reload",
705            |_args, state| {
706                state.log_output.push("Hot-reload triggered.".into());
707                CommandOutput::ok("Reload triggered.")
708            },
709        );
710
711        // screenshot
712        self.register(
713            "screenshot",
714            "Capture the current frame to a file.",
715            "screenshot <filename>",
716            |args, state| {
717                let filename = args.first().copied().unwrap_or("screenshot.png");
718                state.screenshot_path = Some(filename.to_string());
719                CommandOutput::ok(format!("Screenshot will be saved to '{}'", filename))
720            },
721        );
722
723        // profile
724        self.register(
725            "profile",
726            "Control the built-in profiler.",
727            "profile <start|stop|report>",
728            |args, state| {
729                match args.first().copied() {
730                    Some("start") => {
731                        state.profiler_running = true;
732                        CommandOutput::ok("Profiler started.")
733                    }
734                    Some("stop") => {
735                        state.profiler_running = false;
736                        CommandOutput::ok("Profiler stopped.")
737                    }
738                    Some("report") => {
739                        let report = state.profiler_report.clone()
740                            .unwrap_or_else(|| "No report available.".into());
741                        CommandOutput::ok(report)
742                    }
743                    _ => CommandOutput::err("Usage: profile <start|stop|report>"),
744                }
745            },
746        );
747
748        // quit
749        self.register(
750            "quit",
751            "Exit the engine.",
752            "quit",
753            |_args, state| {
754                state.quit_requested = true;
755                CommandOutput::ok("Goodbye.")
756            },
757        );
758
759        // eval — simple expression evaluator
760        self.register(
761            "eval",
762            "Evaluate a mathematical expression.",
763            "eval <expression>",
764            |args, _state| {
765                if args.is_empty() {
766                    return CommandOutput::err("Usage: eval <expression>");
767                }
768                let expr = args.join(" ");
769                match eval_expression(&expr) {
770                    Ok(result) => CommandOutput::ok(format!("{} = {}", expr, result)),
771                    Err(e)     => CommandOutput::err(format!("Eval error: {}", e)),
772                }
773            },
774        );
775
776        // clear
777        self.register(
778            "clear",
779            "Clear the console log.",
780            "clear",
781            |_args, state| {
782                state.log_output.clear();
783                CommandOutput::ok("Console cleared.")
784            },
785        );
786
787        // version
788        self.register(
789            "version",
790            "Print engine version information.",
791            "version",
792            |_args, _state| {
793                CommandOutput::ok("Proof Engine v0.1.0 — mathematical rendering engine")
794            },
795        );
796    }
797}
798
799impl Default for CommandRegistry {
800    fn default() -> Self { Self::new() }
801}
802
803// ─────────────────────────────────────────────────────────────────────────────
804// Simple expression evaluator
805// ─────────────────────────────────────────────────────────────────────────────
806
807/// Evaluate a simple infix mathematical expression.
808/// Supports: +  -  *  /  ^  ( )  sin  cos  tan  sqrt  abs  pi  e
809pub fn eval_expression(expr: &str) -> Result<f64, String> {
810    let tokens = tokenize(expr)?;
811    let mut pos = 0;
812    let result = parse_expr(&tokens, &mut pos)?;
813    if pos != tokens.len() {
814        return Err(format!("Unexpected token at position {}", pos));
815    }
816    Ok(result)
817}
818
819#[derive(Debug, Clone, PartialEq)]
820enum Token {
821    Number(f64),
822    Plus, Minus, Star, Slash, Caret,
823    LParen, RParen,
824    Ident(String),
825    Comma,
826}
827
828fn tokenize(expr: &str) -> Result<Vec<Token>, String> {
829    let mut tokens = Vec::new();
830    let chars: Vec<char> = expr.chars().collect();
831    let mut i = 0;
832    while i < chars.len() {
833        match chars[i] {
834            ' ' | '\t' => { i += 1; }
835            '+' => { tokens.push(Token::Plus);   i += 1; }
836            '-' => { tokens.push(Token::Minus);  i += 1; }
837            '*' => { tokens.push(Token::Star);   i += 1; }
838            '/' => { tokens.push(Token::Slash);  i += 1; }
839            '^' => { tokens.push(Token::Caret);  i += 1; }
840            '(' => { tokens.push(Token::LParen); i += 1; }
841            ')' => { tokens.push(Token::RParen); i += 1; }
842            ',' => { tokens.push(Token::Comma);  i += 1; }
843            c if c.is_ascii_digit() || c == '.' => {
844                let start = i;
845                while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') {
846                    i += 1;
847                }
848                let s: String = chars[start..i].iter().collect();
849                let v: f64 = s.parse().map_err(|_| format!("Bad number: {}", s))?;
850                tokens.push(Token::Number(v));
851            }
852            c if c.is_alphabetic() || c == '_' => {
853                let start = i;
854                while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
855                    i += 1;
856                }
857                let ident: String = chars[start..i].iter().collect();
858                tokens.push(Token::Ident(ident));
859            }
860            c => return Err(format!("Unexpected character: '{}'", c)),
861        }
862    }
863    Ok(tokens)
864}
865
866fn parse_expr(tokens: &[Token], pos: &mut usize) -> Result<f64, String> {
867    parse_add(tokens, pos)
868}
869
870fn parse_add(tokens: &[Token], pos: &mut usize) -> Result<f64, String> {
871    let mut left = parse_mul(tokens, pos)?;
872    while *pos < tokens.len() {
873        match &tokens[*pos] {
874            Token::Plus  => { *pos += 1; left += parse_mul(tokens, pos)?; }
875            Token::Minus => { *pos += 1; left -= parse_mul(tokens, pos)?; }
876            _ => break,
877        }
878    }
879    Ok(left)
880}
881
882fn parse_mul(tokens: &[Token], pos: &mut usize) -> Result<f64, String> {
883    let mut left = parse_pow(tokens, pos)?;
884    while *pos < tokens.len() {
885        match &tokens[*pos] {
886            Token::Star  => { *pos += 1; left *= parse_pow(tokens, pos)?; }
887            Token::Slash => {
888                *pos += 1;
889                let right = parse_pow(tokens, pos)?;
890                if right.abs() < f64::EPSILON {
891                    return Err("Division by zero".into());
892                }
893                left /= right;
894            }
895            _ => break,
896        }
897    }
898    Ok(left)
899}
900
901fn parse_pow(tokens: &[Token], pos: &mut usize) -> Result<f64, String> {
902    let base = parse_unary(tokens, pos)?;
903    if *pos < tokens.len() && tokens[*pos] == Token::Caret {
904        *pos += 1;
905        let exp = parse_pow(tokens, pos)?;
906        return Ok(base.powf(exp));
907    }
908    Ok(base)
909}
910
911fn parse_unary(tokens: &[Token], pos: &mut usize) -> Result<f64, String> {
912    if *pos < tokens.len() && tokens[*pos] == Token::Minus {
913        *pos += 1;
914        return Ok(-parse_primary(tokens, pos)?);
915    }
916    if *pos < tokens.len() && tokens[*pos] == Token::Plus {
917        *pos += 1;
918        return parse_primary(tokens, pos);
919    }
920    parse_primary(tokens, pos)
921}
922
923fn parse_primary(tokens: &[Token], pos: &mut usize) -> Result<f64, String> {
924    if *pos >= tokens.len() {
925        return Err("Unexpected end of expression".into());
926    }
927    match tokens[*pos].clone() {
928        Token::Number(v) => { *pos += 1; Ok(v) }
929        Token::LParen => {
930            *pos += 1;
931            let v = parse_expr(tokens, pos)?;
932            if *pos >= tokens.len() || tokens[*pos] != Token::RParen {
933                return Err("Expected ')'".into());
934            }
935            *pos += 1;
936            Ok(v)
937        }
938        Token::Ident(name) => {
939            *pos += 1;
940            // Constants
941            match name.as_str() {
942                "pi" | "PI" => return Ok(std::f64::consts::PI),
943                "e"  | "E"  => return Ok(std::f64::consts::E),
944                "tau" | "TAU" => return Ok(std::f64::consts::TAU),
945                _ => {}
946            }
947            // Functions — expect '(' arg ')'
948            if *pos < tokens.len() && tokens[*pos] == Token::LParen {
949                *pos += 1;
950                let arg = parse_expr(tokens, pos)?;
951                // Optional second argument
952                let arg2 = if *pos < tokens.len() && tokens[*pos] == Token::Comma {
953                    *pos += 1;
954                    Some(parse_expr(tokens, pos)?)
955                } else {
956                    None
957                };
958                if *pos >= tokens.len() || tokens[*pos] != Token::RParen {
959                    return Err("Expected ')' after function argument".into());
960                }
961                *pos += 1;
962                return match name.as_str() {
963                    "sin"   => Ok(arg.sin()),
964                    "cos"   => Ok(arg.cos()),
965                    "tan"   => Ok(arg.tan()),
966                    "asin"  => Ok(arg.asin()),
967                    "acos"  => Ok(arg.acos()),
968                    "atan"  => Ok(arg.atan()),
969                    "atan2" => Ok(arg.atan2(arg2.unwrap_or(1.0))),
970                    "sqrt"  => Ok(arg.sqrt()),
971                    "abs"   => Ok(arg.abs()),
972                    "ceil"  => Ok(arg.ceil()),
973                    "floor" => Ok(arg.floor()),
974                    "round" => Ok(arg.round()),
975                    "log"   => Ok(arg.ln()),
976                    "log2"  => Ok(arg.log2()),
977                    "log10" => Ok(arg.log10()),
978                    "exp"   => Ok(arg.exp()),
979                    "pow"   => Ok(arg.powf(arg2.unwrap_or(2.0))),
980                    "min"   => Ok(arg.min(arg2.unwrap_or(arg))),
981                    "max"   => Ok(arg.max(arg2.unwrap_or(arg))),
982                    "sign"  => Ok(arg.signum()),
983                    f => Err(format!("Unknown function: {}", f)),
984                };
985            }
986            Err(format!("Unknown identifier: {}", name))
987        }
988        ref t => Err(format!("Unexpected token: {:?}", t)),
989    }
990}
991
992// ─────────────────────────────────────────────────────────────────────────────
993// ConsolePrinter
994// ─────────────────────────────────────────────────────────────────────────────
995
996/// Renders the console to a box-drawing ASCII string.
997pub struct ConsolePrinter {
998    pub width: usize,
999    pub visible_lines: usize,
1000}
1001
1002impl ConsolePrinter {
1003    pub fn new(width: usize, visible_lines: usize) -> Self {
1004        Self { width, visible_lines }
1005    }
1006
1007    pub fn render(
1008        &self,
1009        console: &DevConsole,
1010        filter: &ConsoleFilter,
1011    ) -> String {
1012        let border_top    = format!("┌{}┐\n", "─".repeat(self.width));
1013        let border_bottom = format!("└{}┘\n", "─".repeat(self.width));
1014
1015        let filtered: Vec<&ConsoleLine> = console.lines()
1016            .filter(|l| filter.allows(l))
1017            .collect();
1018
1019        let total = filtered.len();
1020        let start = if total > self.visible_lines {
1021            total - self.visible_lines - console.scroll_offset.min(total.saturating_sub(self.visible_lines))
1022        } else {
1023            0
1024        };
1025
1026        let mut body = String::new();
1027        for line in filtered.iter().skip(start).take(self.visible_lines) {
1028            let text = line.render();
1029            let truncated = if text.len() + 2 > self.width {
1030                format!("{}…", &text[..self.width.saturating_sub(3)])
1031            } else {
1032                text
1033            };
1034            let padding = self.width.saturating_sub(truncated.len());
1035            body.push_str(&format!("│{}{}│\n", truncated, " ".repeat(padding)));
1036        }
1037
1038        // Prompt line
1039        let prompt = format!("> {}{}", console.input_buffer, if console.cursor_visible { "|" } else { "" });
1040        let ppx = self.width.saturating_sub(prompt.len());
1041        let prompt_line = format!("│{}{}│\n", prompt, " ".repeat(ppx));
1042
1043        format!("{}{}{}{}", border_top, body, prompt_line, border_bottom)
1044    }
1045}
1046
1047// ─────────────────────────────────────────────────────────────────────────────
1048// ConsoleSink — bridges log::log! macros
1049// ─────────────────────────────────────────────────────────────────────────────
1050
1051/// A sink that can receive log records from the `log` crate.
1052/// Callers hold a reference and push into the DevConsole.
1053pub struct ConsoleSink {
1054    /// Lines captured from log macros, drained periodically into DevConsole.
1055    pub pending: std::sync::Mutex<Vec<(LogLevel, String)>>,
1056}
1057
1058impl ConsoleSink {
1059    pub fn new() -> Self {
1060        Self { pending: std::sync::Mutex::new(Vec::new()) }
1061    }
1062
1063    pub fn push(&self, level: LogLevel, text: impl Into<String>) {
1064        if let Ok(mut p) = self.pending.lock() {
1065            p.push((level, text.into()));
1066        }
1067    }
1068
1069    pub fn drain(&self) -> Vec<(LogLevel, String)> {
1070        if let Ok(mut p) = self.pending.lock() {
1071            std::mem::take(&mut *p)
1072        } else {
1073            Vec::new()
1074        }
1075    }
1076}
1077
1078impl Default for ConsoleSink {
1079    fn default() -> Self { Self::new() }
1080}
1081
1082// ─────────────────────────────────────────────────────────────────────────────
1083// DevConsole — top-level struct
1084// ─────────────────────────────────────────────────────────────────────────────
1085
1086/// The developer console: log viewer + command input.
1087pub struct DevConsole {
1088    ring:         RingBuffer,
1089    pub filter:   ConsoleFilter,
1090    pub registry: CommandRegistry,
1091    pub state:    ConsoleState,
1092    pub history:  CommandHistory,
1093    pub complete: CommandAutoComplete,
1094
1095    /// Current text in the input field.
1096    pub input_buffer: String,
1097    /// Blinking cursor state.
1098    pub cursor_visible: bool,
1099    cursor_timer: f32,
1100
1101    /// How many lines we've scrolled back from the bottom.
1102    pub scroll_offset: usize,
1103
1104    /// Start time for relative timestamps.
1105    start: Instant,
1106}
1107
1108impl DevConsole {
1109    pub fn new() -> Self {
1110        let mut con = Self {
1111            ring:           RingBuffer::new(),
1112            filter:         ConsoleFilter::new(),
1113            registry:       CommandRegistry::new(),
1114            state:          ConsoleState::new(),
1115            history:        CommandHistory::new(100),
1116            complete:       CommandAutoComplete::new(),
1117            input_buffer:   String::new(),
1118            cursor_visible: true,
1119            cursor_timer:   0.0,
1120            scroll_offset:  0,
1121            start:          Instant::now(),
1122        };
1123        con.log(LogLevel::Info, "Proof Engine console ready. Type 'help' for commands.");
1124        con
1125    }
1126
1127    // ── Logging ───────────────────────────────────────────────────────────────
1128
1129    pub fn log(&mut self, level: LogLevel, text: impl Into<String>) {
1130        let text: String = text.into();
1131        let ts = self.start.elapsed();
1132
1133        // Deduplication: if last line has same text, increment count.
1134        let last_matches = self.ring.buf.last().map(|l| l.text == text).unwrap_or(false);
1135        if last_matches {
1136            if let Some(last) = self.ring.buf.last_mut() {
1137                last.count += 1;
1138                return;
1139            }
1140        }
1141
1142        self.ring.push(ConsoleLine::new(text, level, ts));
1143        // Auto-scroll to bottom.
1144        self.scroll_offset = 0;
1145    }
1146
1147    pub fn trace(&mut self, text: impl Into<String>) { self.log(LogLevel::Trace, text); }
1148    pub fn debug(&mut self, text: impl Into<String>) { self.log(LogLevel::Debug, text); }
1149    pub fn info (&mut self, text: impl Into<String>) { self.log(LogLevel::Info,  text); }
1150    pub fn warn (&mut self, text: impl Into<String>) { self.log(LogLevel::Warn,  text); }
1151    pub fn error(&mut self, text: impl Into<String>) { self.log(LogLevel::Error, text); }
1152    pub fn fatal(&mut self, text: impl Into<String>) { self.log(LogLevel::Fatal, text); }
1153
1154    pub fn lines(&self) -> impl Iterator<Item = &ConsoleLine> {
1155        self.ring.iter()
1156    }
1157
1158    pub fn line_count(&self) -> usize {
1159        self.ring.len()
1160    }
1161
1162    pub fn clear_log(&mut self) {
1163        self.ring.clear();
1164    }
1165
1166    // ── Input ─────────────────────────────────────────────────────────────────
1167
1168    pub fn push_char(&mut self, c: char) {
1169        self.input_buffer.push(c);
1170        self.complete.clear();
1171    }
1172
1173    pub fn pop_char(&mut self) {
1174        self.input_buffer.pop();
1175        self.complete.clear();
1176    }
1177
1178    pub fn clear_input(&mut self) {
1179        self.input_buffer.clear();
1180        self.complete.clear();
1181    }
1182
1183    /// Submit the current input buffer as a command.
1184    pub fn submit(&mut self) -> CommandOutput {
1185        let input = std::mem::take(&mut self.input_buffer);
1186        self.history.push(&input);
1187        self.complete.clear();
1188        self.history.reset_cursor();
1189
1190        self.log(LogLevel::Debug, format!("> {}", input));
1191        let out = self.registry.dispatch(&input, &mut self.state);
1192        self.log(out.level, &out.text);
1193        out
1194    }
1195
1196    /// Attempt tab-completion on the current input.
1197    pub fn tab_complete(&mut self) {
1198        let names = self.registry.names();
1199        let prefix = self.input_buffer.split_whitespace().next().unwrap_or("");
1200        self.complete.compute(prefix, &names);
1201        if let Some(completion) = self.complete.next() {
1202            let completed = completion.to_string();
1203            // Replace only the command word.
1204            let rest = self.input_buffer.trim_start().splitn(2, ' ').nth(1)
1205                .map(|s| format!(" {}", s))
1206                .unwrap_or_default();
1207            self.input_buffer = format!("{}{}", completed, rest);
1208        }
1209    }
1210
1211    pub fn history_up(&mut self) {
1212        if let Some(cmd) = self.history.navigate_up() {
1213            self.input_buffer = cmd.to_string();
1214        }
1215    }
1216
1217    pub fn history_down(&mut self) {
1218        match self.history.navigate_down() {
1219            Some(cmd) => self.input_buffer = cmd.to_string(),
1220            None => self.clear_input(),
1221        }
1222    }
1223
1224    // ── Scrolling ─────────────────────────────────────────────────────────────
1225
1226    pub fn scroll_up(&mut self, lines: usize) {
1227        self.scroll_offset = self.scroll_offset.saturating_add(lines);
1228    }
1229
1230    pub fn scroll_down(&mut self, lines: usize) {
1231        self.scroll_offset = self.scroll_offset.saturating_sub(lines);
1232    }
1233
1234    pub fn scroll_to_bottom(&mut self) {
1235        self.scroll_offset = 0;
1236    }
1237
1238    // ── Tick ──────────────────────────────────────────────────────────────────
1239
1240    pub fn tick(&mut self, dt: f32) {
1241        self.cursor_timer += dt;
1242        if self.cursor_timer >= 0.5 {
1243            self.cursor_visible = !self.cursor_visible;
1244            self.cursor_timer = 0.0;
1245        }
1246    }
1247
1248    // ── Drain sink ────────────────────────────────────────────────────────────
1249
1250    /// Drain a ConsoleSink into this console.
1251    pub fn drain_sink(&mut self, sink: &ConsoleSink) {
1252        for (level, text) in sink.drain() {
1253            self.log(level, text);
1254        }
1255    }
1256}
1257
1258impl Default for DevConsole {
1259    fn default() -> Self { Self::new() }
1260}
1261
1262// ─────────────────────────────────────────────────────────────────────────────
1263// Tests
1264// ─────────────────────────────────────────────────────────────────────────────
1265
1266#[cfg(test)]
1267mod tests {
1268    use super::*;
1269
1270    fn console() -> DevConsole {
1271        DevConsole::new()
1272    }
1273
1274    #[test]
1275    fn test_log_and_count() {
1276        let mut c = console();
1277        let initial = c.line_count();
1278        c.info("hello");
1279        c.warn("world");
1280        assert!(c.line_count() >= initial + 2);
1281    }
1282
1283    #[test]
1284    fn test_log_dedup() {
1285        let mut c = console();
1286        c.ring.clear();
1287        c.info("repeated");
1288        c.info("repeated");
1289        c.info("repeated");
1290        assert_eq!(c.ring.len(), 1);
1291        assert_eq!(c.ring.buf[0].count, 3);
1292    }
1293
1294    #[test]
1295    fn test_command_help() {
1296        let mut c = console();
1297        c.input_buffer = "help".into();
1298        let out = c.submit();
1299        assert!(out.success);
1300        assert!(out.text.contains("help"));
1301    }
1302
1303    #[test]
1304    fn test_command_timescale() {
1305        let mut c = console();
1306        c.input_buffer = "timescale 0.5".into();
1307        let out = c.submit();
1308        assert!(out.success);
1309        assert!((c.state.time_scale - 0.5).abs() < 1e-6);
1310    }
1311
1312    #[test]
1313    fn test_command_unknown() {
1314        let mut c = console();
1315        c.input_buffer = "xyzzy".into();
1316        let out = c.submit();
1317        assert!(!out.success);
1318    }
1319
1320    #[test]
1321    fn test_command_history_nav() {
1322        let mut c = console();
1323        c.input_buffer = "fps".into(); c.submit();
1324        c.input_buffer = "mem".into(); c.submit();
1325        c.history_up();
1326        assert_eq!(c.input_buffer, "mem");
1327        c.history_up();
1328        assert_eq!(c.input_buffer, "fps");
1329        c.history_down();
1330        assert_eq!(c.input_buffer, "mem");
1331    }
1332
1333    #[test]
1334    fn test_tab_complete() {
1335        let mut c = console();
1336        c.input_buffer = "tim".into();
1337        c.tab_complete();
1338        assert!(c.input_buffer.starts_with("timescale"));
1339    }
1340
1341    #[test]
1342    fn test_eval_expression() {
1343        assert!((eval_expression("2 + 3").unwrap() - 5.0).abs() < 1e-9);
1344        assert!((eval_expression("sin(0)").unwrap()).abs() < 1e-9);
1345        assert!((eval_expression("sqrt(4)").unwrap() - 2.0).abs() < 1e-9);
1346        let pi_half = eval_expression("sin(pi * 0.5)").unwrap();
1347        assert!((pi_half - 1.0).abs() < 1e-9);
1348        assert!((eval_expression("2 ^ 10").unwrap() - 1024.0).abs() < 1e-9);
1349        assert!(eval_expression("1 / 0").is_err());
1350    }
1351
1352    #[test]
1353    fn test_eval_nested() {
1354        let v = eval_expression("sqrt(2 * (3 + 1))").unwrap();
1355        assert!((v - std::f64::consts::SQRT_2 * 2.0).abs() < 1e-9);
1356    }
1357
1358    #[test]
1359    fn test_eval_via_command() {
1360        let mut c = console();
1361        c.input_buffer = "eval 2 + 2".into();
1362        let out = c.submit();
1363        assert!(out.success);
1364        assert!(out.text.contains("4"));
1365    }
1366
1367    #[test]
1368    fn test_filter_level() {
1369        let mut f = ConsoleFilter::new();
1370        f.show_debug = false;
1371        let line = ConsoleLine::new("msg", LogLevel::Debug, Duration::ZERO);
1372        assert!(!f.allows(&line));
1373        let line2 = ConsoleLine::new("msg", LogLevel::Error, Duration::ZERO);
1374        assert!(f.allows(&line2));
1375    }
1376
1377    #[test]
1378    fn test_command_spawn_despawn() {
1379        let mut c = console();
1380        c.input_buffer = "spawn enemy 1 2 3".into();
1381        let out = c.submit();
1382        assert!(out.success);
1383        assert!(c.state.entity_count > 0);
1384    }
1385
1386    #[test]
1387    fn test_console_printer() {
1388        let mut c = console();
1389        c.info("test line");
1390        let printer = ConsolePrinter::new(60, 5);
1391        let rendered = printer.render(&c, &c.filter.clone());
1392        assert!(rendered.contains("┌"));
1393        assert!(rendered.contains("└"));
1394    }
1395
1396    #[test]
1397    fn test_sink_drain() {
1398        let sink = ConsoleSink::new();
1399        sink.push(LogLevel::Info, "from sink");
1400        let mut c = console();
1401        c.ring.clear();
1402        c.drain_sink(&sink);
1403        assert!(c.line_count() >= 1);
1404    }
1405}