use std::ffi::OsString;
use std::path::Path;
use anyhow::Result;
pub fn run(_args: &[OsString]) -> Result<()> {
let segment = render_segment()?;
println!("{segment}");
Ok(())
}
pub fn render_segment() -> Result<String> {
let host = short_hostname()?;
let segment = format_segment(host, is_personal_machine());
Ok(segment)
}
pub fn format_segment(host: String, personal: bool) -> String {
if !personal {
return host;
}
match std::env::var_os("CLAUDE_CONFIG_DIR") {
None => host,
Some(dir) => {
let base = Path::new(&dir)
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
let label = strip_claude_prefix(&base);
if label != base {
format!("{label}@{host}")
} else {
host
}
}
}
}
#[allow(dead_code)]
pub fn current_profile() -> Result<String> {
let dir = std::env::var_os("CLAUDE_CONFIG_DIR");
match dir {
None => Ok("unknown".to_owned()),
Some(path) => {
let p = Path::new(&path);
match p.file_name() {
Some(name) => {
let base = name.to_string_lossy();
let label = strip_claude_prefix(&base);
Ok(label.to_owned())
}
None => {
Ok(path.to_string_lossy().into_owned())
}
}
}
}
}
pub fn strip_claude_prefix(s: &str) -> &str {
s.strip_prefix(".claude.").unwrap_or(s)
}
pub const HOST_REPLACE_ENV: &str = "CSM_HOST_REPLACE";
pub fn short_hostname() -> Result<String> {
let raw = hostname()?;
Ok(apply_host_replace(
raw,
std::env::var(HOST_REPLACE_ENV).ok().as_deref(),
))
}
pub fn apply_host_replace(s: String, rule: Option<&str>) -> String {
let Some(rule) = rule.filter(|r| !r.is_empty()) else {
return s;
};
let Some((find, replace)) = rule.split_once('/') else {
return s;
};
if find.is_empty() {
return s;
}
let lower_s = s.to_ascii_lowercase();
let lower_find = find.to_ascii_lowercase();
match lower_s.find(&lower_find) {
Some(idx) => format!("{}{}{}", &s[..idx], replace, &s[idx + find.len()..]),
None => s,
}
}
pub fn hostname() -> Result<String> {
hostname_impl()
}
#[cfg(unix)]
fn hostname_impl() -> Result<String> {
use nix::unistd::gethostname;
let name = gethostname()?;
let raw = name.to_string_lossy();
let short = raw.split('.').next().unwrap_or(&raw);
Ok(short.to_owned())
}
#[cfg(windows)]
fn hostname_impl() -> Result<String> {
use std::os::windows::ffi::OsStringExt;
use windows_sys::Win32::System::SystemInformation::{ComputerNameNetBIOS, GetComputerNameExW};
let mut size: u32 = 0;
unsafe { GetComputerNameExW(ComputerNameNetBIOS, std::ptr::null_mut(), &mut size) };
let mut buf: Vec<u16> = vec![0u16; size as usize];
let ok = unsafe { GetComputerNameExW(ComputerNameNetBIOS, buf.as_mut_ptr(), &mut size) };
if ok == 0 {
anyhow::bail!("GetComputerNameExW failed");
}
buf.truncate(size as usize);
Ok(OsString::from_wide(&buf).to_string_lossy().into_owned())
}
pub fn is_personal_machine() -> bool {
match std::env::var("IS_PERSONAL_MACHINE") {
Ok(v) => matches!(v.trim(), "1" | "true" | "True" | "TRUE" | "yes"),
Err(_) => true,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
fn profile_with_dir(dir: &str) -> String {
let _guard = ENV_LOCK.lock().unwrap();
std::env::set_var("CLAUDE_CONFIG_DIR", dir);
let result = current_profile().expect("current_profile() must not fail");
std::env::remove_var("CLAUDE_CONFIG_DIR");
result
}
fn profile_with_no_var() -> String {
let _guard = ENV_LOCK.lock().unwrap();
std::env::remove_var("CLAUDE_CONFIG_DIR");
current_profile().expect("current_profile() must not fail when var is absent")
}
fn segment_personal(dir: &str) -> String {
let _guard = ENV_LOCK.lock().unwrap();
std::env::set_var("CLAUDE_CONFIG_DIR", dir);
let result = format_segment("Laptop".to_owned(), true);
std::env::remove_var("CLAUDE_CONFIG_DIR");
result
}
fn segment_personal_no_dir() -> String {
let _guard = ENV_LOCK.lock().unwrap();
std::env::remove_var("CLAUDE_CONFIG_DIR");
format_segment("Laptop".to_owned(), true)
}
#[test]
fn strip_prefix_personal() {
assert_eq!(strip_claude_prefix(".claude.home"), "home");
}
#[test]
fn strip_prefix_work() {
assert_eq!(strip_claude_prefix(".claude.work"), "work");
}
#[test]
fn strip_prefix_no_prefix() {
assert_eq!(strip_claude_prefix("myprofile"), "myprofile");
}
#[test]
fn strip_prefix_degenerate_empty_suffix() {
assert_eq!(strip_claude_prefix(".claude."), "");
}
#[test]
fn strip_prefix_bare_claude_no_dot() {
assert_eq!(strip_claude_prefix(".claude"), ".claude");
}
#[test]
fn leaf_personal_path() {
assert_eq!(profile_with_dir("/home/you/.claude.home"), "home");
}
#[test]
fn leaf_work_path() {
assert_eq!(profile_with_dir("/home/you/.claude.work"), "work");
}
#[test]
fn leaf_macos_style_path() {
assert_eq!(profile_with_dir("/Users/example/.claude.home"), "home");
}
#[test]
fn leaf_bare_profile_no_dot_prefix() {
assert_eq!(profile_with_dir("myprofile"), "myprofile");
}
#[test]
fn leaf_trailing_slash_stripped() {
assert_eq!(profile_with_dir("/home/you/.claude.home/"), "home");
}
#[test]
fn absent_env_var_returns_unknown() {
assert_eq!(profile_with_no_var(), "unknown");
}
#[test]
fn leaf_windows_style_path_does_not_panic() {
let result = profile_with_dir(r"C:\Users\example\.claude.home");
assert!(!result.is_empty());
}
const PREFIX_RULE: Option<&str> = Some("Acme-/");
#[test]
fn replace_prefix_basic() {
assert_eq!(
apply_host_replace("Acme-Laptop".to_owned(), PREFIX_RULE),
"Laptop"
);
}
#[test]
fn replace_prefix_workstation() {
assert_eq!(
apply_host_replace("Acme-Workstation".to_owned(), PREFIX_RULE),
"Workstation"
);
}
#[test]
fn replace_prefix_case_insensitive() {
assert_eq!(
apply_host_replace("ACME-WINDOWS".to_owned(), PREFIX_RULE),
"WINDOWS"
);
}
#[test]
fn replace_no_match_unchanged() {
assert_eq!(
apply_host_replace("myhostname".to_owned(), PREFIX_RULE),
"myhostname"
);
}
#[test]
fn replace_no_rule_unchanged() {
assert_eq!(
apply_host_replace("Acme-Laptop".to_owned(), None),
"Acme-Laptop"
);
assert_eq!(
apply_host_replace("Acme-Laptop".to_owned(), Some("")),
"Acme-Laptop"
);
}
#[test]
fn replace_short_string_no_panic() {
assert_eq!(apply_host_replace("abc".to_owned(), PREFIX_RULE), "abc");
}
#[test]
fn replace_malformed_rule_unchanged() {
assert_eq!(
apply_host_replace("Acme-Laptop".to_owned(), Some("noseparator")),
"Acme-Laptop"
);
}
#[test]
fn segment_personal_with_home_dir() {
let seg = segment_personal("/Users/example/.claude.home");
assert_eq!(seg, "home@Laptop");
}
#[test]
fn segment_personal_with_work_dir() {
let seg = segment_personal("/Users/example/.claude.work");
assert_eq!(seg, "work@Laptop");
}
#[test]
fn segment_personal_with_bare_dir_no_prefix() {
let seg = segment_personal("/Users/example/.claude");
assert_eq!(seg, "Laptop");
}
#[test]
fn segment_personal_no_claude_config_dir() {
let seg = segment_personal_no_dir();
assert_eq!(seg, "Laptop");
}
#[test]
fn segment_toss_machine_ignores_dir() {
let _guard = ENV_LOCK.lock().unwrap();
std::env::set_var("CLAUDE_CONFIG_DIR", "/some/.claude.home");
let seg = format_segment("Laptop".to_owned(), false );
std::env::remove_var("CLAUDE_CONFIG_DIR");
assert_eq!(seg, "Laptop");
}
#[test]
fn segment_workstation_personal() {
let _guard = ENV_LOCK.lock().unwrap();
std::env::set_var("CLAUDE_CONFIG_DIR", "/Users/example/.claude.work");
let host = apply_host_replace("Acme-Workstation".to_owned(), Some("Acme-/"));
let seg = format_segment(host, true);
std::env::remove_var("CLAUDE_CONFIG_DIR");
assert_eq!(seg, "work@Workstation");
}
#[test]
fn segment_windows_personal() {
let _guard = ENV_LOCK.lock().unwrap();
std::env::set_var("CLAUDE_CONFIG_DIR", r"C:\Users\example\.claude.home");
let host = apply_host_replace("ACME-WINDOWS".to_owned(), Some("Acme-/"));
let seg = format_segment(host, true);
std::env::remove_var("CLAUDE_CONFIG_DIR");
assert!(!seg.is_empty());
}
#[test]
fn short_hostname_returns_nonempty() {
let h = short_hostname().expect("short_hostname() must not error");
assert!(!h.is_empty());
}
#[test]
fn short_hostname_no_domain_suffix() {
let h = short_hostname().unwrap();
assert!(
!h.contains('.') || h.starts_with('.'),
"short hostname should have no interior FQDN dot: {h}"
);
}
#[test]
fn short_hostname_applies_env_rule() {
let _guard = ENV_LOCK.lock().unwrap();
let raw = hostname().unwrap();
if let Some(first) = raw.chars().next() {
std::env::set_var(HOST_REPLACE_ENV, format!("{first}/"));
let h = short_hostname().unwrap();
std::env::remove_var(HOST_REPLACE_ENV);
assert!(
h.len() < raw.len(),
"rule should have removed one char: {raw} → {h}"
);
}
}
#[test]
fn short_hostname_no_rule_is_raw() {
let _guard = ENV_LOCK.lock().unwrap();
std::env::remove_var(HOST_REPLACE_ENV);
let h = short_hostname().unwrap();
let raw = hostname().unwrap();
assert_eq!(h, raw, "no rule → hostname unchanged");
}
#[test]
fn hostname_returns_nonempty_string() {
let h = hostname().expect("hostname() must not error");
assert!(!h.is_empty(), "hostname must be non-empty");
assert!(
!h.contains('\n'),
"hostname must not contain a newline, got {h:?}"
);
}
#[test]
fn hostname_no_domain_suffix() {
let h = hostname().unwrap();
assert!(!h.starts_with('.'), "hostname must not start with a dot");
assert!(!h.ends_with('.'), "hostname must not end with a dot");
if let Some(first) = h.split('.').next() {
assert!(!first.is_empty(), "first hostname label must be non-empty");
}
}
#[test]
fn run_does_not_panic_or_error() {
let _guard = ENV_LOCK.lock().unwrap();
std::env::set_var("CLAUDE_CONFIG_DIR", "/tmp/.claude.test");
let result = run(&[]);
std::env::remove_var("CLAUDE_CONFIG_DIR");
assert!(
result.is_ok(),
"run() returned Err: {:?}",
result.unwrap_err()
);
}
#[test]
fn render_segment_returns_nonempty() {
let seg = render_segment().expect("render_segment must not error");
assert!(!seg.is_empty());
}
#[test]
fn personal_gate_env_explicit_false() {
let _guard = ENV_LOCK.lock().unwrap();
std::env::set_var("IS_PERSONAL_MACHINE", "0");
assert!(!is_personal_machine(), "IS_PERSONAL_MACHINE=0 → false");
std::env::set_var("IS_PERSONAL_MACHINE", "false");
assert!(!is_personal_machine(), "IS_PERSONAL_MACHINE=false → false");
std::env::remove_var("IS_PERSONAL_MACHINE");
}
#[test]
fn personal_gate_env_explicit_true() {
let _guard = ENV_LOCK.lock().unwrap();
std::env::set_var("IS_PERSONAL_MACHINE", "1");
assert!(is_personal_machine(), "IS_PERSONAL_MACHINE=1 → true");
std::env::remove_var("IS_PERSONAL_MACHINE");
}
#[test]
fn personal_gate_unset_delegates_true() {
let _guard = ENV_LOCK.lock().unwrap();
std::env::remove_var("IS_PERSONAL_MACHINE");
assert!(is_personal_machine());
}
}