1use std::collections::HashMap;
5use std::time::{Duration, Instant};
6
7#[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 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#[derive(Debug, Clone)]
58pub struct ConsoleLine {
59 pub text: String,
60 pub level: LogLevel,
61 pub timestamp: Duration,
63 pub count: u32,
65 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 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#[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
169const 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#[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 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#[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 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 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#[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
353pub 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#[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 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
413pub 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
803pub 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 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 if *pos < tokens.len() && tokens[*pos] == Token::LParen {
949 *pos += 1;
950 let arg = parse_expr(tokens, pos)?;
951 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
992pub 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 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
1047pub struct ConsoleSink {
1054 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
1082pub 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 pub input_buffer: String,
1097 pub cursor_visible: bool,
1099 cursor_timer: f32,
1100
1101 pub scroll_offset: usize,
1103
1104 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 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 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 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 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 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 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 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 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 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 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#[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}