use std::path::PathBuf;
use eframe::egui::{self, CornerRadius, Frame, Margin, RichText, Stroke};
use super::components::widgets;
use crate::theme::Palette;
const GUIDE: &str = "https://manimovassagh.github.io/Ferrodock/docs/cli-and-mcp";
fn invocation(name: &str) -> Option<String> {
let file = if cfg!(windows) {
format!("{name}.exe")
} else {
name.to_owned()
};
let beside: PathBuf = std::env::current_exe().ok()?.parent()?.join(&file);
let on_path = std::env::var_os("PATH")
.is_some_and(|path| std::env::split_paths(&path).any(|dir| dir.join(&file).is_file()));
if on_path {
return Some(name.to_owned());
}
if !beside.is_file() {
return None;
}
let full = beside.display().to_string();
Some(if full.contains(' ') {
format!("\"{full}\"")
} else {
full
})
}
enum Setup {
Command(String),
Config { file: &'static str, json: String },
}
struct Tool {
name: &'static str,
setup: fn(&str) -> Setup,
}
const TOOLS: &[Tool] = &[
Tool {
name: "Claude Code",
setup: |server| Setup::Command(format!("claude mcp add ferrodock -- {server}")),
},
Tool {
name: "Codex",
setup: |server| Setup::Command(format!("codex mcp add ferrodock -- {server}")),
},
Tool {
name: "GitHub Copilot",
setup: |server| {
let entry = serde_json::json!({ "name": "ferrodock", "command": unquote(server) });
Setup::Command(format!("code --add-mcp '{entry}'"))
},
},
Tool {
name: "Gemini CLI",
setup: |server| Setup::Command(format!("gemini mcp add ferrodock {server}")),
},
Tool {
name: "Cursor",
setup: |server| Setup::Config {
file: "~/.cursor/mcp.json",
json: mcp_config(server),
},
},
Tool {
name: "Windsurf",
setup: |server| Setup::Config {
file: "~/.codeium/windsurf/mcp_config.json",
json: mcp_config(server),
},
},
Tool {
name: "Claude Desktop",
setup: |server| Setup::Config {
file: if cfg!(windows) {
r"%APPDATA%\Claude\claude_desktop_config.json"
} else {
"~/Library/Application Support/Claude/claude_desktop_config.json"
},
json: mcp_config(server),
},
},
];
fn unquote(command: &str) -> &str {
command.trim_matches('"')
}
fn mcp_config(command: &str) -> String {
let config = serde_json::json!({
"mcpServers": { "ferrodock": { "command": unquote(command) } }
});
serde_json::to_string_pretty(&config).unwrap_or_default()
}
fn command_row(ui: &mut egui::Ui, p: &Palette, command: &str) {
Frame::new()
.fill(p.bg)
.stroke(Stroke::new(1.0, p.border))
.corner_radius(CornerRadius::same(7))
.inner_margin(Margin::symmetric(10, 6))
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.label(RichText::new(command).monospace().color(p.text));
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.small_button("Copy").clicked() {
ui.ctx().copy_text(command.to_owned());
}
});
});
});
}
fn heading(ui: &mut egui::Ui, p: &Palette, text: &str) {
ui.label(RichText::new(text).size(11.0).color(p.muted));
ui.add_space(4.0);
}
pub fn show(ui: &mut egui::Ui, p: &Palette) -> bool {
let mut close = false;
let modal = egui::Modal::new(egui::Id::unique("ferrodock-integrations"))
.frame(
Frame::new()
.fill(p.sidebar)
.stroke(Stroke::new(1.0, p.border))
.corner_radius(CornerRadius::same(10))
.inner_margin(Margin::same(20)),
)
.show(ui.ctx(), |ui| {
ui.set_width(480.0);
ui.label(
RichText::new("Ferrodock in your terminal and AI tools")
.size(18.0)
.strong()
.color(p.text),
);
ui.add_space(4.0);
ui.label(
RichText::new(
"Two programs come with Ferrodock, using the same engine as the app.",
)
.color(p.muted),
);
ui.add_space(14.0);
let fdock = invocation("fdock");
let mcp = invocation("ferrodock-mcp");
heading(ui, p, "COMMAND LINE: FDOCK");
ui.label(
RichText::new(
"Containers, logs, Quick start and more from a terminal. Add --json for \
scripts.",
)
.color(p.text),
);
ui.add_space(6.0);
let run = fdock.as_deref().unwrap_or("fdock");
for args in ["ps", "quickstart postgres", "logs api -f"] {
command_row(ui, p, &format!("{run} {args}"));
ui.add_space(4.0);
}
ui.add_space(12.0);
heading(ui, p, "AI TOOLS: FERRODOCK-MCP");
ui.label(
RichText::new(
"An MCP server, so AI coding tools can see your containers and start, stop \
or run them for you. Add --read-only to let them look without changing \
anything.",
)
.color(p.text),
);
ui.add_space(6.0);
let server = mcp.as_deref().unwrap_or("ferrodock-mcp");
let picked_id = egui::Id::unique("ferrodock-integrations-tool");
let mut picked = ui
.ctx()
.data(|d| d.get_temp::<usize>(picked_id))
.unwrap_or(0)
.min(TOOLS.len() - 1);
ui.horizontal_wrapped(|ui| {
for (i, tool) in TOOLS.iter().enumerate() {
if ui.selectable_label(i == picked, tool.name).clicked() {
picked = i;
}
}
});
ui.ctx().data_mut(|d| d.insert_temp(picked_id, picked));
ui.add_space(6.0);
match (TOOLS[picked].setup)(server) {
Setup::Command(command) => command_row(ui, p, &command),
Setup::Config { file, json } => {
ui.label(
RichText::new(format!("Add this to {file}:"))
.size(12.0)
.color(p.muted),
);
ui.add_space(2.0);
command_row(ui, p, &json);
}
}
if fdock.is_none() || mcp.is_none() {
ui.add_space(10.0);
ui.label(
RichText::new(
"These programs aren't next to this copy of Ferrodock. They come with \
every download, or install them with \
cargo install ferrodock-cli ferrodock-mcp.",
)
.size(12.0)
.color(p.warning),
);
}
ui.add_space(16.0);
ui.horizontal(|ui| {
ui.hyperlink_to("Read the guide", GUIDE);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
close = widgets::primary_button(ui, "Done").clicked();
});
});
});
!(close || modal.should_close())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn writes_a_json_config() {
let json = mcp_config("\"C:\\Program Files\\Ferrodock\\ferrodock-mcp.exe\"");
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(
value["mcpServers"]["ferrodock"]["command"],
"C:\\Program Files\\Ferrodock\\ferrodock-mcp.exe"
);
let value: serde_json::Value = serde_json::from_str(&mcp_config("ferrodock-mcp")).unwrap();
assert_eq!(value["mcpServers"]["ferrodock"]["command"], "ferrodock-mcp");
}
#[test]
fn every_tool_has_a_setup() {
for tool in TOOLS {
match (tool.setup)("\"C:\\Program Files\\ferrodock-mcp.exe\"") {
Setup::Command(command) => {
assert!(command.contains("ferrodock"), "{}", tool.name);
if let Some(json) = command.split('\'').nth(1) {
let entry: serde_json::Value = serde_json::from_str(json).unwrap();
assert_eq!(entry["command"], "C:\\Program Files\\ferrodock-mcp.exe");
}
}
Setup::Config { file, json } => {
assert!(file.ends_with(".json"), "{}", tool.name);
serde_json::from_str::<serde_json::Value>(&json).unwrap();
}
}
}
}
}