1use std::collections::VecDeque;
33use glam::{Vec3, Vec4};
34
35#[derive(Debug, Clone, PartialEq)]
38pub enum LogLevel { Info, Warn, Error, Success, Debug, Command }
39
40#[derive(Debug, Clone)]
41pub struct LogLine {
42 pub level: LogLevel,
43 pub text: String,
44 pub color: Vec4,
45}
46
47impl LogLine {
48 pub fn new(level: LogLevel, text: impl Into<String>) -> Self {
49 let color = match level {
50 LogLevel::Info => Vec4::new(0.9, 0.9, 0.9, 1.0),
51 LogLevel::Warn => Vec4::new(1.0, 0.85, 0.2, 1.0),
52 LogLevel::Error => Vec4::new(1.0, 0.3, 0.3, 1.0),
53 LogLevel::Success => Vec4::new(0.3, 1.0, 0.5, 1.0),
54 LogLevel::Debug => Vec4::new(0.6, 0.6, 1.0, 1.0),
55 LogLevel::Command => Vec4::new(0.5, 0.8, 1.0, 1.0),
56 };
57 Self { level, text: text.into(), color }
58 }
59
60 pub fn info(text: impl Into<String>) -> Self { Self::new(LogLevel::Info, text) }
61 pub fn warn(text: impl Into<String>) -> Self { Self::new(LogLevel::Warn, text) }
62 pub fn error(text: impl Into<String>) -> Self { Self::new(LogLevel::Error, text) }
63 pub fn success(text: impl Into<String>) -> Self { Self::new(LogLevel::Success, text) }
64 pub fn debug(text: impl Into<String>) -> Self { Self::new(LogLevel::Debug, text) }
65 pub fn command(text: impl Into<String>) -> Self { Self::new(LogLevel::Command, text) }
66}
67
68#[derive(Debug, Clone)]
71pub struct ArgSpec {
72 pub name: String,
73 pub required: bool,
74 pub default: Option<String>,
75 pub completions: Vec<String>,
76 pub description: String,
77}
78
79impl ArgSpec {
80 pub fn required(name: impl Into<String>) -> Self {
81 Self {
82 name: name.into(),
83 required: true,
84 default: None,
85 completions: Vec::new(),
86 description: String::new(),
87 }
88 }
89
90 pub fn optional(name: impl Into<String>, default: impl Into<String>) -> Self {
91 Self {
92 name: name.into(),
93 required: false,
94 default: Some(default.into()),
95 completions: Vec::new(),
96 description: String::new(),
97 }
98 }
99
100 pub fn with_completions(mut self, opts: Vec<String>) -> Self {
101 self.completions = opts;
102 self
103 }
104
105 pub fn with_description(mut self, d: impl Into<String>) -> Self {
106 self.description = d.into();
107 self
108 }
109}
110
111pub type HandlerFn = Box<dyn Fn(&[&str], &mut Vec<LogLine>) + Send + Sync>;
114
115pub struct Command {
116 pub name: String,
117 pub help: String,
118 pub args: Vec<ArgSpec>,
119 pub handler: HandlerFn,
120 pub aliases: Vec<String>,
121 pub hidden: bool,
122}
123
124impl Command {
125 pub fn new(
126 name: impl Into<String>,
127 help: impl Into<String>,
128 handler: HandlerFn,
129 ) -> Self {
130 Self {
131 name: name.into(),
132 help: help.into(),
133 args: Vec::new(),
134 handler,
135 aliases: Vec::new(),
136 hidden: false,
137 }
138 }
139
140 pub fn with_args(mut self, args: Vec<ArgSpec>) -> Self { self.args = args; self }
141 pub fn with_alias(mut self, alias: impl Into<String>) -> Self { self.aliases.push(alias.into()); self }
142 pub fn hidden(mut self) -> Self { self.hidden = true; self }
143
144 pub fn usage(&self) -> String {
145 let mut s = self.name.clone();
146 for arg in &self.args {
147 if arg.required {
148 s.push_str(&format!(" <{}>", arg.name));
149 } else {
150 let def = arg.default.as_deref().unwrap_or("...");
151 s.push_str(&format!(" [{}={}]", arg.name, def));
152 }
153 }
154 s
155 }
156}
157
158#[derive(Debug, Clone)]
161pub struct CompletionResult {
162 pub completions: Vec<String>,
163 pub common_prefix: String,
164}
165
166impl CompletionResult {
167 pub fn empty() -> Self { Self { completions: Vec::new(), common_prefix: String::new() } }
168
169 fn from_list(prefix: &str, candidates: &[impl AsRef<str>]) -> Self {
170 let matching: Vec<String> = candidates.iter()
171 .filter(|c| c.as_ref().starts_with(prefix))
172 .map(|c| c.as_ref().to_owned())
173 .collect();
174 if matching.is_empty() {
175 return Self::empty();
176 }
177 let common = common_prefix(&matching);
178 Self { completions: matching, common_prefix: common }
179 }
180}
181
182fn common_prefix(strings: &[String]) -> String {
183 if strings.is_empty() { return String::new(); }
184 let first = &strings[0];
185 let mut len = first.len();
186 for s in &strings[1..] {
187 len = len.min(s.len());
188 len = first.chars().zip(s.chars()).take(len).take_while(|(a, b)| a == b).count();
189 }
190 first[..len].to_owned()
191}
192
193const MAX_LOG_LINES: usize = 1024;
197const MAX_HISTORY: usize = 200;
199const VISIBLE_LOG_LINES: usize = 20;
201const MAX_INPUT_LEN: usize = 512;
203
204pub struct Console {
205 pub visible: bool,
207 pub input: String,
209 pub cursor: usize,
211 pub log: VecDeque<LogLine>,
213 pub scroll_offset: usize,
215 history: VecDeque<String>,
217 history_index: Option<usize>,
219 history_saved: String,
221 commands: Vec<Command>,
223 pending_completion: Option<CompletionResult>,
225 vars: std::collections::HashMap<String, String>,
227}
228
229impl Console {
230 pub fn new() -> Self {
231 let mut console = Self {
232 visible: false,
233 input: String::new(),
234 cursor: 0,
235 log: VecDeque::new(),
236 scroll_offset: 0,
237 history: VecDeque::new(),
238 history_index: None,
239 history_saved: String::new(),
240 commands: Vec::new(),
241 pending_completion: None,
242 vars: std::collections::HashMap::new(),
243 };
244 console.register_builtins();
245 console
246 }
247
248 pub fn register_command(&mut self, cmd: Command) {
251 self.commands.push(cmd);
252 }
253
254 fn register_builtins(&mut self) {
255 self.commands.push(Command::new("help", "Show help for a command or list all commands",
256 Box::new(|args, out| {
257 if args.is_empty() {
258 out.push(LogLine::info("Type 'list' to see all commands. 'help <command>' for details."));
259 } else {
260 out.push(LogLine::info(format!("Help for '{}': (use 'list' to see all)", args[0])));
261 }
262 })
263 ).with_args(vec![ArgSpec::optional("command", "")]));
264
265 self.commands.push(Command::new("list", "List all registered commands",
266 Box::new(|_args, out| {
267 out.push(LogLine::info("Available commands: help, list, set, get, clear, echo, history, reload, version"));
268 })
269 ));
270
271 self.commands.push(Command::new("clear", "Clear the console output log",
272 Box::new(|_args, out| {
273 out.push(LogLine::new(LogLevel::Debug, "CLEAR"));
274 })
275 ));
276
277 self.commands.push(Command::new("echo", "Print arguments to the console",
278 Box::new(|args, out| {
279 out.push(LogLine::info(args.join(" ")));
280 })
281 ).with_args(vec![ArgSpec::required("text")]));
282
283 self.commands.push(Command::new("set", "Set a console variable: set <name> <value>",
284 Box::new(|args, out| {
285 if args.len() < 2 {
286 out.push(LogLine::warn("Usage: set <name> <value>"));
287 } else {
288 out.push(LogLine::success(format!("SET {} = {}", args[0], args[1..]
289 .join(" "))));
290 }
291 })
292 ).with_args(vec![ArgSpec::required("name"), ArgSpec::required("value")]));
293
294 self.commands.push(Command::new("get", "Get a console variable: get <name>",
295 Box::new(|args, out| {
296 if args.is_empty() {
297 out.push(LogLine::warn("Usage: get <name>"));
298 } else {
299 out.push(LogLine::info(format!("GET {}", args[0])));
300 }
301 })
302 ).with_args(vec![ArgSpec::required("name")]));
303
304 self.commands.push(Command::new("history", "Show command history",
305 Box::new(|_args, out| {
306 out.push(LogLine::info("--- command history ---"));
307 })
308 ));
309
310 self.commands.push(Command::new("version", "Show engine version",
311 Box::new(|_args, out| {
312 out.push(LogLine::info("Proof Engine -- mathematical rendering engine for Rust"));
313 })
314 ));
315
316 self.commands.push(Command::new("reload", "Reload engine config from disk",
317 Box::new(|_args, out| {
318 out.push(LogLine::info("Reloading config..."));
319 })
320 ));
321
322 self.commands.push(Command::new("quit", "Quit the engine",
323 Box::new(|_args, out| {
324 out.push(LogLine::warn("Quit requested."));
325 })
326 ).with_alias("exit").with_alias("q"));
327 }
328
329 pub fn type_char(&mut self, c: char) {
333 if self.input.len() >= MAX_INPUT_LEN { return; }
334 self.input.insert(self.cursor, c);
335 self.cursor += c.len_utf8();
336 self.pending_completion = None;
337 self.history_index = None;
338 }
339
340 pub fn backspace(&mut self) {
342 if self.cursor == 0 { return; }
343 let before = &self.input[..self.cursor];
345 if let Some(c) = before.chars().next_back() {
346 let len = c.len_utf8();
347 self.input.remove(self.cursor - len);
348 self.cursor -= len;
349 }
350 self.pending_completion = None;
351 }
352
353 pub fn delete_forward(&mut self) {
355 if self.cursor >= self.input.len() { return; }
356 self.input.remove(self.cursor);
357 self.pending_completion = None;
358 }
359
360 pub fn cursor_left(&mut self) {
362 if self.cursor == 0 { return; }
363 let before = &self.input[..self.cursor];
364 if let Some(c) = before.chars().next_back() {
365 self.cursor -= c.len_utf8();
366 }
367 }
368
369 pub fn cursor_right(&mut self) {
371 if self.cursor >= self.input.len() { return; }
372 let c = self.input[self.cursor..].chars().next().unwrap();
373 self.cursor += c.len_utf8();
374 }
375
376 pub fn cursor_home(&mut self) { self.cursor = 0; }
378
379 pub fn cursor_end(&mut self) { self.cursor = self.input.len(); }
381
382 pub fn clear_input(&mut self) {
384 self.input.clear();
385 self.cursor = 0;
386 self.history_index = None;
387 }
388
389 pub fn history_prev(&mut self) {
393 if self.history.is_empty() { return; }
394 match self.history_index {
395 None => {
396 self.history_saved = self.input.clone();
397 self.history_index = Some(self.history.len() - 1);
398 }
399 Some(0) => return,
400 Some(ref mut i) => *i -= 1,
401 }
402 if let Some(idx) = self.history_index {
403 self.input = self.history[idx].clone();
404 self.cursor = self.input.len();
405 }
406 }
407
408 pub fn history_next(&mut self) {
410 match self.history_index {
411 None => return,
412 Some(i) if i + 1 >= self.history.len() => {
413 self.history_index = None;
414 self.input = self.history_saved.clone();
415 self.cursor = self.input.len();
416 }
417 Some(ref mut i) => {
418 *i += 1;
419 let idx = *i;
420 self.input = self.history[idx].clone();
421 self.cursor = self.input.len();
422 }
423 }
424 }
425
426 pub fn tab_complete(&mut self) {
430 let input = self.input.trim_start().to_owned();
431 if input.is_empty() {
432 let names: Vec<String> = self.commands.iter()
434 .filter(|c| !c.hidden)
435 .map(|c| c.name.clone())
436 .collect();
437 for name in &names { self.log.push_back(LogLine::debug(name.clone())); }
438 self.trim_log();
439 return;
440 }
441
442 let parts: Vec<&str> = input.splitn(2, ' ').collect();
443 let command_word = parts[0];
444
445 if parts.len() == 1 {
446 let all_names: Vec<String> = self.commands.iter()
448 .flat_map(|c| std::iter::once(c.name.clone()).chain(c.aliases.iter().cloned()))
449 .filter(|n| !n.is_empty())
450 .collect();
451 let result = CompletionResult::from_list(command_word, &all_names);
452 if result.completions.len() == 1 {
453 self.input = result.completions[0].clone() + " ";
454 self.cursor = self.input.len();
455 } else if result.completions.len() > 1 {
456 if result.common_prefix.len() > command_word.len() {
457 self.input = result.common_prefix.clone();
458 self.cursor = self.input.len();
459 }
460 for c in &result.completions {
461 self.log.push_back(LogLine::debug(c.clone()));
462 }
463 self.trim_log();
464 }
465 self.pending_completion = Some(result);
466 } else {
467 let partial_arg = parts[1];
469 if let Some(cmd) = self.commands.iter().find(|c| c.name == command_word || c.aliases.contains(&command_word.to_owned())) {
470 let completions: Vec<String> = cmd.args.iter()
471 .flat_map(|a| a.completions.iter().cloned())
472 .collect();
473 let result = CompletionResult::from_list(partial_arg, &completions);
474 if result.completions.len() == 1 {
475 self.input = format!("{} {}", command_word, result.completions[0]);
476 self.cursor = self.input.len();
477 } else if result.completions.len() > 1 {
478 for c in &result.completions {
479 self.log.push_back(LogLine::debug(c.clone()));
480 }
481 self.trim_log();
482 }
483 self.pending_completion = Some(result);
484 }
485 }
486 }
487
488 pub fn submit(&mut self) -> ConsoleAction {
492 let line = self.input.trim().to_owned();
493 if line.is_empty() { return ConsoleAction::None; }
494
495 if self.history.back().map(|l| l.as_str()) != Some(&line) {
497 self.history.push_back(line.clone());
498 if self.history.len() > MAX_HISTORY {
499 self.history.pop_front();
500 }
501 }
502 self.history_index = None;
503 self.history_saved.clear();
504
505 self.log.push_back(LogLine::command(format!("> {}", line)));
506 self.input.clear();
507 self.cursor = 0;
508 self.scroll_offset = 0;
509
510 self.execute_line(&line)
511 }
512
513 fn execute_line(&mut self, line: &str) -> ConsoleAction {
514 let mut parts = tokenize(line);
515 if parts.is_empty() { return ConsoleAction::None; }
516
517 let cmd_name = parts.remove(0);
518 let args: Vec<&str> = parts.iter().map(|s| s.as_str()).collect();
519
520 match cmd_name.as_str() {
522 "quit" | "exit" | "q" => {
523 self.log.push_back(LogLine::warn("Quitting..."));
524 self.trim_log();
525 return ConsoleAction::Quit;
526 }
527 "clear" => {
528 self.log.clear();
529 self.scroll_offset = 0;
530 return ConsoleAction::None;
531 }
532 "history" => {
533 for (i, h) in self.history.iter().enumerate() {
534 self.log.push_back(LogLine::debug(format!("{:4}: {}", i + 1, h)));
535 }
536 self.trim_log();
537 return ConsoleAction::None;
538 }
539 "set" if args.len() >= 2 => {
540 let val = args[1..].join(" ");
541 self.vars.insert(args[0].to_owned(), val.clone());
542 self.log.push_back(LogLine::success(format!("{} = {}", args[0], val)));
543 self.trim_log();
544 return ConsoleAction::None;
545 }
546 "get" if !args.is_empty() => {
547 let val = self.vars.get(args[0]).cloned().unwrap_or_else(|| "<undefined>".into());
548 self.log.push_back(LogLine::info(format!("{} = {}", args[0], val)));
549 self.trim_log();
550 return ConsoleAction::None;
551 }
552 _ => {}
553 }
554
555 let found = self.commands.iter().any(|c| {
557 c.name == cmd_name || c.aliases.contains(&cmd_name)
558 });
559
560 if found {
561 let mut output: Vec<LogLine> = Vec::new();
562 for cmd in &self.commands {
564 if cmd.name == cmd_name || cmd.aliases.contains(&cmd_name) {
565 (cmd.handler)(&args, &mut output);
566 break;
567 }
568 }
569 for line in output {
570 self.log.push_back(line);
571 }
572 if cmd_name == "help" && !args.is_empty() {
574 let target = args[0];
575 if let Some(cmd) = self.commands.iter().find(|c| c.name == target) {
576 self.log.push_back(LogLine::info(format!(" {}", cmd.usage())));
577 self.log.push_back(LogLine::info(format!(" {}", cmd.help)));
578 } else {
579 self.log.push_back(LogLine::warn(format!("Unknown command: '{}'", target)));
580 }
581 }
582 } else {
583 self.log.push_back(LogLine::error(format!("Unknown command: '{}'. Type 'list' for help.", cmd_name)));
584 }
585
586 self.trim_log();
587 ConsoleAction::None
588 }
589
590 fn trim_log(&mut self) {
591 while self.log.len() > MAX_LOG_LINES {
592 self.log.pop_front();
593 if self.scroll_offset > 0 {
594 self.scroll_offset = self.scroll_offset.saturating_sub(1);
595 }
596 }
597 }
598
599 pub fn scroll_up(&mut self, lines: usize) {
602 let max_scroll = self.log.len().saturating_sub(VISIBLE_LOG_LINES);
603 self.scroll_offset = (self.scroll_offset + lines).min(max_scroll);
604 }
605
606 pub fn scroll_down(&mut self, lines: usize) {
607 self.scroll_offset = self.scroll_offset.saturating_sub(lines);
608 }
609
610 pub fn scroll_to_bottom(&mut self) { self.scroll_offset = 0; }
611 pub fn scroll_to_top(&mut self) {
612 self.scroll_offset = self.log.len().saturating_sub(VISIBLE_LOG_LINES);
613 }
614
615 pub fn visible_lines(&self) -> impl Iterator<Item = &LogLine> {
619 let total = self.log.len();
620 let start = if self.scroll_offset + VISIBLE_LOG_LINES > total {
621 0
622 } else {
623 total - VISIBLE_LOG_LINES - self.scroll_offset
624 };
625 let end = (total - self.scroll_offset).min(total);
626 self.log.range(start..end)
627 }
628
629 pub fn input_before_cursor(&self) -> &str { &self.input[..self.cursor] }
631 pub fn input_after_cursor(&self) -> &str { &self.input[self.cursor..] }
632
633 pub fn toggle(&mut self) { self.visible = !self.visible; }
635
636 pub fn print(&mut self, line: LogLine) {
638 self.log.push_back(line);
639 self.trim_log();
640 }
641
642 pub fn println(&mut self, text: impl Into<String>) {
643 self.print(LogLine::info(text));
644 }
645
646 pub fn print_warn(&mut self, text: impl Into<String>) {
647 self.print(LogLine::warn(text));
648 }
649
650 pub fn print_error(&mut self, text: impl Into<String>) {
651 self.print(LogLine::error(text));
652 }
653
654 pub fn print_success(&mut self, text: impl Into<String>) {
655 self.print(LogLine::success(text));
656 }
657}
658
659impl Default for Console {
660 fn default() -> Self { Self::new() }
661}
662
663#[derive(Debug, Clone, PartialEq)]
667pub enum ConsoleAction {
668 None,
669 Quit,
670 Reload,
671 RunScript(String),
672 SetVar { name: String, value: String },
673}
674
675fn tokenize(line: &str) -> Vec<String> {
679 let mut tokens = Vec::new();
680 let mut current = String::new();
681 let mut in_quotes = false;
682 let mut quote_char = '"';
683
684 for c in line.chars() {
685 match c {
686 '"' | '\'' if !in_quotes => { in_quotes = true; quote_char = c; }
687 c if in_quotes && c == quote_char => { in_quotes = false; }
688 ' ' | '\t' if !in_quotes => {
689 if !current.is_empty() {
690 tokens.push(current.clone());
691 current.clear();
692 }
693 }
694 _ => current.push(c),
695 }
696 }
697 if !current.is_empty() { tokens.push(current); }
698 tokens
699}
700
701#[derive(Debug, Clone, Default)]
706pub struct ConsoleSink {
707 pub pending: Vec<LogLine>,
708}
709
710impl ConsoleSink {
711 pub fn new() -> Self { Self::default() }
712
713 pub fn info(&mut self, text: impl Into<String>) { self.pending.push(LogLine::info(text)); }
714 pub fn warn(&mut self, text: impl Into<String>) { self.pending.push(LogLine::warn(text)); }
715 pub fn error(&mut self, text: impl Into<String>) { self.pending.push(LogLine::error(text)); }
716 pub fn success(&mut self, text: impl Into<String>) { self.pending.push(LogLine::success(text)); }
717
718 pub fn flush(&mut self, console: &mut Console) {
720 for line in self.pending.drain(..) {
721 console.print(line);
722 }
723 }
724}