//! Console panel for displaying output and error messages //! //! Shows a scrollable log of messages with color coding and a REPL input. use eframe::egui; use egui_extras::{Size, StripBuilder}; use crate::gui::state::{GuiState, MessageKind}; use crate::repl::{MetaCommand, ListTarget, ExecuteResult}; /// Console panel for output messages and REPL input pub struct ConsolePanel { /// Whether to auto-scroll to bottom auto_scroll: bool, /// REPL input buffer input: String, /// Command history history: Vec, /// Current position in history (for up/down navigation) history_pos: Option, } impl ConsolePanel { pub fn new() -> Self { Self { auto_scroll: true, input: String::new(), history: Vec::new(), history_pos: None, } } pub fn show(&mut self, ui: &mut egui::Ui, state: &mut GuiState) { StripBuilder::new(ui) .size(Size::exact(28.0)) .size(Size::remainder().at_least(80.0)) .size(Size::exact(28.0)) .vertical(|mut strip| { strip.cell(|ui| { ui.horizontal(|ui| { ui.heading("Console"); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { if ui .add_sized([64.0, 22.0], egui::Button::new("Clear")) .clicked() { state.clear_console(); } ui.add_sized( [112.0, 22.0], egui::Checkbox::new(&mut self.auto_scroll, "Auto-scroll"), ); ui.add_sized( [96.0, 22.0], egui::Label::new(format!("{} messages", state.console_messages.len())), ); }); }); }); strip.cell(|ui| { egui::Frame::default() .inner_margin(egui::Margin::same(4)) .show(ui, |ui| { egui::ScrollArea::vertical() .auto_shrink([false, false]) .stick_to_bottom(self.auto_scroll) .show(ui, |ui| { if state.console_messages.is_empty() { ui.label("Type commands below (e.g., :help, :list, or geolog code)"); } else { for message in &state.console_messages { let color = match message.kind { MessageKind::Info => egui::Color32::LIGHT_GRAY, MessageKind::Success => egui::Color32::from_rgb(100, 200, 100), MessageKind::Error => egui::Color32::from_rgb(255, 100, 100), MessageKind::Warning => egui::Color32::from_rgb(255, 200, 100), }; let prefix = match message.kind { MessageKind::Info => ">", MessageKind::Success => "+", MessageKind::Error => "!", MessageKind::Warning => "?", }; ui.horizontal(|ui| { ui.add_sized( [12.0, 18.0], egui::Label::new( egui::RichText::new(prefix) .color(color) .monospace(), ), ); ui.label( egui::RichText::new(&message.text) .color(color) .monospace(), ); }); } } }); }); }); strip.cell(|ui| { ui.horizontal(|ui| { ui.add_sized( [68.0, 22.0], egui::Label::new( egui::RichText::new("geolog>") .monospace() .color(egui::Color32::from_rgb(100, 150, 255)), ), ); let run_width = 56.0; let spacing = ui.spacing().item_spacing.x; let input_width = (ui.available_width() - run_width - spacing).max(120.0); let response = ui.add_sized( [input_width, 22.0], egui::TextEdit::singleline(&mut self.input) .font(egui::FontId::monospace(13.0)) .hint_text(":help for commands"), ); if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) { self.execute_input(state); response.request_focus(); } if response.has_focus() { if ui.input(|i| i.key_pressed(egui::Key::ArrowUp)) { self.history_up(); } if ui.input(|i| i.key_pressed(egui::Key::ArrowDown)) { self.history_down(); } } if ui .add_sized([run_width, 22.0], egui::Button::new("Run")) .clicked() { self.execute_input(state); } }); }); }); } fn execute_input(&mut self, state: &mut GuiState) { let input = self.input.trim().to_string(); if input.is_empty() { return; } // Add to history self.history.push(input.clone()); self.history_pos = None; // Echo the input state.log_info(format!("geolog> {}", input)); // Process the input if input.starts_with(':') { // Meta command let cmd = MetaCommand::parse(&input); self.handle_meta_command(state, cmd); } else { // Geolog code match state.repl.execute_geolog(&input) { Ok(results) => { for result in results { match result { ExecuteResult::Namespace(name) => { state.log_info(format!("Namespace: {}", name)); } ExecuteResult::Theory { name, num_sorts, num_functions, num_relations, num_axioms, } => { let mut parts = vec![format!("{} sorts", num_sorts)]; if num_functions > 0 { parts.push(format!("{} functions", num_functions)); } if num_relations > 0 { parts.push(format!("{} relations", num_relations)); } if num_axioms > 0 { parts.push(format!("{} axioms", num_axioms)); } state.log_success(format!( "Defined theory {} ({})", name, parts.join(", ") )); } ExecuteResult::Instance { name, theory_name, num_elements, } => { state.log_success(format!( "Defined instance {} : {} ({} elements)", name, theory_name, num_elements )); } ExecuteResult::Query(_) => { state.log_info("Query executed"); } } } } Err(e) => { state.log_error(e); } } } self.input.clear(); } fn handle_meta_command(&mut self, state: &mut GuiState, cmd: MetaCommand) { match cmd { MetaCommand::Help(topic) => { match topic.as_deref() { None => { state.log_info("Commands:"); state.log_info(" :help Show this help"); state.log_info(" :list List theories and instances"); state.log_info(" :inspect Inspect a theory or instance"); state.log_info(" :chase Run chase on instance"); state.log_info(" :reset Reset all state"); state.log_info(" :clear Clear console"); state.log_info(""); state.log_info("Or type geolog code directly."); } Some(t) => { state.log_info(format!("Help topic: {}", t)); } } } MetaCommand::List(target) => { match target { ListTarget::Theories | ListTarget::All => { let theories = state.repl.list_theories(); if theories.is_empty() { state.log_info("No theories defined."); } else { state.log_info("Theories:"); for t in theories { state.log_info(format!( " {} ({} sorts, {} rels, {} axioms)", t.name, t.num_sorts, t.num_relations, t.num_axioms )); } } } ListTarget::Instances => {} } match target { ListTarget::Instances | ListTarget::All => { let instances = state.repl.list_instances(); if instances.is_empty() { state.log_info("No instances defined."); } else { state.log_info("Instances:"); for i in instances { state.log_info(format!( " {} : {} ({} elements)", i.name, i.theory_name, i.num_elements )); } } } ListTarget::Theories => {} } } MetaCommand::Inspect(name) => { use crate::repl::{InspectResult, format_theory_detail, format_instance_detail}; match state.repl.inspect(&name) { Some(InspectResult::Theory(detail)) => { for line in format_theory_detail(&detail).lines() { state.log_info(line.to_string()); } } Some(InspectResult::Instance(detail)) => { for line in format_instance_detail(&detail).lines() { state.log_info(line.to_string()); } } None => { state.log_error(format!("Not found: {}", name)); } } } MetaCommand::Clear => { state.clear_console(); } MetaCommand::Reset => { state.repl.reset(); state.selected_item = None; state.log_success("State reset."); } MetaCommand::Chase { instance, max_iterations } => { self.run_chase(state, &instance, max_iterations); } MetaCommand::Source(path) => { match std::fs::read_to_string(&path) { Ok(content) => { state.editor_content = content; state.current_file = Some(path.clone()); state.log_info(format!("Loaded: {}", path.display())); state.execute_editor(); } Err(e) => { state.log_error(format!("Failed to load: {}", e)); } } } _ => { state.log_warning("Command not yet implemented in GUI"); } } } fn run_chase(&self, state: &mut GuiState, instance_name: &str, max_iterations: Option) { use crate::core::RelationStorage; use crate::query::chase::chase_fixpoint; let theory_name = match state.repl.instances.get(instance_name) { Some(e) => e.theory_name.clone(), None => { state.log_error(format!("Instance '{}' not found", instance_name)); return; } }; let theory = match state.repl.theories.get(&theory_name) { Some(t) => t.clone(), None => { state.log_error(format!("Theory '{}' not found", theory_name)); return; } }; let sig = &theory.theory.signature; let axioms = &theory.theory.axioms; if axioms.is_empty() { state.log_warning(format!("Theory '{}' has no axioms", theory_name)); return; } state.log_info(format!("Running chase on '{}' ({} axioms)...", instance_name, axioms.len())); let entry = state.repl.instances.get_mut(instance_name).unwrap(); let max_iter = max_iterations.unwrap_or(100); let start = std::time::Instant::now(); let result = chase_fixpoint( axioms, &mut entry.structure, &mut state.repl.store.universe, sig, max_iter, ); match result { Ok(iterations) => { let elapsed = start.elapsed(); // Capture structure info before releasing the borrow let total_tuples: usize = entry.structure.relations.iter().map(|r| r.len()).sum(); let num_elements = entry.structure.len(); state.log_success(format!( "Chase completed in {} iterations ({:.2}ms)", iterations, elapsed.as_secs_f64() * 1000.0 )); state.log_info(format!( "Structure: {} elements, {} relation tuples", num_elements, total_tuples )); } Err(e) => { state.log_error(format!("Chase error: {}", e)); } } } fn history_up(&mut self) { if self.history.is_empty() { return; } match self.history_pos { None => { self.history_pos = Some(self.history.len() - 1); } Some(pos) if pos > 0 => { self.history_pos = Some(pos - 1); } _ => {} } if let Some(pos) = self.history_pos { self.input = self.history[pos].clone(); } } fn history_down(&mut self) { match self.history_pos { Some(pos) if pos + 1 < self.history.len() => { self.history_pos = Some(pos + 1); self.input = self.history[pos + 1].clone(); } Some(_) => { self.history_pos = None; self.input.clear(); } None => {} } } } impl Default for ConsolePanel { fn default() -> Self { Self::new() } }