use std::env;
use std::process::Command;
#[derive(Debug, Clone)]
pub struct IdeInfo {
pub name: String,
pub display_name: String,
}
pub mod ide_definitions {
use super::IdeInfo;
pub fn vscode() -> IdeInfo {
IdeInfo {
name: "vscode".to_string(),
display_name: "VS Code".to_string(),
}
}
pub fn cursor() -> IdeInfo {
IdeInfo {
name: "cursor".to_string(),
display_name: "Cursor".to_string(),
}
}
pub fn codespaces() -> IdeInfo {
IdeInfo {
name: "codespaces".to_string(),
display_name: "GitHub Codespaces".to_string(),
}
}
pub fn vscodefork() -> IdeInfo {
IdeInfo {
name: "vscodefork".to_string(),
display_name: "IDE".to_string(),
}
}
pub fn windsurf() -> IdeInfo {
IdeInfo {
name: "windsurf".to_string(),
display_name: "Windsurf".to_string(),
}
}
pub fn zed() -> IdeInfo {
IdeInfo {
name: "zed".to_string(),
display_name: "Zed".to_string(),
}
}
}
pub fn detect_ide_from_env() -> Option<IdeInfo> {
if env::var("CURSOR_TRACE_ID").is_ok() {
return Some(ide_definitions::cursor());
}
if env::var("CODESPACES").is_ok() {
return Some(ide_definitions::codespaces());
}
if env::var("WINDSURF_TRACE_ID").is_ok() {
return Some(ide_definitions::windsurf());
}
if env::var("ZED_TERM").is_ok() {
return Some(ide_definitions::zed());
}
if env::var("TERM_PROGRAM").ok().as_deref() == Some("vscode") {
return Some(ide_definitions::vscode());
}
None
}
fn verify_vscode(ide: IdeInfo, command: &str) -> IdeInfo {
if ide.name != "vscode" {
return ide;
}
let cmd_lower = command.to_lowercase();
if cmd_lower.contains("code") || cmd_lower.is_empty() {
ide_definitions::vscode()
} else {
ide_definitions::vscodefork()
}
}
pub fn detect_ide(process_info: Option<&IdeProcessInfo>) -> Option<IdeInfo> {
if env::var("TERM_PROGRAM").ok().as_deref() != Some("vscode") {
return None;
}
let ide = detect_ide_from_env()?;
if let Some(info) = process_info {
Some(verify_vscode(ide, &info.command))
} else {
Some(ide)
}
}
#[derive(Debug, Clone)]
pub struct IdeProcessInfo {
pub pid: u32,
pub command: String,
}
#[cfg(unix)]
pub async fn get_ide_process_info() -> Option<IdeProcessInfo> {
const MAX_TRAVERSAL_DEPTH: usize = 32;
let shells = ["zsh", "bash", "sh", "tcsh", "csh", "ksh", "fish", "dash"];
let mut current_pid = std::process::id();
for _ in 0..MAX_TRAVERSAL_DEPTH {
if let Some((parent_pid, name, _command)) = get_process_info(current_pid) {
let is_shell = shells.iter().any(|&s| name == s);
if is_shell {
let mut ide_pid = parent_pid;
if let Some((grandparent_pid, _, _)) = get_process_info(parent_pid)
&& grandparent_pid > 1
{
ide_pid = grandparent_pid;
}
if let Some((_, _, ide_command)) = get_process_info(ide_pid) {
return Some(IdeProcessInfo {
pid: ide_pid,
command: ide_command,
});
}
return Some(IdeProcessInfo {
pid: ide_pid,
command: String::new(),
});
}
if parent_pid <= 1 {
break;
}
current_pid = parent_pid;
} else {
break;
}
}
get_process_info(current_pid).map(|(_, _, command)| IdeProcessInfo {
pid: current_pid,
command,
})
}
#[cfg(unix)]
fn get_process_info(pid: u32) -> Option<(u32, String, String)> {
let output = Command::new("ps")
.args(["-o", "ppid=,command=", "-p", &pid.to_string()])
.output()
.ok()?;
let stdout = String::from_utf8_lossy(&output.stdout);
let trimmed = stdout.trim();
if trimmed.is_empty() {
return None;
}
let parts: Vec<&str> = trimmed.splitn(2, char::is_whitespace).collect();
if parts.is_empty() {
return None;
}
let parent_pid: u32 = parts[0].trim().parse().unwrap_or(1);
let full_command = parts.get(1).map(|s| s.trim()).unwrap_or("");
let process_name = full_command
.split_whitespace()
.next()
.map(|s| {
std::path::Path::new(s)
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default()
})
.unwrap_or_default();
Some((parent_pid, process_name, full_command.to_string()))
}
#[cfg(windows)]
pub async fn get_ide_process_info() -> Option<IdeProcessInfo> {
let output = Command::new("powershell")
.args([
"-Command",
"Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -eq $PID } | Select-Object ParentProcessId | ConvertTo-Json"
])
.output()
.ok()?;
let stdout = String::from_utf8_lossy(&output.stdout);
Some(IdeProcessInfo {
pid: std::process::id(),
command: String::new(),
})
}
#[cfg(windows)]
fn get_process_info(_pid: u32) -> Option<(u32, String, String)> {
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_ide_from_env_vscode() {
let _ = detect_ide_from_env();
}
#[test]
fn test_ide_definitions() {
let vscode = ide_definitions::vscode();
assert_eq!(vscode.name, "vscode");
assert_eq!(vscode.display_name, "VS Code");
let cursor = ide_definitions::cursor();
assert_eq!(cursor.name, "cursor");
}
}