use crate::extensions::p10k::config::p9k_param;
use crate::ported::lex::untokenize;
use crate::ported::params::{getsparam, setsparam};
use crate::ported::subst::{singsub, subst_parse_str};
use crate::ported::utils::errflag;
use crate::vm_helper::glob_match_static;
use std::collections::HashMap;
use std::os::unix::fs::MetadataExt;
use std::sync::atomic::Ordering;
use std::sync::{Mutex, OnceLock};
pub fn apply_content_expansion(segment: &str, state: Option<&str>, content: &str) -> String {
apply_expansion(
segment,
state,
"CONTENT_EXPANSION",
"${P9K_CONTENT}",
"P9K_CONTENT",
content,
)
}
pub fn apply_visual_identifier_expansion(segment: &str, state: Option<&str>, icon: &str) -> String {
apply_expansion(
segment,
state,
"VISUAL_IDENTIFIER_EXPANSION",
"${P9K_VISUAL_IDENTIFIER}",
"P9K_VISUAL_IDENTIFIER",
icon,
)
}
fn apply_expansion(
segment: &str,
state: Option<&str>,
param: &str,
default_template: &str,
var_name: &str,
value: &str,
) -> String {
let template = p9k_param(segment, state, param, default_template);
if template == default_template {
return value.to_string();
}
if template.is_empty() {
return String::new();
}
if !template.contains('$') && !template.contains('`') {
return template;
}
let hermetic = !is_non_hermetic(&template);
if hermetic {
if let Some(hit) = cache_get(&template, value) {
return hit;
}
}
setsparam(var_name, value);
match eval_template(&template) {
Some(expanded) => {
if hermetic {
cache_put(&template, value, &expanded);
}
expanded
}
None => {
tracing::debug!(target: "p10k", segment, param, template = %template,
"expansion template failed to evaluate; using raw value");
value.to_string()
}
}
}
fn eval_template(template: &str) -> Option<String> {
let saved = errflag.load(Ordering::Relaxed);
errflag.store(0, Ordering::Relaxed);
let result = subst_parse_str(template, true, false).map(|parsed| singsub(&parsed));
let failed = errflag.load(Ordering::Relaxed) != 0;
errflag.store(saved, Ordering::Relaxed);
match result {
Some(expanded) if !failed => Some(untokenize(&expanded)),
_ => None,
}
}
fn is_non_hermetic(template: &str) -> bool {
let b = template.as_bytes();
for i in 0..b.len().saturating_sub(1) {
if b[i] == b'$' && b[i + 1] == b'(' && (i == 0 || b[i - 1] != b'\\') {
return true;
}
}
false
}
static EXP_CACHE: OnceLock<Mutex<Vec<((String, String), String)>>> = OnceLock::new();
const EXP_CACHE_CAP: usize = 32;
fn cache_get(template: &str, value: &str) -> Option<String> {
let m = EXP_CACHE.get_or_init(Default::default);
let mut guard = m.lock().ok()?;
let pos = guard
.iter()
.position(|((t, v), _)| t == template && v == value)?;
let entry = guard.remove(pos);
let result = entry.1.clone();
guard.insert(0, entry);
Some(result)
}
fn cache_put(template: &str, value: &str, result: &str) {
let m = EXP_CACHE.get_or_init(Default::default);
if let Ok(mut guard) = m.lock() {
guard.retain(|((t, v), _)| !(t == template && v == value));
guard.insert(
0,
((template.to_string(), value.to_string()), result.to_string()),
);
guard.truncate(EXP_CACHE_CAP);
}
}
pub fn show_on_upglob(segment: &str) -> bool {
let pattern = p9k_param(segment, None, "SHOW_ON_UPGLOB", "");
if pattern.is_empty() {
return true;
}
upglob_from(&cwd(), &home_dir(), &pattern)
}
fn cwd() -> String {
if let Some(p) = getsparam("PWD") {
if !p.is_empty() {
return p;
}
}
std::env::current_dir()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_default()
}
fn home_dir() -> String {
match getsparam("HOME") {
Some(h) if !h.is_empty() => h,
_ => std::env::var("HOME").unwrap_or_default(),
}
}
fn upglob_from(cwd: &str, home: &str, pattern: &str) -> bool {
parent_dirs(cwd, home)
.iter()
.any(|dir| dir_has_match(dir, pattern))
}
fn parent_dirs(cwd: &str, home: &str) -> Vec<String> {
if !cwd.starts_with('/') || cwd == "/" {
return Vec::new();
}
let under_home =
!home.is_empty() && home != "/" && (cwd == home || cwd.starts_with(&format!("{home}/")));
let mut dirs = Vec::new();
let mut d = cwd.trim_end_matches('/').to_string();
loop {
if d.is_empty() || d == "/" {
break; }
dirs.push(d.clone());
if under_home && d == home {
break; }
d = match d.rfind('/') {
Some(0) => "/".to_string(),
Some(i) => d[..i].to_string(),
None => break,
};
}
dirs
}
static GLOB_CACHE: OnceLock<Mutex<HashMap<(String, String), (i64, bool)>>> = OnceLock::new();
fn dir_has_match(dir: &str, pattern: &str) -> bool {
let mtime = std::fs::metadata(dir).map(|m| m.mtime()).unwrap_or(-1);
let m = GLOB_CACHE.get_or_init(Default::default);
let key = (dir.to_string(), pattern.to_string());
if let Ok(guard) = m.lock() {
if let Some(&(cached_mtime, matched)) = guard.get(&key) {
if cached_mtime == mtime {
return matched;
}
}
}
let alts = split_alternatives(pattern);
let mut matched = false;
if let Ok(rd) = std::fs::read_dir(dir) {
'entries: for entry in rd.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
for alt in &alts {
if name.starts_with('.') && !alt.starts_with('.') {
continue;
}
if glob_match_static(&name, alt) {
matched = true;
break 'entries;
}
}
}
}
if let Ok(mut guard) = m.lock() {
guard.insert(key, (mtime, matched)); }
matched
}
fn split_alternatives(pattern: &str) -> Vec<String> {
let mut alts = Vec::new();
let mut depth = 0usize;
let mut cur = String::new();
for c in pattern.chars() {
match c {
'(' => {
depth += 1;
cur.push(c);
}
')' => {
depth = depth.saturating_sub(1);
cur.push(c);
}
'|' if depth == 0 => alts.push(std::mem::take(&mut cur)),
_ => cur.push(c),
}
}
alts.push(cur);
alts
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ported::options::{opt_state_get, opt_state_set};
use crate::ported::params::{setsparam, unsetparam};
fn with_exec<F: FnOnce()>(body: F) {
let _g = crate::test_util::global_state_lock();
let saved = opt_state_get("exec").unwrap_or(false);
opt_state_set("exec", true);
body();
opt_state_set("exec", saved);
}
#[test]
fn default_template_short_circuits() {
with_exec(|| {
unsetparam("POWERLEVEL9K_DIR_CONTENT_EXPANSION");
unsetparam("POWERLEVEL9K_CONTENT_EXPANSION");
assert_eq!(apply_content_expansion("dir", None, "~/src"), "~/src");
unsetparam("POWERLEVEL9K_DIR_VISUAL_IDENTIFIER_EXPANSION");
unsetparam("POWERLEVEL9K_VISUAL_IDENTIFIER_EXPANSION");
assert_eq!(apply_visual_identifier_expansion("dir", None, "\u{e0b0}"), "\u{e0b0}");
});
}
#[test]
fn empty_and_literal_overrides() {
with_exec(|| {
setsparam("POWERLEVEL9K_DIR_CONTENT_EXPANSION", "");
assert_eq!(apply_content_expansion("dir", None, "~/src"), "");
setsparam("POWERLEVEL9K_DIR_CONTENT_EXPANSION", "fixed");
assert_eq!(apply_content_expansion("dir", None, "~/src"), "fixed");
unsetparam("POWERLEVEL9K_DIR_CONTENT_EXPANSION");
});
}
#[test]
fn template_evaluates_with_p9k_content() {
with_exec(|| {
setsparam("POWERLEVEL9K_DIR_CONTENT_EXPANSION", "${P9K_CONTENT:u}");
assert_eq!(apply_content_expansion("dir", None, "abc"), "ABC");
assert_eq!(apply_content_expansion("dir", None, "abc"), "ABC");
unsetparam("POWERLEVEL9K_DIR_CONTENT_EXPANSION");
});
}
#[test]
fn non_hermetic_detection() {
assert!(is_non_hermetic("$(date)"));
assert!(is_non_hermetic("x$(date)"));
assert!(!is_non_hermetic("\\$(date)"));
assert!(!is_non_hermetic("${P9K_CONTENT}"));
assert!(!is_non_hermetic(""));
}
#[test]
fn alternation_split() {
assert_eq!(split_alternatives("a"), vec!["a"]);
assert_eq!(
split_alternatives("project.json|*.csproj|*.(java|class)"),
vec!["project.json", "*.csproj", "*.(java|class)"]
);
}
#[test]
fn parent_dirs_boundaries() {
assert!(parent_dirs("/", "/home/u").is_empty());
assert!(parent_dirs("relative", "/home/u").is_empty());
assert_eq!(
parent_dirs("/opt/foo/bar", "/home/u"),
vec!["/opt/foo/bar", "/opt/foo", "/opt"]
);
assert_eq!(
parent_dirs("/home/u/a/b", "/home/u"),
vec!["/home/u/a/b", "/home/u/a", "/home/u"]
);
assert_eq!(parent_dirs("/home/u", "/home/u"), vec!["/home/u"]);
}
#[test]
fn upglob_dir_walk() {
let td = tempfile::tempdir().expect("tempdir");
let root = td.path();
let deep = root.join("a/b/c");
std::fs::create_dir_all(&deep).expect("mkdirs");
std::fs::write(root.join("a/Cargo.toml"), "").expect("marker");
std::fs::write(root.join("a/b/.ruby-version"), "").expect("dotmarker");
let cwd = deep.to_string_lossy().into_owned();
let home = root.to_string_lossy().into_owned();
assert!(upglob_from(&cwd, &home, "Cargo.toml"));
assert!(upglob_from(&cwd, &home, "*.toml"));
assert!(upglob_from(&cwd, &home, "no-such|Cargo.toml"));
assert!(!upglob_from(&cwd, &home, "no-such-file.xyz"));
assert!(upglob_from(&cwd, &home, ".ruby-version"));
assert!(!upglob_from(&cwd, &home, "*ruby-version"));
}
}