diff --git a/Cargo.lock b/Cargo.lock index 51105c9..e2a5797 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1050,7 +1050,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1136,6 +1136,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "float_next_after" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" + [[package]] name = "fnv" version = "1.0.7" @@ -1222,6 +1228,7 @@ version = "0.1.0" dependencies = [ "anyhow", "iced", + "iced_aw", "imara-diff", "nucleo-matcher", "rmcp", @@ -1675,6 +1682,21 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "iced_aw" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e05df3019f20c6decea93d035b32a2afc7b329d89cc5a68cca097d0e0a1889" +dependencies = [ + "cfg-if", + "chrono", + "iced", + "iced_fonts", + "itertools", + "num-format", + "num-traits", +] + [[package]] name = "iced_core" version = "0.13.2" @@ -1695,6 +1717,15 @@ dependencies = [ "web-time", ] +[[package]] +name = "iced_fonts" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df7deb0800a850ee25c8a42559f72c0f249e577feb3aad37b9b65dc1e517e52a" +dependencies = [ + "iced_core", +] + [[package]] name = "iced_futures" version = "0.13.2" @@ -1736,6 +1767,7 @@ dependencies = [ "iced_core", "iced_futures", "log", + "lyon_path", "once_cell", "raw-window-handle", "rustc-hash 2.1.1", @@ -1799,6 +1831,7 @@ dependencies = [ "iced_glyphon", "iced_graphics", "log", + "lyon", "once_cell", "rustc-hash 2.1.1", "thiserror 1.0.69", @@ -1886,6 +1919,15 @@ dependencies = [ "libc", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -2049,6 +2091,58 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +[[package]] +name = "lyon" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbcb7d54d54c8937364c9d41902d066656817dce1e03a44e5533afebd1ef4352" +dependencies = [ + "lyon_algorithms", + "lyon_tessellation", +] + +[[package]] +name = "lyon_algorithms" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c0829e28c4f336396f250d850c3987e16ce6db057ffe047ce0dd54aab6b647" +dependencies = [ + "lyon_path", + "num-traits", +] + +[[package]] +name = "lyon_geom" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e16770d760c7848b0c1c2d209101e408207a65168109509f8483837a36cf2e7" +dependencies = [ + "arrayvec", + "euclid", + "num-traits", +] + +[[package]] +name = "lyon_path" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aeca86bcfd632a15984ba029b539ffb811e0a70bf55e814ef8b0f54f506fdeb" +dependencies = [ + "lyon_geom", + "num-traits", +] + +[[package]] +name = "lyon_tessellation" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3f586142e1280335b1bc89539f7c97dd80f08fc43e9ab1b74ef0a42b04aa353" +dependencies = [ + "float_next_after", + "lyon_path", + "num-traits", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -2239,6 +2333,16 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "num-format" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" +dependencies = [ + "arrayvec", + "itoa", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2246,6 +2350,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -3115,7 +3220,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3582,7 +3687,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix 1.1.2", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4373,7 +4478,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 4b5d1c7..3207b39 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [dependencies] anyhow = "1.0.100" iced = { version = "0.13.1", features = ["tokio"] } +iced_aw = { version = "0.11.0", features = ["tab_bar"] } imara-diff = "0.2.0" nucleo-matcher = "0.3.1" rmcp = { version = "0.8.0", features = ["server", "macros", "transport-sse-server", "transport-io", "transport-streamable-http-server", "elicitation", "schemars"] } diff --git a/src/gui.rs b/src/gui.rs index 78e09b0..982eebe 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -1,24 +1,200 @@ -use iced::{Element, widget::text_editor}; +use crate::fossil::FossilManager; +use iced::{ + Element, + widget::{button, column, row, text, text_editor}, +}; +use iced_aw::{TabLabel, Tabs}; +use std::collections::HashMap; #[derive(Default)] pub struct State { - content: text_editor::Content, + fossil_manager: FossilManager, + tab_editors: HashMap, + active_tab: Option, + tab_order: Vec, } #[derive(Debug, Clone)] pub enum Message { - Edit(text_editor::Action), + Edit(String, text_editor::Action), + TabSelected(String), + CreateFossil, + SyncFossil(String), + DeleteFossil(String), + RefreshFromManager, +} + +impl State { + pub fn new() -> Self { + Self::default() + } + + pub fn with_shared_manager(fossil_manager: FossilManager) -> Self { + Self { + fossil_manager, + tab_editors: HashMap::new(), + active_tab: None, + tab_order: Vec::new(), + } + } + + pub fn add_fossil(&mut self, id: String, content: String) { + // Add to fossil manager + { + let mut fossils = self.fossil_manager.fossils.lock().unwrap(); + fossils.insert(id.clone(), crate::fossil::Fossil::new(content.clone())); + } + + // Add editor tab + self.tab_editors + .insert(id.clone(), text_editor::Content::with_text(&content)); + self.tab_order.push(id.clone()); + self.active_tab = Some(id); + } + + pub fn sync_fossil(&mut self, id: &str) { + if let Some(editor_content) = self.tab_editors.get(id) { + let content = editor_content.text(); + let mut fossils = self.fossil_manager.fossils.lock().unwrap(); + if let Some(fossil) = fossils.get_mut(id) { + fossil.commit(content); + } + } + } + + pub fn load_fossil_to_editor(&mut self, id: &str) { + let fossils = self.fossil_manager.fossils.lock().unwrap(); + if let Some(fossil) = fossils.get(id) { + if let Some(latest_content) = fossil.latest() { + self.tab_editors.insert( + id.to_string(), + text_editor::Content::with_text(latest_content), + ); + } + } + } + + pub fn refresh_from_manager(&mut self) { + let fossils = self.fossil_manager.fossils.lock().unwrap(); + let fossil_ids: Vec = fossils.keys().cloned().collect(); + drop(fossils); + + // Add new fossils that don't exist in GUI + for id in &fossil_ids { + if !self.tab_editors.contains_key(id) { + self.load_fossil_to_editor(id); + self.tab_order.push(id.clone()); + } + } + + // Remove fossils that no longer exist in manager + let gui_fossil_ids: Vec = self.tab_editors.keys().cloned().collect(); + for id in gui_fossil_ids { + if !fossil_ids.contains(&id) { + self.tab_editors.remove(&id); + self.tab_order.retain(|x| x != &id); + if self.active_tab.as_ref() == Some(&id) { + self.active_tab = self.tab_order.first().cloned(); + } + } + } + + // Update active tab if none is selected but tabs exist + if self.active_tab.is_none() && !self.tab_order.is_empty() { + self.active_tab = Some(self.tab_order[0].clone()); + } + } } pub fn view(state: &State) -> Element<'_, Message> { - text_editor(&state.content) - .placeholder("Type something here...") - .on_action(Message::Edit) - .into() + let mut tabs = Tabs::new(Message::TabSelected); + + for id in &state.tab_order { + tabs = tabs.push( + id.clone(), + TabLabel::Text(id.clone()), + tab_content(state, id), + ); + } + + let tabs = if let Some(active_id) = &state.active_tab { + tabs.set_active_tab(active_id) + } else { + tabs + }; + + let controls = row![ + button("New Fossil").on_press(Message::CreateFossil), + button("Sync Active").on_press_maybe( + state + .active_tab + .as_ref() + .map(|id| Message::SyncFossil(id.clone())) + ), + button("Delete Active").on_press_maybe( + state + .active_tab + .as_ref() + .map(|id| Message::DeleteFossil(id.clone())) + ), + button("Refresh").on_press(Message::RefreshFromManager), + ] + .spacing(10) + .padding(10); + + column![controls, tabs].spacing(10).padding(10).into() } -pub fn update(state: &mut State, message: Message) { - match message { - Message::Edit(action) => {} +fn tab_content<'a>(state: &'a State, fossil_id: &str) -> Element<'a, Message> { + if let Some(content) = state.tab_editors.get(fossil_id) { + text_editor(content) + .placeholder("Edit your fossil content here...") + .on_action({ + let fossil_id = fossil_id.to_string(); + move |action| Message::Edit(fossil_id.clone(), action) + }) + .into() + } else { + text("No content available").into() } } + +pub fn update(state: &mut State, message: Message) -> iced::Task { + match message { + Message::Edit(fossil_id, action) => { + if let Some(content) = state.tab_editors.get_mut(&fossil_id) { + content.perform(action); + } + } + Message::TabSelected(tab_id) => { + state.active_tab = Some(tab_id); + } + Message::CreateFossil => { + let new_id = format!("fossil_{}", state.tab_order.len() + 1); + state.add_fossil(new_id, String::new()); + } + Message::SyncFossil(fossil_id) => { + state.sync_fossil(&fossil_id); + } + Message::DeleteFossil(fossil_id) => { + // Remove from fossil manager + { + let mut fossils = state.fossil_manager.fossils.lock().unwrap(); + fossils.remove(&fossil_id); + } + + // Remove from GUI state + state.tab_editors.remove(&fossil_id); + state.tab_order.retain(|id| id != &fossil_id); + + // Update active tab + if state.active_tab.as_ref() == Some(&fossil_id) { + state.active_tab = state.tab_order.first().cloned(); + } + } + Message::RefreshFromManager => { + state.refresh_from_manager(); + } + } + iced::Task::none() +} diff --git a/src/main.rs b/src/main.rs index f25be96..c591679 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,9 @@ use crate::{ - gui::{update, view}, + gui::{update, view, State}, mcp::FossilEditor, + fossil::FossilManager, }; -use iced::Executor; +use iced::Task; use rmcp::{ServiceExt, transport::stdio}; use tokio::runtime::Runtime; use tracing_subscriber::{self, EnvFilter}; @@ -21,7 +22,10 @@ fn main() -> iced::Result { let rt = Runtime::new().unwrap(); - let editor = FossilEditor::new(); + // Create shared fossil manager + let shared_fossil_manager = FossilManager::new(); + + let editor = FossilEditor::with_shared_state(shared_fossil_manager.clone()); rt.spawn(async { let service = editor .serve(stdio()) @@ -34,5 +38,12 @@ fn main() -> iced::Result { service.waiting().await.unwrap(); }); - iced::run("A cool counter", update, view) + iced::application("Fossil Editor", update, view) + .default_font(iced::Font::MONOSPACE) + .run_with(move || { + let mut state = State::with_shared_manager(shared_fossil_manager); + // Add a sample fossil to start with + state.add_fossil("sample.py".to_string(), "def hello():\n print('Hello, World!')".to_string()); + (state, Task::none()) + }) } diff --git a/src/mcp.rs b/src/mcp.rs index 0b32e3e..137badb 100644 --- a/src/mcp.rs +++ b/src/mcp.rs @@ -80,6 +80,13 @@ impl FossilEditor { } } + pub fn with_shared_state(state: FossilManager) -> Self { + Self { + tool_router: Self::tool_router(), + state, + } + } + #[tool(description = "Creates a new code fossil in memory with initial content.")] fn create_fossil( &self,