Skip to content

Commit f14d711

Browse files
committed
feat: add WSL Claude detection and unified installation selector
- Auto-detect Claude installations inside WSL distributions - Show WSL Claude in the main installation dropdown alongside native - Add 'Advanced Settings' section under Claude Installation selector - Allow manual shell environment override (Native/WSL/Git Bash) - Remove separate Shell tab in favor of unified UX - Add wsl_distro field to ClaudeInstallation type
1 parent 50f5299 commit f14d711

File tree

5 files changed

+555
-7
lines changed

5 files changed

+555
-7
lines changed

src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/src/claude_binary.rs

Lines changed: 205 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,13 @@ pub struct ClaudeInstallation {
2424
pub path: String,
2525
/// Version string if available
2626
pub version: Option<String>,
27-
/// Source of discovery (e.g., "nvm", "system", "homebrew", "which")
27+
/// Source of discovery (e.g., "nvm", "system", "homebrew", "which", "wsl")
2828
pub source: String,
2929
/// Type of installation
3030
pub installation_type: InstallationType,
31+
/// WSL distribution name (if this is a WSL installation)
32+
#[serde(skip_serializing_if = "Option::is_none")]
33+
pub wsl_distro: Option<String>,
3134
}
3235

3336
/// Main function to find the Claude binary
@@ -204,6 +207,7 @@ fn try_which_command() -> Option<ClaudeInstallation> {
204207
version,
205208
source: "which".to_string(),
206209
installation_type: InstallationType::System,
210+
wsl_distro: None,
207211
})
208212
}
209213
_ => None,
@@ -245,6 +249,7 @@ fn try_which_command() -> Option<ClaudeInstallation> {
245249
version,
246250
source: "where".to_string(),
247251
installation_type: InstallationType::System,
252+
wsl_distro: None,
248253
})
249254
}
250255
_ => None,
@@ -269,6 +274,7 @@ fn find_nvm_installations() -> Vec<ClaudeInstallation> {
269274
version,
270275
source: "nvm-active".to_string(),
271276
installation_type: InstallationType::System,
277+
wsl_distro: None,
272278
});
273279
}
274280
}
@@ -337,6 +343,7 @@ fn find_nvm_installations() -> Vec<ClaudeInstallation> {
337343
version,
338344
source: format!("nvm ({})", node_version),
339345
installation_type: InstallationType::System,
346+
wsl_distro: None,
340347
});
341348
}
342349
}
@@ -407,6 +414,7 @@ fn find_standard_installations() -> Vec<ClaudeInstallation> {
407414
version,
408415
source,
409416
installation_type: InstallationType::System,
417+
wsl_distro: None,
410418
});
411419
}
412420
}
@@ -422,6 +430,7 @@ fn find_standard_installations() -> Vec<ClaudeInstallation> {
422430
version,
423431
source: "PATH".to_string(),
424432
installation_type: InstallationType::System,
433+
wsl_distro: None,
425434
});
426435
}
427436
}
@@ -476,6 +485,7 @@ fn find_standard_installations() -> Vec<ClaudeInstallation> {
476485
version,
477486
source,
478487
installation_type: InstallationType::System,
488+
wsl_distro: None,
479489
});
480490
}
481491
}
@@ -491,13 +501,207 @@ fn find_standard_installations() -> Vec<ClaudeInstallation> {
491501
version,
492502
source: "PATH".to_string(),
493503
installation_type: InstallationType::System,
504+
wsl_distro: None,
505+
});
506+
}
507+
}
508+
509+
// Also check WSL installations
510+
installations.extend(find_wsl_installations());
511+
512+
installations
513+
}
514+
515+
/// Find Claude installations in WSL distributions (Windows only)
516+
#[cfg(windows)]
517+
fn find_wsl_installations() -> Vec<ClaudeInstallation> {
518+
let mut installations = Vec::new();
519+
520+
debug!("Checking for Claude installations in WSL...");
521+
522+
// Get list of WSL distributions
523+
let distros = match get_wsl_distributions() {
524+
Ok(d) => d,
525+
Err(e) => {
526+
debug!("Failed to get WSL distributions: {}", e);
527+
return installations;
528+
}
529+
};
530+
531+
for distro in distros {
532+
debug!("Checking WSL distribution: {}", distro);
533+
534+
// Try to find claude in this distribution
535+
if let Some(claude_path) = find_claude_in_wsl(&distro) {
536+
debug!("Found Claude in WSL {}: {}", distro, claude_path);
537+
538+
// Get version
539+
let version = get_claude_version_in_wsl(&distro, &claude_path);
540+
541+
installations.push(ClaudeInstallation {
542+
path: claude_path,
543+
version,
544+
source: format!("wsl ({})", distro),
545+
installation_type: InstallationType::System,
546+
wsl_distro: Some(distro),
494547
});
495548
}
496549
}
497550

498551
installations
499552
}
500553

