Hassan Abedi bd9b1cc11d WIP
2026-03-20 13:52:11 +01:00

440 lines
17 KiB
Rust

//! 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<String>,
/// Current position in history (for up/down navigation)
history_pos: Option<usize>,
}
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 <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 => {}
}
}
}
impl Default for ConsolePanel {
fn default() -> Self {
Self::new()
}
}