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