Polish for shipping: robust auto-detect, empty state, real icons, end-user README
- cli_usage::default_command now enumerates WSL distros and probes each for claude before falling back; no more hardcoded -d Ubuntu. - New autodetect_claude_command Tauri command + IPC binding so the UI knows whether claude is reachable. - App.svelte: clear 'Claude Code not found' empty state with install link. - Real icons: scripts/make-icon.py generates a 1024x1024 source.png; runtime produces 32/128/256 PNGs and a multi-resolution .ico. README in icons/ explains how to regen. - README rewritten for friends: install / requirements / troubleshooting on top; build-from-source moved to bottom.
This commit is contained in:
parent
0a960dae2d
commit
9be856d37c
14 changed files with 321 additions and 128 deletions
|
|
@ -67,27 +67,52 @@ pub fn fetch_blocking(command_override: Option<&str>) -> Result<CliUsage> {
|
|||
|
||||
/// Pick a sensible default command line for invoking `claude`.
|
||||
///
|
||||
/// On Windows, `claude` may resolve to a Windows-native install that isn't
|
||||
/// authenticated, while the user's real session lives in WSL. Prefer the
|
||||
/// WSL Ubuntu invocation when a `wsl.exe` is detectable on PATH.
|
||||
/// Order:
|
||||
/// 1. Native `claude` (Windows: `claude.exe` on PATH; Unix: `claude`).
|
||||
/// 2. On Windows: enumerate WSL distros via `wsl.exe -l -q` and probe
|
||||
/// each by running `bash -lc 'command -v claude'`. First hit wins.
|
||||
/// 3. Fallback: bare `claude` (will fail, but at least with a clear error).
|
||||
///
|
||||
/// On Linux/macOS, just `claude`.
|
||||
/// This is called fresh on every `/usage` fetch, but each probe is cheap
|
||||
/// (<200ms typical) and only runs when no override is set.
|
||||
fn default_command() -> CommandBuilder {
|
||||
if cfg!(windows) {
|
||||
// Probe for wsl.exe; if present, run claude through a login bash in
|
||||
// the Ubuntu distro (the most common dev setup, and the user's PATH
|
||||
// is wired through .profile / .bashrc so `claude` resolves).
|
||||
if which_exists("wsl.exe") {
|
||||
let mut c = CommandBuilder::new("wsl.exe");
|
||||
for a in ["-d", "Ubuntu", "bash", "-lc", "claude"] {
|
||||
c.arg(a);
|
||||
}
|
||||
return c;
|
||||
if let Some(parts) = autodetect_command() {
|
||||
let mut c = CommandBuilder::new(&parts[0]);
|
||||
for a in &parts[1..] {
|
||||
c.arg(a);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
CommandBuilder::new("claude")
|
||||
}
|
||||
|
||||
/// Returns the auto-detected argv (program + args) for invoking claude, or
|
||||
/// None if nothing reachable was found.
|
||||
pub fn autodetect_command() -> Option<Vec<String>> {
|
||||
// 1. Native claude.
|
||||
if which_exists("claude") {
|
||||
return Some(vec!["claude".to_string()]);
|
||||
}
|
||||
|
||||
// 2. WSL distros (Windows only).
|
||||
if cfg!(windows) && which_exists("wsl.exe") {
|
||||
for distro in list_wsl_distros() {
|
||||
if probe_claude_in_wsl(&distro) {
|
||||
return Some(vec![
|
||||
"wsl.exe".to_string(),
|
||||
"-d".to_string(),
|
||||
distro,
|
||||
"bash".to_string(),
|
||||
"-lc".to_string(),
|
||||
"claude".to_string(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn which_exists(name: &str) -> bool {
|
||||
use std::process::Command;
|
||||
let probe = if cfg!(windows) { "where" } else { "which" };
|
||||
|
|
@ -98,6 +123,36 @@ fn which_exists(name: &str) -> bool {
|
|||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn list_wsl_distros() -> Vec<String> {
|
||||
use std::process::Command;
|
||||
let Ok(out) = Command::new("wsl.exe").args(["-l", "-q"]).output() else {
|
||||
return Vec::new();
|
||||
};
|
||||
if !out.status.success() {
|
||||
return Vec::new();
|
||||
}
|
||||
// wsl.exe outputs UTF-16LE.
|
||||
let raw_u16: Vec<u16> = out
|
||||
.stdout
|
||||
.chunks_exact(2)
|
||||
.map(|b| u16::from_le_bytes([b[0], b[1]]))
|
||||
.collect();
|
||||
String::from_utf16_lossy(&raw_u16)
|
||||
.lines()
|
||||
.map(|l| l.trim_matches(|c: char| c == '\u{FEFF}' || c.is_whitespace()).to_string())
|
||||
.filter(|l| !l.is_empty())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn probe_claude_in_wsl(distro: &str) -> bool {
|
||||
use std::process::Command;
|
||||
Command::new("wsl.exe")
|
||||
.args(["-d", distro, "bash", "-lc", "command -v claude"])
|
||||
.output()
|
||||
.map(|o| o.status.success() && !o.stdout.is_empty())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Spawn the CLI in a PTY, send `/usage`, capture stdout for `total_timeout`,
|
||||
/// then send `/exit` and return raw bytes (still containing ANSI escapes).
|
||||
fn drive_claude_usage(command_override: Option<&str>, total_timeout: Duration) -> Result<Vec<u8>> {
|
||||
|
|
|
|||
|
|
@ -83,6 +83,13 @@ pub async fn get_cli_usage(
|
|||
Ok(state.cli_usage.read().clone())
|
||||
}
|
||||
|
||||
/// What the auto-detect found. Used by the empty-state UI to tell the
|
||||
/// user whether claude is even reachable.
|
||||
#[tauri::command]
|
||||
pub async fn autodetect_claude_command() -> Result<Option<Vec<String>>, String> {
|
||||
Ok(crate::cli_usage::autodetect_command())
|
||||
}
|
||||
|
||||
/// Force-refresh /usage by spawning the CLI now. Slow (~3-5s); use sparingly.
|
||||
#[tauri::command]
|
||||
pub async fn refresh_cli_usage(
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ pub fn run() {
|
|||
commands::detect_plan_tier,
|
||||
commands::get_cli_usage,
|
||||
commands::refresh_cli_usage,
|
||||
commands::autodetect_claude_command,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue