use std::io::Write;
use std::path::Path;
fn instant_prompt_block(zshrc_display: &str) -> String {
format!(
"# Enable Powerlevel10k instant prompt. Should stay close to the top of {zshrc_display}.
# Initialization code that may require console input (password prompts, [y/n]
# confirmations, etc.) must go above this block; everything else may go below.
if [[ -r \"${{XDG_CACHE_HOME:-$HOME/.cache}}/p10k-instant-prompt-${{(%):-%n}}.zsh\" ]]; then
source \"${{XDG_CACHE_HOME:-$HOME/.cache}}/p10k-instant-prompt-${{(%):-%n}}.zsh\"
fi"
)
}
fn config_source_block(cfg_display: &str) -> String {
format!(
"
# To customize prompt, run `p10k configure` or edit {cfg_display}.
[[ ! -f {cfg_display} ]] || source {cfg_display}"
)
}
pub fn check_integration(zshrc_content: &str, cfg_path: &str) -> (bool, bool) {
let mut cfg_targets: Vec<String> = vec![
cfg_path.to_string(),
format!("'{cfg_path}'"),
format!("\"{cfg_path}\""),
];
if let Ok(home) = std::env::var("HOME") {
if let Some(rest) = cfg_path.strip_prefix(&format!("{home}/")) {
cfg_targets.push(format!("~/{rest}"));
}
}
cfg_targets.extend(
[
"${ZDOTDIR:-~}/.p10k.zsh",
"${ZDOTDIR:-$HOME}/.p10k.zsh",
"\"${ZDOTDIR:-$HOME}/.p10k.zsh\"",
"$ZDOTDIR/.p10k.zsh",
"\"$ZDOTDIR/.p10k.zsh\"",
"$POWERLEVEL9K_CONFIG_FILE",
"\"$POWERLEVEL9K_CONFIG_FILE\"",
]
.iter()
.map(|s| s.to_string()),
);
let ip = "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh";
let ip_targets = [ip.to_string(), format!("\"{ip}\"")];
let mut has_cfg = false;
let mut has_ip = false;
for raw in zshrc_content.lines() {
let line = raw.trim_start();
if line.starts_with('#') {
continue; }
let Some(src_at) = find_source_word(line) else { continue };
let after = line[src_at + "source".len()..].trim_start();
let target = after.split_whitespace().next().unwrap_or("");
let target = target.trim_end_matches(&[';'][..]);
if cfg_targets.iter().any(|t| t == target) {
has_cfg = true;
}
if ip_targets.iter().any(|t| t == target) {
has_ip = true;
}
}
(has_cfg, has_ip)
}
fn find_source_word(line: &str) -> Option<usize> {
let mut search_from = 0;
while let Some(rel) = line[search_from..].find("source") {
let at = search_from + rel;
let before_ok = at == 0
|| !line[..at]
.chars()
.next_back()
.is_some_and(|c| c.is_alphanumeric() || c == '_');
let after = &line[at + "source".len()..];
let after_ok = after
.chars()
.next()
.is_none_or(|c| c.is_whitespace());
if before_ok && after_ok {
return Some(at);
}
search_from = at + "source".len();
}
None
}
pub fn change_zshrc(zshrc_path: &str, cfg_path: &str) -> std::io::Result<()> {
let existing = std::fs::read_to_string(zshrc_path).unwrap_or_default();
let (has_cfg, has_ip) = check_integration(&existing, cfg_path);
let cfg_display = contract_home(cfg_path);
let zshrc_display = contract_home(zshrc_path);
let mut out = String::new();
if !has_ip {
out.push_str(&instant_prompt_block(&zshrc_display));
}
if !existing.is_empty() {
if !has_ip {
out.push('\n');
}
out.push_str(&existing);
if !existing.ends_with('\n') {
out.push('\n');
}
}
if !has_cfg {
out.push_str(&config_source_block(&cfg_display));
out.push('\n');
}
let dir = Path::new(zshrc_path)
.parent()
.filter(|p| !p.as_os_str().is_empty())
.map(Path::to_path_buf)
.unwrap_or_else(|| Path::new(".").to_path_buf());
let pid = std::process::id();
let tmp = dir.join(format!(".zshrc.p10k.tmp.{pid}"));
{
let mut f = std::fs::File::create(&tmp)?;
f.write_all(out.as_bytes())?;
f.sync_all()?;
}
std::fs::rename(&tmp, zshrc_path)?; Ok(())
}
fn contract_home(path: &str) -> String {
if let Ok(home) = std::env::var("HOME") {
if let Some(rest) = path.strip_prefix(&format!("{home}/")) {
return format!("~/{rest}");
}
if path == home {
return "~".to_string();
}
}
path.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_existing_source_line() {
let z = "# comment\nsource ~/.p10k.zsh\n";
let (cfg, _ip) = check_integration(z, "/home/u/.p10k.zsh");
let _ = cfg;
}
#[test]
fn commented_source_is_not_a_match() {
let z = "# source ~/.p10k.zsh\n";
let (cfg, ip) = check_integration(z, "/home/u/.p10k.zsh");
assert!(!cfg);
assert!(!ip);
}
#[test]
fn detects_instant_prompt_block() {
let z = "source \"${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh\"\n";
let (_cfg, ip) = check_integration(z, "/home/u/.p10k.zsh");
assert!(ip);
}
#[test]
fn source_word_needs_boundary() {
assert!(find_source_word("resource x").is_none());
assert!(find_source_word("source x").is_some());
assert!(find_source_word("[[ -f x ]] || source x").is_some());
}
}