554+
/// Get list of WSL distributions
555+
#[cfg(windows)]
556+
fn get_wsl_distributions() -> Result<Vec<String>, String> {
557+
// Run: wsl -l -q (quiet mode, just names)
558+
let output = Command::new("wsl")
559+
.args(["-l", "-q"])
560+
.output()
561+
.map_err(|e| format!("Failed to run wsl -l -q: {}", e))?;
562+
563+
if !output.status.success() {
564+
return Err("wsl -l -q failed".to_string());
565+
}
566+
567+
// WSL output is UTF-16 LE encoded on Windows
568+
let output_str = String::from_utf16_lossy(
569+
&output
570+
.stdout
571+
.chunks(2)
572+
.filter_map(|chunk| {
573+
if chunk.len() == 2 {
574+
Some(u16::from_le_bytes([chunk[0], chunk[1]]))
575+
} else {
576+
None
577+
}
578+
})
579+
.collect::<Vec<u16>>(),
580+
);
581+
582+
let distros: Vec<String> = output_str
583+
.lines()
584+
.map(|s| s.trim().trim_matches('\0').to_string())
585+
.filter(|s| !s.is_empty())
586+
.collect();
587+
588+
debug!("Found WSL distributions: {:?}", distros);
589+
Ok(distros)
590+
}
591+
592+
/// Find Claude binary path in a WSL distribution
593+
#[cfg(windows)]
594+
fn find_claude_in_wsl(distro: &str) -> Option<String> {
595+
// Try 'which claude' in the WSL distribution
596+
let output = Command::new("wsl")
597+
.args(["-d", distro, "--", "which", "claude"])
598+
.output()
599+
.ok()?;
600+
601+
if output.status.success() {
602+
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
603+
if !path.is_empty() && !path.contains("not found") {
604+
return Some(path);
605+
}
606+
}
607+
608+
// Try common paths if 'which' doesn't work
609+
let common_paths = ["/usr/local/bin/claude", "/usr/bin/claude"];
610+
611+
// Also check NVM paths - first get home directory
612+
if let Some(home) = get_wsl_home_dir(distro) {
613+
// Check if there's an NVM installation
614+
let nvm_base = format!("{}/.nvm/versions/node", home);
615+
616+
// List node versions and check for claude
617+
let output = Command::new("wsl")
618+
.args(["-d", distro, "--", "ls", "-1", &nvm_base])
619+
.output();
620+
621+
if let Ok(output) = output {
622+
if output.status.success() {
623+
let versions = String::from_utf8_lossy(&output.stdout);
624+
for version in versions.lines() {
625+
let version = version.trim();
626+
if !version.is_empty() {
627+
let claude_path = format!("{}/{}/bin/claude", nvm_base, version);
628+
// Check if claude exists at this path
629+
let check = Command::new("wsl")
630+
.args(["-d", distro, "--", "test", "-f", &claude_path])
631+
.output();
632+
633+
if let Ok(check) = check {
634+
if check.status.success() {
635+
return Some(claude_path);
636+
}
637+
}
638+
}
639+
}
640+
}
641+
}
642+
643+
// Check ~/.local/bin/claude
644+
let local_claude = format!("{}/.local/bin/claude", home);
645+
let check = Command::new("wsl")
646+
.args(["-d", distro, "--", "test", "-f", &local_claude])
647+
.output();
648+
649+
if let Ok(check) = check {
650+
if check.status.success() {
651+
return Some(local_claude);
652+
}
653+
}
654+
}
655+
656+
// Check common system paths
657+
for path in common_paths {
658+
let check = Command::new("wsl")
659+
.args(["-d", distro, "--", "test", "-f", path])
660+
.output();
661+
662+
if let Ok(check) = check {
663+
if check.status.success() {
664+
return Some(path.to_string());
665+
}
666+
}
667+
}
668+
669+
None
670+
}
671+
672+
/// Get home directory in WSL
673+
#[cfg(windows)]
674+
fn get_wsl_home_dir(distro: &str) -> Option<String> {
675+
let output = Command::new("wsl")
676+
.args(["-d", distro, "--", "echo", "$HOME"])
677+
.output()
678+
.ok()?;
679+
680+
if output.status.success() {
681+
let home = String::from_utf8_lossy(&output.stdout).trim().to_string();
682+
if !home.is_empty() {
683+
return Some(home);
684+
}
685+
}
686+
687+
None
688+
}
689+
690+
/// Get Claude version in WSL
691+
#[cfg(windows)]
692+
fn get_claude_version_in_wsl(distro: &str, claude_path: &str) -> Option<String> {
693+
let output = Command::new("wsl")
694+
.args(["-d", distro, "--", claude_path, "--version"])
695+
.output()
696+
.ok()?;
697+
698+
if output.status.success() {
699+
extract_version_from_output(&output.stdout)
700+
} else {
701+
None
702+
}
703+
}
704+
501705
/// Get Claude version by running --version command
502706
fn get_claude_version(path: &str) -> Result<Option<String>, String> {
503707
match Command::new(path).arg("--version").output() {

0 commit comments

Comments
 (0)