Compare commits

..

2 Commits

Author SHA1 Message Date
senstella
02ef2098b3 add gui perhaps 2025-10-08 20:26:04 +09:00
senstella
e6c6f94616 iced? 2025-10-08 15:31:39 +09:00
5 changed files with 352 additions and 18 deletions

114
Cargo.lock generated
View File

@@ -1050,7 +1050,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.59.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -1136,6 +1136,12 @@ dependencies = [
"miniz_oxide", "miniz_oxide",
] ]
[[package]]
name = "float_next_after"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8"
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@@ -1222,6 +1228,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"iced", "iced",
"iced_aw",
"imara-diff", "imara-diff",
"nucleo-matcher", "nucleo-matcher",
"rmcp", "rmcp",
@@ -1675,6 +1682,21 @@ dependencies = [
"thiserror 1.0.69", "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]] [[package]]
name = "iced_core" name = "iced_core"
version = "0.13.2" version = "0.13.2"
@@ -1695,6 +1717,15 @@ dependencies = [
"web-time", "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]] [[package]]
name = "iced_futures" name = "iced_futures"
version = "0.13.2" version = "0.13.2"
@@ -1705,6 +1736,7 @@ dependencies = [
"iced_core", "iced_core",
"log", "log",
"rustc-hash 2.1.1", "rustc-hash 2.1.1",
"tokio",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"wasm-timer", "wasm-timer",
] ]
@@ -1735,6 +1767,7 @@ dependencies = [
"iced_core", "iced_core",
"iced_futures", "iced_futures",
"log", "log",
"lyon_path",
"once_cell", "once_cell",
"raw-window-handle", "raw-window-handle",
"rustc-hash 2.1.1", "rustc-hash 2.1.1",
@@ -1798,6 +1831,7 @@ dependencies = [
"iced_glyphon", "iced_glyphon",
"iced_graphics", "iced_graphics",
"log", "log",
"lyon",
"once_cell", "once_cell",
"rustc-hash 2.1.1", "rustc-hash 2.1.1",
"thiserror 1.0.69", "thiserror 1.0.69",
@@ -1885,6 +1919,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.15" version = "1.0.15"
@@ -2048,6 +2091,58 @@ version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" 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]] [[package]]
name = "malloc_buf" name = "malloc_buf"
version = "0.0.6" version = "0.0.6"
@@ -2238,6 +2333,16 @@ dependencies = [
"unicode-segmentation", "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]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@@ -2245,6 +2350,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"libm",
] ]
[[package]] [[package]]
@@ -3114,7 +3220,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys 0.11.0", "linux-raw-sys 0.11.0",
"windows-sys 0.59.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -3581,7 +3687,7 @@ dependencies = [
"getrandom 0.3.3", "getrandom 0.3.3",
"once_cell", "once_cell",
"rustix 1.1.2", "rustix 1.1.2",
"windows-sys 0.59.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -4372,7 +4478,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]

View File

@@ -5,7 +5,8 @@ edition = "2024"
[dependencies] [dependencies]
anyhow = "1.0.100" anyhow = "1.0.100"
iced = "0.13.1" iced = { version = "0.13.1", features = ["tokio"] }
iced_aw = { version = "0.11.0", features = ["tab_bar"] }
imara-diff = "0.2.0" imara-diff = "0.2.0"
nucleo-matcher = "0.3.1" nucleo-matcher = "0.3.1"
rmcp = { version = "0.8.0", features = ["server", "macros", "transport-sse-server", "transport-io", "transport-streamable-http-server", "elicitation", "schemars"] } rmcp = { version = "0.8.0", features = ["server", "macros", "transport-sse-server", "transport-io", "transport-streamable-http-server", "elicitation", "schemars"] }

View File

@@ -0,0 +1,200 @@
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 {
fossil_manager: FossilManager,
tab_editors: HashMap<String, text_editor::Content>,
active_tab: Option<String>,
tab_order: Vec<String>,
}
#[derive(Debug, Clone)]
pub enum Message {
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<String> = 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<String> = 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> {
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()
}
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<Message> {
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()
}

View File

@@ -1,22 +1,32 @@
use crate::mcp::FossilEditor; use crate::{
gui::{update, view, State},
mcp::FossilEditor,
fossil::FossilManager,
};
use iced::Task;
use rmcp::{ServiceExt, transport::stdio}; use rmcp::{ServiceExt, transport::stdio};
use tokio::runtime::Runtime;
use tracing_subscriber::{self, EnvFilter}; use tracing_subscriber::{self, EnvFilter};
mod fossil; mod fossil;
mod gui;
mod matcher; mod matcher;
mod mcp; mod mcp;
#[tokio::main] fn main() -> iced::Result {
async fn main() {
tracing_subscriber::fmt() tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env().add_directive(tracing::Level::DEBUG.into())) .with_env_filter(EnvFilter::from_default_env().add_directive(tracing::Level::DEBUG.into()))
.with_writer(std::io::stderr) .with_writer(std::io::stderr)
.with_ansi(false) .with_ansi(false)
.init(); .init();
tracing::info!("Starting MCP server"); 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 let service = editor
.serve(stdio()) .serve(stdio())
.await .await
@@ -26,4 +36,14 @@ async fn main() {
.unwrap(); .unwrap();
service.waiting().await.unwrap(); service.waiting().await.unwrap();
});
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())
})
} }

View File

@@ -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.")] #[tool(description = "Creates a new code fossil in memory with initial content.")]
fn create_fossil( fn create_fossil(
&self, &self,