use crate::platform;
use crate::tools::probe_app::{classify_running_app, AppKind};
use rmcp::model::{CallToolResult, Content};
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize)]
pub struct ListWindowsParams {
pub app_name: Option<String>,
}
pub fn list_windows(params: ListWindowsParams) -> CallToolResult {
let windows = if let Some(app_name) = params.app_name {
match platform::find_windows_by_app(&app_name) {
Ok(w) => w,
Err(e) => return CallToolResult::error(vec![Content::text(e)]),
}
} else {
match platform::list_windows() {
Ok(w) => w,
Err(e) => return CallToolResult::error(vec![Content::text(e)]),
}
};
match serde_json::to_string_pretty(&windows) {
Ok(json) => CallToolResult::success(vec![Content::text(json)]),
Err(e) => CallToolResult::error(vec![Content::text(format!(
"Failed to serialize windows: {}",
e
))]),
}
}
#[derive(Debug, Deserialize)]
pub struct ListAppsParams {
pub app_name: Option<String>,
pub user_apps_only: Option<bool>,
}
pub fn list_apps(params: ListAppsParams) -> CallToolResult {
let mut apps = platform::list_apps();
if let Some(ref name) = params.app_name {
let needle = name.to_lowercase();
apps.retain(|app| app.name.to_lowercase().contains(&needle));
}
if params.user_apps_only.unwrap_or(false) {
apps.retain(|app| app.is_user_app);
}
match serde_json::to_string_pretty(&apps) {
Ok(json) => CallToolResult::success(vec![Content::text(json)]),
Err(e) => CallToolResult::error(vec![Content::text(format!(
"Failed to serialize apps: {}",
e
))]),
}
}
#[derive(Debug, Deserialize)]
pub struct LaunchAppParams {
pub app_name: String,
pub args: Option<Vec<String>>,
pub background: Option<bool>,
}
pub fn launch_app(params: LaunchAppParams) -> CallToolResult {
let args = params.args.as_deref().unwrap_or(&[]);
let background = params.background.unwrap_or(false);
if !args.is_empty() && platform::is_app_running(¶ms.app_name) {
return CallToolResult::error(vec![Content::text(format!(
"'{}' is already running. CLI args only apply on fresh launch. Use quit_app to quit it first, then retry.",
params.app_name
))]);
}
match platform::launch_app(¶ms.app_name, args, background) {
Ok(()) => CallToolResult::success(vec![Content::text(format!(
"Launched '{}'",
params.app_name
))]),
Err(e) => CallToolResult::error(vec![Content::text(e)]),
}
}
#[derive(Debug, Deserialize)]
pub struct QuitAppParams {
pub app_name: String,
pub force: Option<bool>,
}
pub fn quit_app(params: QuitAppParams) -> CallToolResult {
let force = params.force.unwrap_or(false);
match platform::quit_app(¶ms.app_name, force) {
Ok(count) => {
let method = if force { "Force-killed" } else { "Quit" };
CallToolResult::success(vec![Content::text(format!(
"{} '{}' ({} instance{})",
method,
params.app_name,
count,
if count == 1 { "" } else { "s" }
))])
}
Err(e) => CallToolResult::error(vec![Content::text(e)]),
}
}
#[derive(Debug, Deserialize)]
pub struct FocusWindowParams {
pub window_id: Option<u32>,
pub app_name: Option<String>,
pub pid: Option<i32>,
}
#[derive(Debug, Serialize)]
pub struct FocusWindowResult {
pub app_name: String,
pub pid: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub bundle_id: Option<String>,
pub kind: AppKind,
}
fn focused(result: FocusWindowResult) -> CallToolResult {
match serde_json::to_string(&result) {
Ok(json) => CallToolResult::success(vec![Content::text(json)]),
Err(e) => CallToolResult::error(vec![Content::text(format!(
"Failed to serialize focus_window result: {}",
e
))]),
}
}
fn error(msg: impl Into<String>) -> CallToolResult {
CallToolResult::error(vec![Content::text(msg.into())])
}
fn build_result_for_pid(pid: i32, fallback_name: Option<&str>) -> FocusWindowResult {
if let Some(app) = platform::list_apps().into_iter().find(|a| a.pid == pid) {
let kind = classify_running_app(app.pid, app.bundle_id.as_deref(), &app.name);
FocusWindowResult {
app_name: app.name,
pid: app.pid,
bundle_id: app.bundle_id,
kind,
}
} else {
let name = fallback_name.unwrap_or("").to_string();
let kind = if name.is_empty() {
AppKind::Native
} else {
classify_running_app(pid, None, &name)
};
FocusWindowResult {
app_name: name,
pid,
bundle_id: None,
kind,
}
}
}
pub fn focus_window(params: FocusWindowParams) -> CallToolResult {
if let Some(app_name) = params.app_name {
focus_by_app_name(&app_name)
} else if let Some(pid) = params.pid {
focus_by_pid(pid)
} else if let Some(window_id) = params.window_id {
focus_by_window_id(window_id)
} else {
error("Provide one of: window_id, app_name, or pid")
}
}
fn focus_by_app_name(app_name: &str) -> CallToolResult {
if platform::activate_app(app_name) {
let pid = platform::find_windows_by_app(app_name)
.ok()
.and_then(|w| w.first().map(|win| win.owner_pid as i32));
if let Some(pid) = pid {
platform::raise_windows(pid);
return focused(build_result_for_pid(pid, Some(app_name)));
}
return focused(FocusWindowResult {
app_name: app_name.to_string(),
pid: 0,
bundle_id: None,
kind: AppKind::Native,
});
}
let pid = platform::find_windows_by_app(app_name)
.ok()
.and_then(|w| w.first().map(|win| win.owner_pid as i32));
if let Some(pid) = pid {
if platform::activate_app_by_pid(pid) {
platform::raise_windows(pid);
return focused(build_result_for_pid(pid, Some(app_name)));
}
}
error(format!(
"No app found matching '{}'. Use list_apps to find the correct app name.",
app_name
))
}
fn focus_by_pid(pid: i32) -> CallToolResult {
if platform::activate_app_by_pid(pid) {
platform::raise_windows(pid);
focused(build_result_for_pid(pid, None))
} else {
error(format!(
"No app found with PID {}. Use list_apps to find running apps.",
pid
))
}
}
fn focus_by_window_id(window_id: u32) -> CallToolResult {
match platform::find_window_by_id(window_id) {
Ok(Some(window)) => {
let pid = window.owner_pid as i32;
let owner_name = window.owner_name.clone();
if platform::activate_app_by_pid(pid) {
platform::raise_windows(pid);
focused(build_result_for_pid(pid, Some(&owner_name)))
} else {
error(format!(
"Found window {} but failed to activate its owning app (PID {}).",
window_id, window.owner_pid
))
}
}
Ok(None) => error(format!(
"Window {} not found. Use list_windows to find available windows.",
window_id
)),
Err(e) => error(e),
}
}
#[cfg(test)]
mod focus_window_tests {
use super::*;
#[test]
fn result_serializes_structured_fields() {
let result = FocusWindowResult {
app_name: "Signal".to_string(),
pid: 16024,
bundle_id: Some("org.whispersystems.signal-desktop".to_string()),
kind: AppKind::ElectronApp,
};
let json: serde_json::Value = serde_json::to_value(&result).unwrap();
assert_eq!(json["app_name"], "Signal");
assert_eq!(json["pid"], 16024);
assert_eq!(json["bundle_id"], "org.whispersystems.signal-desktop");
assert_eq!(json["kind"], "ElectronApp");
}
#[test]
fn result_omits_missing_bundle_id() {
let result = FocusWindowResult {
app_name: "Notepad".to_string(),
pid: 42,
bundle_id: None,
kind: AppKind::Native,
};
let json: serde_json::Value = serde_json::to_value(&result).unwrap();
assert!(!json.as_object().unwrap().contains_key("bundle_id"));
assert_eq!(json["kind"], "Native");
}
}