2026-03-20 11:01:04 +01:00
|
|
|
//! Console panel for displaying output and error messages
|
|
|
|
|
//!
|
2026-03-20 11:05:58 +01:00
|
|
|
//! Shows a scrollable log of messages with color coding and a REPL input.
|
2026-03-20 11:01:04 +01:00
|
|
|
|
|
|
|
|
use eframe::egui;
|
|
|
|
|
|
|
|
|
|
use crate::gui::state::{GuiState, MessageKind};
|
2026-03-20 11:05:58 +01:00
|
|
|
use crate::repl::{MetaCommand, ListTarget, ExecuteResult};
|
2026-03-20 11:01:04 +01:00
|
|
|
|
2026-03-20 11:05:58 +01:00
|
|
|
/// Console panel for output messages and REPL input
|
2026-03-20 11:01:04 +01:00
|
|
|
pub struct ConsolePanel {
|
|
|
|
|
/// Whether to auto-scroll to bottom
|
|
|
|
|
auto_scroll: bool,
|
2026-03-20 11:05:58 +01:00
|
|
|
/// REPL input buffer
|
|
|
|
|
input: String,
|
|
|
|
|
/// Command history
|
|
|
|
|
history: Vec<String>,
|
|
|
|
|
/// Current position in history (for up/down navigation)
|
|
|
|
|
history_pos: Option<usize>,
|
2026-03-20 11:01:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ConsolePanel {
|
|
|
|
|
pub fn new() -> Self {
|
2026-03-20 11:05:58 +01:00
|
|
|
Self {
|
|
|
|
|
auto_scroll: true,
|
|
|
|
|
input: String::new(),
|
|
|
|
|
history: Vec::new(),
|
|
|
|
|
history_pos: None,
|
|
|
|
|
}
|
2026-03-20 11:01:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn show(&mut self, ui: &mut egui::Ui, state: &mut GuiState) {
|
|
|
|
|
// Header with title and buttons
|
|
|
|
|
ui.horizontal(|ui| {
|
|
|
|
|
ui.heading("Console");
|
|
|
|
|
|
|
|
|
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
|
|
|
|
if ui.button("Clear").clicked() {
|
|
|
|
|
state.clear_console();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ui.checkbox(&mut self.auto_scroll, "Auto-scroll");
|
|
|
|
|
|
|
|
|
|
ui.label(format!("{} messages", state.console_messages.len()));
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
ui.separator();
|
|
|
|
|
|
2026-03-20 11:05:58 +01:00
|
|
|
// Scrollable message area (takes most of the space)
|
|
|
|
|
let available_height = (ui.available_height() - 30.0).max(50.0); // Reserve space for input
|
2026-03-20 11:01:04 +01:00
|
|
|
let scroll_area = egui::ScrollArea::vertical()
|
|
|
|
|
.auto_shrink([false, false])
|
2026-03-20 11:05:58 +01:00
|
|
|
.max_height(available_height)
|
2026-03-20 11:01:04 +01:00
|
|
|
.stick_to_bottom(self.auto_scroll);
|
|
|
|
|
|
|
|
|
|
scroll_area.show(ui, |ui| {
|
|
|
|
|
if state.console_messages.is_empty() {
|
2026-03-20 11:05:58 +01:00
|
|
|
ui.label("Type commands below (e.g., :help, :list, or geolog code)");
|
2026-03-20 11:01:04 +01:00
|
|
|
} 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.label(
|
|
|
|
|
egui::RichText::new(prefix)
|
|
|
|
|
.color(color)
|
|
|
|
|
.monospace(),
|
|
|
|
|
);
|
|
|
|
|
ui.label(
|
|
|
|
|
egui::RichText::new(&message.text)
|
|
|
|
|
.color(color)
|
|
|
|
|
.monospace(),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-03-20 11:05:58 +01:00
|
|
|
|
|
|
|
|
// REPL input at the bottom
|
|
|
|
|
ui.separator();
|
|
|
|
|
ui.horizontal(|ui| {
|
|
|
|
|
ui.label(
|
|
|
|
|
egui::RichText::new("geolog>")
|
|
|
|
|
.monospace()
|
|
|
|
|
.color(egui::Color32::from_rgb(100, 150, 255)),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let response = ui.add(
|
|
|
|
|
egui::TextEdit::singleline(&mut self.input)
|
|
|
|
|
.font(egui::FontId::monospace(13.0))
|
|
|
|
|
.desired_width((ui.available_width() - 60.0).max(50.0))
|
|
|
|
|
.hint_text(":help for commands"),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Handle Enter key
|
|
|
|
|
if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
|
|
|
|
self.execute_input(state);
|
|
|
|
|
response.request_focus();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle Up/Down for history
|
|
|
|
|
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.button("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 <n> Inspect a theory or instance");
|
|
|
|
|
state.log_info(" :chase <inst> 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<usize>) {
|
|
|
|
|
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 => {}
|
|
|
|
|
}
|
2026-03-20 11:01:04 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for ConsolePanel {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self::new()
|
|
|
|
|
}
|
|
|
|
|
}
|