use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::{Mutex, OnceLock};
use std::time::UNIX_EPOCH;
use crate::extensions::p10k::config::{p9k_global, p9k_param, p9k_param_arr, p9k_param_bool};
use crate::extensions::p10k::icons::icon;
use crate::extensions::p10k::render::Segment;
use crate::ported::params::{getsparam, setsparam, unsetparam};
use crate::ported::utils::getshfunc;
fn envv(name: &str) -> Option<String> {
getsparam(name)
.filter(|v| !v.is_empty())
.or_else(|| std::env::var(name).ok().filter(|v| !v.is_empty()))
}
fn cwd() -> PathBuf {
if let Some(pwd) = envv("PWD") {
let p = PathBuf::from(&pwd);
if p.is_absolute() {
return p;
}
}
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"))
}
fn home() -> PathBuf {
envv("HOME").map(PathBuf::from).unwrap_or_else(|| PathBuf::from("/"))
}
fn mtime(path: &Path) -> i64 {
fs::metadata(path)
.and_then(|m| m.modified())
.ok()
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
.map(|d| d.as_secs() as i64)
.unwrap_or(-1)
}
type StatSig = Vec<(PathBuf, i64)>;
static STAT_CACHE: OnceLock<Mutex<HashMap<String, (StatSig, Vec<String>)>>> = OnceLock::new();
fn stat_sig(files: &[PathBuf]) -> StatSig {
files.iter().map(|f| (f.clone(), mtime(f))).collect()
}
fn cache_get(key: &str, files: &[PathBuf]) -> Option<Vec<String>> {
let sig = stat_sig(files);
let m = STAT_CACHE.get_or_init(Default::default);
if let Ok(guard) = m.lock() {
if let Some((cached_sig, vals)) = guard.get(key) {
if *cached_sig == sig {
return Some(vals.clone());
}
}
}
None
}
fn cache_set(key: &str, files: &[PathBuf], vals: Vec<String>) -> Vec<String> {
let sig = stat_sig(files);
let m = STAT_CACHE.get_or_init(Default::default);
if let Ok(mut guard) = m.lock() {
guard.insert(key.to_string(), (sig, vals.clone()));
}
vals
}
fn have_cmd(name: &str) -> Option<PathBuf> {
static CMD_CACHE: OnceLock<Mutex<HashMap<String, (String, Option<PathBuf>)>>> =
OnceLock::new();
let path_var = envv("PATH").unwrap_or_default();
let m = CMD_CACHE.get_or_init(Default::default);
if let Ok(guard) = m.lock() {
if let Some((p, hit)) = guard.get(name) {
if *p == path_var {
return hit.clone();
}
}
}
let mut found = None;
for dir in path_var.split(':').filter(|d| !d.is_empty()) {
let cand = Path::new(dir).join(name);
let is_exec = fs::metadata(&cand)
.map(|md| {
use std::os::unix::fs::PermissionsExt;
md.is_file() && md.permissions().mode() & 0o111 != 0
})
.unwrap_or(false);
if is_exec {
found = Some(cand);
break;
}
}
if let Ok(mut guard) = m.lock() {
guard.insert(name.to_string(), (path_var, found.clone()));
}
found
}
fn cmd_or_func(name: &str) -> bool {
have_cmd(name).is_some() || getshfunc(name).is_some()
}
fn run_cmd(
bin: &Path,
args: &[&str],
merge_stderr: bool,
extra_env: &[(&str, &str)],
) -> Option<(bool, String)> {
let mut c = Command::new(bin);
c.args(args);
for (k, v) in extra_env {
c.env(k, v);
}
c.stdin(std::process::Stdio::null());
if !merge_stderr {
c.stderr(std::process::Stdio::null());
}
match c.output() {
Ok(out) => {
let mut text = String::from_utf8_lossy(&out.stdout).into_owned();
if merge_stderr {
text.push_str(&String::from_utf8_lossy(&out.stderr));
}
Some((out.status.success(), text.trim_end_matches('\n').to_string()))
}
Err(e) => {
tracing::debug!(target: "p10k", cmd = %bin.display(), %e, "command failed to spawn");
None
}
}
}
fn cached_cmd(
merge_stderr: bool,
witness: Option<&Path>,
cmd: &str,
args: &[&str],
) -> Option<String> {
let bin = have_cmd(cmd)?;
let key = format!(
"cached_cmd {} {} {}",
cmd,
args.join(" "),
witness.map(|w| w.display().to_string()).unwrap_or_default()
);
let mut files: Vec<PathBuf> = Vec::new();
if let Some(w) = witness {
files.push(w.to_path_buf());
}
files.push(bin.clone());
let vals = match cache_get(&key, &files) {
Some(v) => v,
None => {
let (ok, text) = run_cmd(&bin, args, merge_stderr, &[]).unwrap_or((false, String::new()));
cache_set(
&key,
&files,
vec![if ok { "1".into() } else { "0".into() }, text],
)
}
};
if vals.first().map(String::as_str) == Some("1") {
Some(vals.get(1).cloned().unwrap_or_default())
} else {
None
}
}
fn read_word(path: &Path) -> Option<String> {
let content = fs::read_to_string(path).ok()?;
let first_line = content.lines().next()?.trim_end_matches('\r');
let word = first_line.split_whitespace().next()?.to_string();
if word.is_empty() { None } else { Some(word) }
}
fn read_pyenv_like_version_file(path: &Path, prefix: &str) -> Option<String> {
let bytes = fs::read(path).ok()?;
let slice = &bytes[..bytes.len().min(1024)]; let content = String::from_utf8_lossy(slice);
let mut versions: Vec<String> = Vec::new();
for line in content.lines() {
let Some(word) = line.split_whitespace().next() else { continue };
if word.starts_with('#') {
continue;
}
versions.push(word.strip_prefix(prefix).unwrap_or(word).to_string());
}
let joined = versions.join(":"); if joined.is_empty() { None } else { Some(joined) }
}
fn upfind(name: &str) -> Option<PathBuf> {
upfind_pred(&|dir: &Path| dir.join(name).exists())
}
fn upfind_pred(pred: &dyn Fn(&Path) -> bool) -> Option<PathBuf> {
let start = cwd();
let mut dir: &Path = &start;
loop {
if pred(dir) {
return Some(dir.to_path_buf());
}
match dir.parent() {
Some(p) => dir = p,
None => return None,
}
}
}
fn dir_has_ext(dir: &Path, exts: &[&str]) -> bool {
let Ok(rd) = fs::read_dir(dir) else { return false };
for entry in rd.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
if name.starts_with('.') {
continue;
}
if let Some(ext) = name.rsplit('.').next() {
if name.contains('.') && exts.contains(&ext) {
return true;
}
}
}
false
}
fn esc(s: &str) -> String {
s.replace('%', "%%")
}
fn color1() -> String {
if p9k_global("COLOR_SCHEME", "") == "light" { "7".into() } else { "0".into() }
}
fn seg(
name: &str,
state: Option<&str>,
bg: &str,
fg: &str,
icon_key: &str,
content: String,
) -> Segment {
let glyph = if icon_key.is_empty() {
None
} else {
let g = icon(icon_key);
if g.is_empty() { None } else { Some(g.to_string()) }
};
Segment {
name: name.to_string(),
state: state.map(|s| s.to_ascii_uppercase()),
content,
icon: glyph,
fg: fg.to_string(),
bg: bg.to_string(),
}
}
fn hidden() -> Option<Vec<Segment>> {
Some(Vec::new())
}
fn one(s: Segment) -> Option<Vec<Segment>> {
Some(vec![s])
}
fn glob_match(pat: &str, text: &str) -> bool {
fn inner(p: &[u8], t: &[u8]) -> bool {
if p.is_empty() {
return t.is_empty();
}
if p[0] == b'*' {
(0..=t.len()).any(|i| inner(&p[1..], &t[i..]))
} else {
!t.is_empty() && p[0] == t[0] && inner(&p[1..], &t[1..])
}
}
inner(pat.as_bytes(), text.as_bytes())
}
fn classes_state(segment: &str, text: &str) -> Option<String> {
let classes = p9k_param_arr(segment, None, "CLASSES");
let mut it = classes.iter();
while let (Some(pat), Some(class)) = (it.next(), it.next()) {
if glob_match(pat, text) {
if class.is_empty() {
return None;
}
return Some(class.to_ascii_uppercase());
}
}
None
}
struct EnvTool {
seg: &'static str,
prefix: &'static str,
version_file: &'static str,
root_default: &'static str,
file_prefix: &'static str,
}
impl EnvTool {
fn root(&self) -> PathBuf {
envv(&format!("{}_ROOT", self.prefix))
.map(PathBuf::from)
.unwrap_or_else(|| home().join(self.root_default))
}
fn read_version_file(&self, path: &Path) -> Option<String> {
if self.file_prefix.is_empty() {
read_word(path)
} else {
read_pyenv_like_version_file(path, self.file_prefix)
}
}
fn global_version(&self) -> String {
self.read_version_file(&self.root().join("version"))
.unwrap_or_else(|| "system".to_string())
}
fn sources(&self) -> Vec<String> {
let s = p9k_param_arr(self.seg, None, "SOURCES");
if s.is_empty() {
vec!["shell".into(), "local".into(), "global".into()]
} else {
s
}
}
fn resolve(&self) -> Option<String> {
let sources = self.sources();
let has = |what: &str| sources.iter().any(|s| s == what);
let v = if let Some(shell_v) = envv(&format!("{}_VERSION", self.prefix)) {
if !has("shell") {
return None;
}
if self.file_prefix.is_empty() {
shell_v
} else {
shell_v
.split(':')
.map(|p| p.strip_prefix(self.file_prefix).unwrap_or(p))
.collect::<Vec<_>>()
.join(":")
}
} else {
if !has("local") && !has("global") {
return None;
}
let mut found: Option<String> = None;
if let Some(d) = envv(&format!("{}_DIR", self.prefix)).filter(|d| d != ".") {
let dir = if Path::new(&d).is_absolute() {
PathBuf::from(&d)
} else {
cwd().join(&d) };
let dir = dir.canonicalize().unwrap_or(dir);
let cwd_a = cwd().canonicalize().unwrap_or_else(|_| cwd());
if dir != cwd_a {
let mut cur: &Path = &dir;
loop {
if let Some(v) = self.read_version_file(&cur.join(self.version_file)) {
if !has("local") {
return None;
}
found = Some(v);
break;
}
match cur.parent() {
Some(p) => cur = p,
None => break,
}
}
}
}
if found.is_none() {
if let Some(dir) = upfind(self.version_file) {
if let Some(v) = self.read_version_file(&dir.join(self.version_file)) {
if !has("local") {
return None;
}
found = Some(v);
}
}
}
match found {
Some(v) => v,
None => {
if !p9k_param_bool(self.seg, None, "PROMPT_ALWAYS_SHOW", false) {
return None;
}
if !has("global") {
return None;
}
self.global_version()
}
}
};
if !p9k_param_bool(self.seg, None, "PROMPT_ALWAYS_SHOW", false)
&& v == self.global_version()
{
return None;
}
if !p9k_param_bool(self.seg, None, "SHOW_SYSTEM", true) && v == "system" {
return None;
}
Some(v)
}
}
fn envtool_segment(tool: &EnvTool, bg: &str, fg: &str, icon_key: &str) -> Option<Vec<Segment>> {
if !cmd_or_func(tool.seg) && !tool.root().is_dir() {
return hidden();
}
match tool.resolve() {
Some(v) => one(seg(tool.seg, None, bg, fg, icon_key, esc(&v))),
None => hidden(),
}
}
const PYENV_TOOL: EnvTool = EnvTool {
seg: "pyenv",
prefix: "PYENV",
version_file: ".python-version",
root_default: ".pyenv",
file_prefix: "python-",
};
fn pyenv_compute(force_show: bool) -> Option<(String, Option<String>)> {
let tool = PYENV_TOOL;
let v = if force_show {
if let Some(shell_v) = envv("PYENV_VERSION") {
shell_v
.split(':')
.map(|p| p.strip_prefix("python-").unwrap_or(p))
.collect::<Vec<_>>()
.join(":")
} else if let Some(dir) = upfind(".python-version") {
read_pyenv_like_version_file(&dir.join(".python-version"), "python-")?
} else {
tool.global_version()
}
} else {
tool.resolve()?
};
let versions_dir = tool.root().join("versions");
let versions_real = versions_dir.canonicalize().unwrap_or_else(|_| versions_dir.clone());
let mut python_version: Option<String> = None;
for name in v.split(':') {
let cand = versions_dir.join(name);
let real = cand.canonicalize().unwrap_or(cand);
if let Ok(rest) = real.strip_prefix(&versions_real) {
if let Some(first) = rest.components().next() {
python_version = Some(first.as_os_str().to_string_lossy().into_owned());
break;
}
}
}
Some((v, python_version))
}
fn segment_pyenv() -> Option<Vec<Segment>> {
if !cmd_or_func("pyenv") && !PYENV_TOOL.root().is_dir() {
return hidden();
}
match pyenv_compute(false) {
Some((v, py)) => {
match &py {
Some(pv) => {
let _ = setsparam("P9K_PYENV_PYTHON_VERSION", pv);
}
None => {
let _ = unsetparam("P9K_PYENV_PYTHON_VERSION");
}
}
one(seg("pyenv", None, "blue", &color1(), "PYTHON_ICON", esc(&v)))
}
None => {
let _ = unsetparam("P9K_PYENV_PYTHON_VERSION"); hidden()
}
}
}
fn python_version() -> Option<String> {
let python = have_cmd("python")?; let shim = PYENV_TOOL.root().join("shims/python");
if python == shim {
if let Some((_, Some(pv))) = pyenv_compute(true) {
if pv.chars().next().is_some_and(|c| c.is_ascii_digit()) {
return Some(pv);
}
}
}
let out = cached_cmd(true, None, "python", &["--version"])?;
let rest = out.strip_prefix("Python ")?;
let ver: String = rest
.chars()
.take_while(|c| c.is_ascii_digit() || *c == '.')
.collect();
if ver.is_empty() { None } else { Some(ver) }
}
fn segment_direnv() -> Option<Vec<Segment>> {
if envv("DIRENV_DIR").is_none() {
return hidden();
}
one(seg("direnv", None, &color1(), "yellow", "DIRENV_ICON", String::new()))
}
fn segment_virtualenv() -> Option<Vec<Segment>> {
let Some(venv) = envv("VIRTUAL_ENV") else { return hidden() };
let mut msg = String::new();
if p9k_param_bool("virtualenv", None, "SHOW_PYTHON_VERSION", true) {
if let Some(ver) = python_version() {
msg.push_str(&esc(&ver));
msg.push(' ');
}
}
let vpath = PathBuf::from(&venv);
let base = vpath
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
let mut generics = p9k_param_arr("virtualenv", None, "GENERIC_NAMES");
if generics.is_empty() {
generics = vec!["virtualenv".into(), "venv".into(), ".venv".into(), "env".into()];
}
let v = if generics.iter().any(|g| glob_match(g, &base)) {
vpath
.parent()
.and_then(|p| p.file_name())
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or(base)
} else {
base
};
let l = p9k_param("virtualenv", None, "LEFT_DELIMITER", "(");
let r = p9k_param("virtualenv", None, "RIGHT_DELIMITER", ")");
msg.push_str(&format!("{l}{}{r}", esc(&v)));
match p9k_param("virtualenv", None, "SHOW_WITH_PYENV", "true").as_str() {
"false" => {
if let Some((_, Some(_))) = pyenv_compute(false) {
return hidden();
}
}
"if-different" => {
if let Some((pv, _)) = pyenv_compute(false) {
if pv == v {
return hidden();
}
}
}
_ => {}
}
one(seg("virtualenv", None, "blue", &color1(), "PYTHON_ICON", msg))
}
fn segment_anaconda() -> Option<Vec<Segment>> {
let Some(p) = envv("CONDA_PREFIX").or_else(|| envv("CONDA_ENV_PATH")) else {
let _ = unsetparam("P9K_ANACONDA_PYTHON_VERSION");
return hidden();
};
let mut msg = String::new();
match python_version() {
Some(ver) => {
let _ = setsparam("P9K_ANACONDA_PYTHON_VERSION", &ver);
if p9k_param_bool("anaconda", None, "SHOW_PYTHON_VERSION", true) {
msg.push_str(&esc(&ver));
msg.push(' ');
}
}
None => {
let _ = unsetparam("P9K_ANACONDA_PYTHON_VERSION"); }
}
let base = Path::new(&p)
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
let l = p9k_param("anaconda", None, "LEFT_DELIMITER", "("); let r = p9k_param("anaconda", None, "RIGHT_DELIMITER", ")"); msg.push_str(&format!("{l}{}{r}", esc(&base)));
one(seg("anaconda", None, "blue", &color1(), "PYTHON_ICON", msg))
}
fn segment_node_version() -> Option<Vec<Segment>> {
if have_cmd("node").is_none() {
return hidden(); }
let out = match upfind("package.json") {
Some(dir) => cached_cmd(false, Some(&dir.join("package.json")), "node", &["--version"]),
None => {
if p9k_param_bool("node_version", None, "PROJECT_ONLY", false) {
return hidden();
}
cached_cmd(false, None, "node", &["--version"])
}
};
let Some(out) = out else { return hidden() };
let Some(v) = out.strip_prefix('v').filter(|r| !r.is_empty()) else {
return hidden();
};
one(seg("node_version", None, "green", "white", "NODE_ICON", esc(v)))
}
fn segment_go_version() -> Option<Vec<Segment>> {
if have_cmd("go").is_none() {
return hidden(); }
let Some(out) = cached_cmd(false, None, "go", &["version"]) else {
return hidden();
};
let v = out.split_whitespace().find_map(|tok| {
let rest = tok.strip_prefix("go")?;
(!rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit() || c == '.'))
.then(|| rest.to_string())
});
let Some(v) = v else { return hidden() };
if p9k_param_bool("go_version", None, "PROJECT_ONLY", true) {
let gopath = envv("GOPATH").or_else(|| {
let hg = home().join("go");
if hg.is_dir() {
Some(hg.to_string_lossy().into_owned())
} else {
cached_cmd(false, None, "go", &["env", "GOPATH"]).filter(|p| !p.is_empty())
}
});
let in_gopath = gopath
.map(|p| cwd().starts_with(Path::new(&p)))
.unwrap_or(false);
if !in_gopath && upfind("go.mod").is_none() {
return hidden(); }
}
one(seg("go_version", None, "green", "grey93", "GO_ICON", esc(&v)))
}
fn segment_rust_version() -> Option<Vec<Segment>> {
let Some(rustc) = have_cmd("rustc") else {
let _ = unsetparam("P9K_RUST_VERSION"); return hidden(); };
if p9k_param_bool("rust_version", None, "PROJECT_ONLY", true) && upfind("Cargo.toml").is_none()
{
let _ = unsetparam("P9K_RUST_VERSION");
return hidden();
}
let toolchain = envv("RUSTUP_TOOLCHAIN").or_else(|| {
upfind("rust-toolchain").and_then(|d| read_word(&d.join("rust-toolchain")))
});
let rustup_home = envv("RUSTUP_HOME")
.map(PathBuf::from)
.unwrap_or_else(|| home().join(".rustup"));
let key = format!("rust_version {}", toolchain.as_deref().unwrap_or(""));
let files = vec![rustc.clone(), rustup_home.join("settings.toml")];
let vals = match cache_get(&key, &files) {
Some(v) => v,
None => {
let out = run_cmd(&rustc, &["--version"], false, &[])
.filter(|(ok, _)| *ok)
.map(|(_, text)| text)
.unwrap_or_default();
cache_set(&key, &files, vec![out])
}
};
let full = vals.first().cloned().unwrap_or_default();
let v = full
.strip_prefix("rustc ")
.unwrap_or(&full)
.split_whitespace()
.next()
.unwrap_or("")
.to_string();
if v.is_empty() {
let _ = unsetparam("P9K_RUST_VERSION");
return hidden(); }
let _ = setsparam("P9K_RUST_VERSION", &full); one(seg("rust_version", None, "darkorange", &color1(), "RUST_ICON", esc(&v)))
}
fn segment_os_icon() -> Option<Vec<Segment>> {
let key = if cfg!(target_os = "macos") {
"APPLE_ICON" } else if cfg!(target_os = "freebsd") || cfg!(target_os = "openbsd") {
"FREEBSD_ICON" } else if cfg!(target_os = "android") {
"ANDROID_ICON" } else if cfg!(target_os = "linux") {
linux_distro_icon()
} else {
"LINUX_ICON"
};
let glyph = icon(key);
let glyph = if glyph.is_empty() { icon("LINUX_ICON") } else { glyph };
one(seg("os_icon", None, "black", "white", "", glyph.to_string()))
}
fn linux_distro_icon() -> &'static str {
let content = fs::read_to_string("/etc/os-release").unwrap_or_default();
let id = content
.lines()
.find_map(|l| l.strip_prefix("ID="))
.unwrap_or("")
.trim_matches('"')
.to_ascii_lowercase();
if id == "amzn" {
return "LINUX_AMZN_ICON"; }
const MAP: &[(&str, &str)] = &[
("arch", "LINUX_ARCH_ICON"),
("debian", "LINUX_DEBIAN_ICON"),
("raspbian", "LINUX_RASPBIAN_ICON"),
("ubuntu", "LINUX_UBUNTU_ICON"),
("elementary", "LINUX_ELEMENTARY_ICON"),
("fedora", "LINUX_FEDORA_ICON"),
("coreos", "LINUX_COREOS_ICON"),
("kali", "LINUX_KALI_ICON"),
("gentoo", "LINUX_GENTOO_ICON"),
("mageia", "LINUX_MAGEIA_ICON"),
("centos", "LINUX_CENTOS_ICON"),
("opensuse", "LINUX_OPENSUSE_ICON"),
("tumbleweed", "LINUX_OPENSUSE_ICON"),
("sabayon", "LINUX_SABAYON_ICON"),
("slackware", "LINUX_SLACKWARE_ICON"),
("linuxmint", "LINUX_MINT_ICON"),
("alpine", "LINUX_ALPINE_ICON"),
("aosc", "LINUX_AOSC_ICON"),
("nixos", "LINUX_NIXOS_ICON"),
("devuan", "LINUX_DEVUAN_ICON"),
("manjaro", "LINUX_MANJARO_ICON"),
("void", "LINUX_VOID_ICON"),
("artix", "LINUX_ARTIX_ICON"),
("rhel", "LINUX_RHEL_ICON"),
];
for (pat, key) in MAP {
if id.contains(pat) {
return key;
}
}
"LINUX_ICON" }
fn segment_java_version() -> Option<Vec<Segment>> {
let Some(java) = have_cmd("java") else {
return hidden(); };
if p9k_param_bool("java_version", None, "PROJECT_ONLY", false) {
const NAMES: &[&str] = &[
"pom.xml", "build.gradle.kts", "build.sbt", "deps.edn", "project.clj", "build.boot",
];
const EXTS: &[&str] = &["java", "class", "jar", "gradle", "clj", "cljc"];
let found = upfind_pred(&|dir: &Path| {
NAMES.iter().any(|n| dir.join(n).exists()) || dir_has_ext(dir, EXTS)
});
if found.is_none() {
return hidden();
}
}
let mut files = vec![java.clone()];
if let Some(jh) = envv("JAVA_HOME") {
files.push(Path::new(&jh).join("release"));
}
let vals = match cache_get("java_version", &files) {
Some(v) => v,
None => {
let raw = run_cmd(&java, &["-fullversion"], true, &[])
.map(|(_, text)| text)
.unwrap_or_default();
let v = raw
.split_once('"')
.and_then(|(_, rest)| rest.rsplit_once('"'))
.map(|(v, _)| v.to_string())
.unwrap_or_default();
let v = if p9k_param_bool("java_version", None, "FULL", true) {
v
} else {
v.split('-').next().unwrap_or("").to_string()
};
cache_set("java_version", &files, vec![esc(&v)]) }
};
let v = vals.first().cloned().unwrap_or_default();
if v.is_empty() {
return hidden(); }
one(seg("java_version", None, "red", "white", "JAVA_ICON", v))
}
fn segment_package() -> Option<Vec<Segment>> {
let _ = unsetparam("P9K_PACKAGE_NAME"); let _ = unsetparam("P9K_PACKAGE_VERSION");
let Some(dir) = upfind("package.json") else { return hidden() };
let file = dir.join("package.json");
let key = format!("package {}", file.display());
let files = vec![file.clone()];
let vals = match cache_get(&key, &files) {
Some(v) => v,
None => {
let parsed: Option<(String, String)> = fs::read_to_string(&file)
.ok()
.and_then(|data| {
serde_json::from_str::<serde_json::Value>(data.trim_start_matches('\u{feff}'))
.ok()
})
.and_then(|json| {
let name = json.get("name")?.as_str()?.to_string();
let version = json.get("version")?.as_str()?.to_string();
let bad = |s: &str| s.is_empty() || s.contains('\n') || s.contains('\\');
if bad(&name) || bad(&version) {
None
} else {
Some((name, version))
}
});
match parsed {
Some((n, v)) => cache_set(&key, &files, vec!["1".into(), n, v]),
None => cache_set(&key, &files, vec!["0".into()]), }
}
};
if vals.first().map(String::as_str) != Some("1") {
return hidden(); }
let name = vals.get(1).cloned().unwrap_or_default();
let version = vals.get(2).cloned().unwrap_or_default();
let _ = setsparam("P9K_PACKAGE_NAME", &name); let _ = setsparam("P9K_PACKAGE_VERSION", &version);
one(seg("package", None, "cyan", &color1(), "PACKAGE_ICON", esc(&version)))
}
fn segment_rvm() -> Option<Vec<Segment>> {
let Some(gem_home) = envv("GEM_HOME") else { return hidden() };
if !gem_home.contains("rvm") {
return hidden();
}
let ruby_string = getsparam("ruby_string").unwrap_or_default();
let rvm_path = getsparam("rvm_path").unwrap_or_default();
if ruby_string == format!("{rvm_path}/bin/ruby") {
return hidden();
}
let mut v = Path::new(&gem_home)
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
if !p9k_param_bool("rvm", None, "SHOW_GEMSET", false) {
let sep = getsparam("rvm_gemset_separator")
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "@".into());
if let Some(idx) = v.find(&sep) {
v.truncate(idx);
}
}
if !p9k_param_bool("rvm", None, "SHOW_PREFIX", false) {
if let Some(idx) = v.find('-') {
v = v[idx + 1..].to_string();
}
}
if v.is_empty() {
return hidden(); }
one(seg("rvm", None, "240", &color1(), "RUBY_ICON", esc(&v)))
}
fn segment_fvm() -> Option<Vec<Segment>> {
if have_cmd("fvm").is_none() {
return hidden(); }
fn version_from_link(link: &Path, strip_suffix: &str) -> Option<String> {
let target = fs::read_link(link).ok()?;
let t = target.to_string_lossy();
let t = t.strip_suffix(strip_suffix).unwrap_or(&t);
let (head, v) = t.rsplit_once('/')?;
(head.ends_with("versions") && !v.is_empty()).then(|| v.to_string())
}
if let Some(dir) = upfind(".fvm") {
if let Some(v) = version_from_link(&dir.join(".fvm/flutter_sdk"), "") {
return one(seg("fvm", None, "blue", &color1(), "FLUTTER_ICON", esc(&v)));
}
}
if let Some(dir) = upfind("fvm") {
if let Some(v) = version_from_link(&dir.join("fvm"), "/bin/flutter") {
return one(seg("fvm", None, "blue", &color1(), "FLUTTER_ICON", esc(&v)));
}
}
hidden()
}
fn nvm_ls_default(nvm_dir: &Path) -> Option<String> {
let mut v = "default".to_string();
let mut seen = vec![v.clone()];
loop {
let alias = nvm_dir.join("alias").join(&v);
let Ok(content) = fs::read_to_string(&alias) else { break };
let target = content
.lines()
.next()
.unwrap_or("")
.trim_end_matches('\r')
.to_string();
if target.is_empty() {
break; }
if seen.contains(&target) {
return None; }
seen.push(target.clone());
v = target;
}
match v.as_str() {
"default" | "N/A" => return None, "system" | "v" => return Some("system".to_string()), _ => {}
}
if let Some(rest) = v.strip_prefix("iojs-") {
if rest.chars().next().is_some_and(|c| c.is_ascii_digit()) {
v = format!("iojs-v{rest}");
}
} else if v.chars().next().is_some_and(|c| c.is_ascii_digit()) {
v = format!("v{v}"); }
if v.starts_with('v') && v.matches('.').count() >= 2 {
if nvm_dir.join("versions/node").join(&v).join("bin/node").exists()
|| nvm_dir.join(&v).join("bin/node").exists()
{
return Some(v);
}
if nvm_dir.join("versions/io.js").join(&v).join("bin/node").exists() {
return Some(format!("iojs-{v}"));
}
return None;
}
fn parse(name: &str) -> Option<(u64, u64, u64)> {
let rest = name.strip_prefix('v')?;
let mut it = rest.split('.');
let a = it.next()?.parse().ok()?;
let b = it.next()?.parse().ok()?;
let c = it.next()?.parse().ok()?;
Some((a, b, c))
}
let (dirs, accept): (Vec<PathBuf>, Box<dyn Fn(&str, (u64, u64, u64)) -> bool>) =
match v.as_str() {
"node" | "node-" | "stable" => (
vec![nvm_dir.join("versions/node"), nvm_dir.to_path_buf()],
Box::new(|_, (a, b, _)| a >= 1 || b % 2 == 0),
),
"unstable" => (
vec![nvm_dir.join("versions/node"), nvm_dir.to_path_buf()],
Box::new(|_, (a, b, _)| a == 0 && b % 2 == 1),
),
_ if v.starts_with("iojs") => {
let want = format!(
"v{}",
v.trim_start_matches("iojs")
.trim_start_matches('-')
.trim_start_matches('v')
);
(
vec![nvm_dir.join("versions/io.js")],
Box::new(move |name: &str, _| name.starts_with(&want)),
)
}
_ => {
let want = format!("v{}", v.trim_start_matches('v'));
(
vec![
nvm_dir.join("versions/node"),
nvm_dir.to_path_buf(),
nvm_dir.join("versions/io.js"),
],
Box::new(move |name: &str, _| name.starts_with(&want)),
)
}
};
let mut best: Option<((u64, u64, u64), String)> = None;
for d in &dirs {
let Ok(rd) = fs::read_dir(d) else { continue };
for entry in rd.flatten() {
if !entry.path().is_dir() {
continue; }
let name = entry.file_name().to_string_lossy().into_owned();
let Some(nums) = parse(&name) else { continue }; if !accept(&name, nums) {
continue;
}
let display = if d.ends_with("io.js") { format!("iojs-{name}") } else { name };
if best.as_ref().map(|(n, _)| nums > *n).unwrap_or(true) {
best = Some((nums, display));
}
}
}
best.map(|(_, name)| name)
}
fn segment_nvm() -> Option<Vec<Segment>> {
let Some(nvm_dir_s) = envv("NVM_DIR") else { return hidden() };
let nvm_dir = PathBuf::from(&nvm_dir_s);
let Some(node) = have_cmd("node") else { return hidden() }; let node_real = node.canonicalize().unwrap_or(node);
let nvm_real = nvm_dir.canonicalize().unwrap_or_else(|_| nvm_dir.clone());
let current = if node_real.starts_with(nvm_real.join("versions/io.js")) {
let Some(v) = cached_cmd(false, None, "iojs", &["--version"]) else {
return hidden();
};
format!("iojs-v{}", v.trim_start_matches('v'))
} else if node_real.starts_with(&nvm_real) {
let Some(v) = cached_cmd(false, None, "node", &["--version"]) else {
return hidden();
};
format!("v{}", v.trim_start_matches('v'))
} else {
"system".to_string() };
if let Some(default) = nvm_ls_default(&nvm_dir) {
if default == current {
return hidden();
}
}
let shown = current.strip_prefix('v').unwrap_or(¤t);
one(seg("nvm", None, "magenta", "black", "NODE_ICON", esc(shown)))
}
fn segment_nodeenv() -> Option<Vec<Segment>> {
let Some(nve) = envv("NODE_VIRTUAL_ENV") else { return hidden() };
let mut msg = String::new();
if p9k_param_bool("nodeenv", None, "SHOW_NODE_VERSION", true) {
if let Some(v) = cached_cmd(false, None, "node", &["--version"]) {
msg.push_str(&esc(&v));
msg.push(' ');
}
}
let base = Path::new(&nve)
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
let l = p9k_param("nodeenv", None, "LEFT_DELIMITER", "[");
let r = p9k_param("nodeenv", None, "RIGHT_DELIMITER", "]");
msg.push_str(&format!("{l}{}{r}", esc(&base)));
one(seg("nodeenv", None, "black", "green", "NODE_ICON", msg))
}
fn nodenv_version_transform(v: &str) -> Option<String> {
let dir = envv("NODENV_ROOT")
.map(PathBuf::from)
.unwrap_or_else(|| home().join(".nodenv"))
.join("versions");
if v.is_empty() || v == "system" {
return Some(v.to_string()); }
let candidates = [
v.to_string(), v.replacen('v', "", 1), v.strip_prefix("node-").unwrap_or(v).to_string(), v.strip_prefix("node-v").unwrap_or(v).to_string(), ];
for c in candidates {
if dir.join(&c).is_dir() {
return Some(c);
}
}
None }
fn segment_nodenv() -> Option<Vec<Segment>> {
const TOOL: EnvTool = EnvTool {
seg: "nodenv",
prefix: "NODENV",
version_file: ".node-version",
root_default: ".nodenv",
file_prefix: "",
};
if !cmd_or_func("nodenv") && !TOOL.root().is_dir() {
return hidden(); }
let always_show = p9k_param_bool("nodenv", None, "PROMPT_ALWAYS_SHOW", false);
let from_shell = envv("NODENV_VERSION").is_some();
let Some(v) = TOOL.resolve() else { return hidden() };
let v = if from_shell {
v
} else {
match nodenv_version_transform(&v) {
Some(t) => t,
None => return hidden(),
}
};
if !always_show {
if let Some(g) = nodenv_version_transform(&TOOL.global_version()) {
if g == v {
return hidden();
}
}
}
if !p9k_param_bool("nodenv", None, "SHOW_SYSTEM", true) && v == "system" {
return hidden();
}
one(seg("nodenv", None, "black", "green", "NODE_ICON", esc(&v)))
}
fn segment_goenv() -> Option<Vec<Segment>> {
const TOOL: EnvTool = EnvTool {
seg: "goenv",
prefix: "GOENV",
version_file: ".go-version",
root_default: ".goenv",
file_prefix: "go-",
};
envtool_segment(&TOOL, "blue", &color1(), "GO_ICON")
}
fn segment_rbenv() -> Option<Vec<Segment>> {
const TOOL: EnvTool = EnvTool {
seg: "rbenv",
prefix: "RBENV",
version_file: ".ruby-version",
root_default: ".rbenv",
file_prefix: "",
};
envtool_segment(&TOOL, "red", &color1(), "RUBY_ICON")
}
fn segment_luaenv() -> Option<Vec<Segment>> {
const TOOL: EnvTool = EnvTool {
seg: "luaenv",
prefix: "LUAENV",
version_file: ".lua-version",
root_default: ".luaenv",
file_prefix: "",
};
envtool_segment(&TOOL, "blue", &color1(), "LUA_ICON")
}
fn segment_jenv() -> Option<Vec<Segment>> {
const TOOL: EnvTool = EnvTool {
seg: "jenv",
prefix: "JENV",
version_file: ".java-version",
root_default: ".jenv",
file_prefix: "",
};
envtool_segment(&TOOL, "white", "red", "JAVA_ICON")
}
fn segment_plenv() -> Option<Vec<Segment>> {
const TOOL: EnvTool = EnvTool {
seg: "plenv",
prefix: "PLENV",
version_file: ".perl-version",
root_default: ".plenv",
file_prefix: "",
};
envtool_segment(&TOOL, "blue", &color1(), "PERL_ICON")
}
fn segment_phpenv() -> Option<Vec<Segment>> {
const TOOL: EnvTool = EnvTool {
seg: "phpenv",
prefix: "PHPENV",
version_file: ".php-version",
root_default: ".phpenv",
file_prefix: "",
};
envtool_segment(&TOOL, "magenta", &color1(), "PHP_ICON")
}
fn segment_scalaenv() -> Option<Vec<Segment>> {
const TOOL: EnvTool = EnvTool {
seg: "scalaenv",
prefix: "SCALAENV",
version_file: ".scala-version",
root_default: ".scalaenv",
file_prefix: "",
};
envtool_segment(&TOOL, "red", &color1(), "SCALA_ICON")
}
fn haskell_stack_version(yaml: &Path) -> Option<String> {
let stack = have_cmd("stack")?;
let stack_root = envv("STACK_ROOT")
.map(PathBuf::from)
.unwrap_or_else(|| home().join(".stack"));
let key = format!("haskell_stack {}", yaml.display());
let files = vec![
yaml.to_path_buf(),
stack_root.join("pantry/pantry.sqlite3"),
stack_root.join("stack.sqlite3"),
];
let vals = match cache_get(&key, &files) {
Some(v) => v,
None => {
let out = run_cmd(
&stack,
&[
"--silent",
"--no-install-ghc",
"--skip-ghc-check",
"--no-terminal",
"--color=never",
"--lock-file=read-only",
"query",
"compiler",
"actual",
],
false,
&[("STACK_YAML", &yaml.to_string_lossy())],
)
.filter(|(ok, _)| *ok)
.map(|(_, text)| text)
.unwrap_or_default();
cache_set(&key, &files, vec![out])
}
};
vals.into_iter().next().filter(|v| !v.is_empty())
}
fn segment_haskell_stack() -> Option<Vec<Segment>> {
if have_cmd("stack").is_none() {
return hidden(); }
let sources = {
let s = p9k_param_arr("haskell_stack", None, "SOURCES");
if s.is_empty() {
vec!["shell".into(), "local".into()] } else {
s
}
};
let has = |w: &str| sources.iter().any(|s| s == w);
let always_show = p9k_param_bool("haskell_stack", None, "PROMPT_ALWAYS_SHOW", true);
let stack_root = envv("STACK_ROOT")
.map(PathBuf::from)
.unwrap_or_else(|| home().join(".stack"));
let global_yaml = stack_root.join("global-project/stack.yaml");
let v = if let Some(sy) = envv("STACK_YAML") {
if !has("shell") {
return hidden();
}
haskell_stack_version(Path::new(&sy))
} else {
if !has("local") && !has("global") {
return hidden(); }
match upfind("stack.yaml") {
Some(dir) => {
if !has("local") {
return hidden();
}
haskell_stack_version(&dir.join("stack.yaml"))
}
None => {
if !always_show || !has("global") {
return hidden();
}
haskell_stack_version(&global_yaml)
}
}
};
let Some(v) = v else { return hidden() }; if !always_show {
if let Some(g) = haskell_stack_version(&global_yaml) {
if g == v {
return hidden();
}
}
}
one(seg("haskell_stack", None, "yellow", &color1(), "HASKELL_ICON", esc(&v)))
}
fn segment_kubecontext() -> Option<Vec<Segment>> {
if have_cmd("kubectl").is_none() {
return hidden(); }
let cfg_files: Vec<PathBuf> = envv("KUBECONFIG")
.map(|k| k.split(':').filter(|p| !p.is_empty()).map(PathBuf::from).collect())
.unwrap_or_else(|| vec![home().join(".kube/config")]);
let vals = match cache_get("kubecontext", &cfg_files) {
Some(v) => v,
None => {
let parsed = kubectl_current_context().unwrap_or_else(|| vec![String::new(); 10]);
cache_set("kubecontext", &cfg_files, parsed)
}
};
let get = |i: usize| vals.get(i).cloned().unwrap_or_default();
for (i, p) in [
"P9K_KUBECONTEXT_NAME",
"P9K_KUBECONTEXT_NAMESPACE",
"P9K_KUBECONTEXT_CLUSTER",
"P9K_KUBECONTEXT_USER",
"P9K_KUBECONTEXT_CLOUD_NAME",
"P9K_KUBECONTEXT_CLOUD_ACCOUNT",
"P9K_KUBECONTEXT_CLOUD_ZONE",
"P9K_KUBECONTEXT_CLOUD_CLUSTER",
]
.iter()
.enumerate()
{
let _ = setsparam(p, &get(i));
}
let text = get(8);
if text.is_empty() {
return hidden(); }
let state = get(9);
one(seg(
"kubecontext",
if state.is_empty() { None } else { Some(state.as_str()) },
"magenta",
"white",
"KUBERNETES_ICON",
esc(&text),
))
}
fn kubectl_current_context() -> Option<Vec<String>> {
let kubectl = have_cmd("kubectl")?;
let (ok, out) = run_cmd(&kubectl, &["config", "view", "-o=yaml"], false, &[])?;
if !ok {
return None;
}
let unquote = |s: &str| -> String {
if s.len() >= 2 && s.starts_with('"') && s.ends_with('"') {
s[1..s.len() - 1].to_string()
} else {
s.to_string()
}
};
let lines: Vec<&str> = out.lines().collect();
let ctx_lines: Vec<&str> = lines
.iter()
.filter(|l| l.starts_with("current-context: "))
.copied()
.collect();
if ctx_lines.len() != 1 {
return None;
}
let name = unquote(ctx_lines[0].trim_start_matches("current-context: "));
let mut namespace = String::new();
let mut cluster = String::new();
let mut user = String::new();
if let Some(cpos) = lines.iter().position(|l| *l == "contexts:") {
let tail = &lines[cpos + 1..];
let name_line_a = format!(" name: {name}");
let name_line_b = format!(" name: \"{name}\"");
if let Some(npos) = tail.iter().position(|l| **l == name_line_a || **l == name_line_b) {
for line in tail[..npos].iter().rev() {
if *line == "- context:" {
break; } else if let Some(v) = line.strip_prefix(" cluster: ") {
cluster = unquote(v); } else if let Some(v) = line.strip_prefix(" namespace: ") {
namespace = unquote(v); } else if let Some(v) = line.strip_prefix(" user: ") {
user = unquote(v); }
}
}
}
if name.is_empty() {
return Some(vec![String::new(); 10]);
}
if namespace.is_empty() {
namespace = "default".to_string(); }
let mut cloud_name = String::new();
let mut cloud_account = String::new();
let mut cloud_zone = String::new();
let mut cloud_cluster = String::new();
let mut text = String::new();
let shorten = p9k_param_arr("kubecontext", None, "SHORTEN");
if let Some(rest) = cluster.strip_prefix("gke_") {
let parts: Vec<&str> = rest.splitn(3, '_').collect();
if parts.len() == 3 && parts.iter().all(|p| !p.is_empty()) {
cloud_name = "gke".into();
cloud_account = parts[0].into();
cloud_zone = parts[1].into();
cloud_cluster = parts[2].into();
if shorten.iter().any(|s| s == "gke") {
text = cloud_cluster.clone();
}
}
} else if let Some(rest) = cluster.strip_prefix("arn:aws:eks:") {
let parts: Vec<&str> = rest.splitn(3, ':').collect();
if parts.len() == 3 {
if let Some(c) = parts[2].strip_prefix("cluster/") {
cloud_name = "eks".into();
cloud_zone = parts[0].into();
cloud_account = parts[1].into();
cloud_cluster = c.into();
if shorten.iter().any(|s| s == "eks") {
text = cloud_cluster.clone();
}
}
}
}
if text.is_empty() {
text = name.clone();
let show_default_ns = p9k_param_bool("kubecontext", None, "SHOW_DEFAULT_NAMESPACE", true);
if show_default_ns || (namespace != "default" && namespace != name) {
text.push('/');
text.push_str(&namespace);
}
}
let state = classes_state("kubecontext", &text).unwrap_or_default();
Some(vec![
name, namespace, cluster, user, cloud_name, cloud_account, cloud_zone, cloud_cluster,
text, state,
])
}
fn segment_terraform() -> Option<Vec<Segment>> {
if have_cmd("terraform").is_none() {
return hidden(); }
let ws = match envv("TF_WORKSPACE") {
Some(w) => w,
None => {
let data_dir = envv("TF_DATA_DIR").unwrap_or_else(|| ".terraform".to_string());
let dd = Path::new(&data_dir);
let env_file = if dd.is_absolute() {
dd.join("environment")
} else {
cwd().join(dd).join("environment") };
match read_word(&env_file) {
Some(w) => w,
None => return hidden(),
}
}
};
if ws.is_empty()
|| (ws == "default" && !p9k_param_bool("terraform", None, "SHOW_DEFAULT", false))
{
return hidden();
}
let state = classes_state("terraform", &ws);
one(seg(
"terraform",
state.as_deref(),
&color1(),
"blue",
"TERRAFORM_ICON",
esc(&ws),
))
}
fn parse_aws_config(cfg: &Path) -> Vec<(String, String)> {
let Ok(content) = fs::read_to_string(cfg) else { return Vec::new() };
let mut out = Vec::new();
let mut profile: Option<String> = None;
for line in content.lines() {
let t = line.trim();
if t == "[default]" || t.starts_with("[default]") && t[9..].trim_start().starts_with('#') {
profile = Some("default".to_string());
} else if let Some(rest) = t.strip_prefix("[profile") {
if let Some(name) = rest.strip_suffix(']') {
let name = name.trim();
if !name.is_empty() {
profile = Some(name.trim_matches('"').trim_matches('\'').to_string());
}
}
} else if let Some(rest) = t.strip_prefix("region") {
let rest = rest.trim_start();
if let Some(val) = rest.strip_prefix('=') {
if let Some(p) = profile.take() {
out.push((p, val.trim().to_string()));
}
}
}
}
out
}
fn segment_aws() -> Option<Vec<Segment>> {
let Some(profile) = envv("AWS_VAULT")
.or_else(|| envv("AWSUME_PROFILE"))
.or_else(|| envv("AWS_PROFILE"))
.or_else(|| envv("AWS_DEFAULT_PROFILE"))
else {
return hidden();
};
let _ = setsparam("P9K_AWS_PROFILE", &profile); let state = classes_state("aws", &profile);
let region = envv("AWS_REGION")
.or_else(|| envv("AWS_DEFAULT_REGION"))
.or_else(|| {
let cfg = envv("AWS_CONFIG_FILE")
.map(PathBuf::from)
.unwrap_or_else(|| home().join(".aws/config"));
let key = format!("aws_config {}", cfg.display());
let files = vec![cfg.clone()];
let vals = match cache_get(&key, &files) {
Some(v) => v,
None => {
let flat: Vec<String> = parse_aws_config(&cfg)
.into_iter()
.flat_map(|(p, r)| [p, r])
.collect();
cache_set(&key, &files, flat)
}
};
let mut it = vals.iter();
while let (Some(p), Some(r)) = (it.next(), it.next()) {
if *p == profile {
return Some(r.clone());
}
}
None
});
match region {
Some(r) => {
let _ = setsparam("P9K_AWS_REGION", &r); }
None => {
let _ = unsetparam("P9K_AWS_REGION");
}
}
one(seg("aws", state.as_deref(), "red", "white", "AWS_ICON", esc(&profile)))
}
fn segment_aws_eb_env() -> Option<Vec<Segment>> {
if have_cmd("eb").is_none() {
return hidden(); }
let Some(dir) = upfind(".elasticbeanstalk") else { return hidden() };
let cfg = dir.join(".elasticbeanstalk/config.yml");
let key = format!("aws_eb_env {}", dir.display());
let files = vec![cfg];
let vals = match cache_get(&key, &files) {
Some(v) => v,
None => {
let env = have_cmd("eb")
.and_then(|eb| run_cmd(&eb, &["list"], false, &[]))
.filter(|(ok, _)| *ok)
.map(|(_, out)| {
out.lines()
.find(|l| l.starts_with("* "))
.map(|l| l[2..].to_string())
.unwrap_or_default()
})
.unwrap_or_default();
cache_set(&key, &files, vec![env])
}
};
let env = vals.into_iter().next().unwrap_or_default();
if env.is_empty() {
return hidden(); }
one(seg("aws_eb_env", None, "black", "green", "AWS_EB_ICON", esc(&env)))
}
fn segment_azure() -> Option<Vec<Segment>> {
if have_cmd("az").is_none() {
return hidden(); }
let cfg = envv("AZURE_CONFIG_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| home().join(".azure"))
.join("azureProfile.json");
let key = format!("azure {}", cfg.display());
let files = vec![cfg.clone()];
let vals = match cache_get(&key, &files) {
Some(v) => v,
None => {
let name = fs::read_to_string(&cfg)
.ok()
.and_then(|data| {
serde_json::from_str::<serde_json::Value>(data.trim_start_matches('\u{feff}'))
.ok()
})
.and_then(|json| {
json.get("subscriptions")?.as_array()?.iter().find_map(|s| {
if s.get("isDefault").and_then(|d| d.as_bool()) == Some(true) {
s.get("name").and_then(|n| n.as_str()).map(str::to_string)
} else {
None
}
})
})
.unwrap_or_default();
let name = name.lines().next().unwrap_or("").to_string();
cache_set(&key, &files, vec![name])
}
};
let name = vals.into_iter().next().unwrap_or_default();
if name.is_empty() {
return hidden(); }
let state = classes_state("azure", &name);
one(seg("azure", state.as_deref(), "blue", "white", "AZURE_ICON", esc(&name)))
}
fn segment_gcloud() -> Option<Vec<Segment>> {
if have_cmd("gcloud").is_none() {
return hidden(); }
let active_cfg_file = home().join(".config/gcloud/active_config");
let Some(configuration) = read_word(&active_cfg_file) else {
return hidden();
};
let _ = setsparam("P9K_GCLOUD_CONFIGURATION", &configuration); let cfg_file = home().join(format!(".config/gcloud/configurations/config_{configuration}"));
let key = format!("gcloud {configuration}");
let files = vec![cfg_file];
let vals = match cache_get(&key, &files) {
Some(v) => v,
None => {
let pair = have_cmd("gcloud")
.and_then(|g| {
run_cmd(
&g,
&[
"config",
"configurations",
"describe",
&configuration,
"--format=value[separator=\"\u{1}\"](properties.core.account,properties.core.project)",
],
false,
&[],
)
})
.filter(|(ok, _)| *ok)
.map(|(_, out)| out)
.unwrap_or_default();
let first_line = pair.lines().next().unwrap_or("");
let mut it = first_line.splitn(2, '\u{1}');
let account = it.next().unwrap_or("").to_string();
let project_id = it.next().unwrap_or("").to_string();
cache_set(&key, &files, vec![account, project_id])
}
};
let account = vals.first().cloned().unwrap_or_default();
let project_id = vals.get(1).cloned().unwrap_or_default();
if !account.is_empty() {
let _ = setsparam("P9K_GCLOUD_ACCOUNT", &account); }
if !project_id.is_empty() {
let _ = setsparam("P9K_GCLOUD_PROJECT_ID", &project_id); let _ = setsparam("P9K_GCLOUD_PROJECT", &project_id);
}
if account.is_empty() && project_id.is_empty() {
return hidden();
}
let content = format!("{}:{}", esc(&account), esc(&project_id));
one(seg("gcloud", Some("PARTIAL"), "blue", "white", "GCLOUD_ICON", content))
}
fn segment_google_app_cred() -> Option<Vec<Segment>> {
for p in [
"P9K_GOOGLE_APP_CRED_TYPE",
"P9K_GOOGLE_APP_CRED_PROJECT_ID",
"P9K_GOOGLE_APP_CRED_CLIENT_EMAIL",
] {
let _ = unsetparam(p); }
let Some(cred_path) = envv("GOOGLE_APPLICATION_CREDENTIALS") else {
return hidden(); };
let cred = PathBuf::from(&cred_path);
let key = format!("google_app_cred {}", cred.display());
let files = vec![cred.clone()];
let vals = match cache_get(&key, &files) {
Some(v) => v,
None => {
let fields = fs::read_to_string(&cred)
.ok()
.and_then(|data| {
serde_json::from_str::<serde_json::Value>(data.trim_start_matches('\u{feff}'))
.ok()
})
.map(|json| {
let f = |k: &str| {
json.get(k).and_then(|v| v.as_str()).unwrap_or("").to_string()
};
(f("type"), f("project_id"), f("client_email"))
});
match fields {
Some((t, p, e)) => {
let text = format!("{t}:{p}:{e}"); let state = classes_state("google_app_cred", &text).unwrap_or_default();
cache_set(&key, &files, vec!["1".into(), t, p, e, text, state])
}
None => cache_set(&key, &files, vec!["0".into()]), }
}
};
if vals.first().map(String::as_str) != Some("1") {
return hidden(); }
let get = |i: usize| vals.get(i).cloned().unwrap_or_default();
let _ = setsparam("P9K_GOOGLE_APP_CRED_TYPE", &get(1)); let _ = setsparam("P9K_GOOGLE_APP_CRED_PROJECT_ID", &get(2));
let _ = setsparam("P9K_GOOGLE_APP_CRED_CLIENT_EMAIL", &get(3));
let state = get(5);
one(seg(
"google_app_cred",
if state.is_empty() { None } else { Some(state.as_str()) },
"blue",
"white",
"GCLOUD_ICON",
esc(&get(4)),
))
}
fn segment_nordvpn() -> Option<Vec<Segment>> {
if have_cmd("nordvpn").is_none() {
return hidden(); }
if !Path::new("/run/nordvpn/nordvpnd.sock").exists() {
return hidden();
}
tracing::debug!(
target: "p10k",
"nordvpn daemon socket present but the raw-gRPC status probe is not ported; segment hidden"
);
hidden()
}
fn segment_ranger() -> Option<Vec<Segment>> {
let Some(level) = envv("RANGER_LEVEL") else { return hidden() };
one(seg("ranger", None, &color1(), "yellow", "RANGER_ICON", esc(&level)))
}
fn segment_nnn() -> Option<Vec<Segment>> {
let Some(level) = envv("NNNLVL").filter(|l| l != "0") else {
return hidden();
};
one(seg("nnn", None, "6", &color1(), "NNN_ICON", esc(&level)))
}
fn segment_asdf() -> Option<Vec<Segment>> {
let root = envv("ASDF_DATA_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| home().join(".asdf"));
if !cmd_or_func("asdf") && !root.is_dir() {
return hidden();
}
let plugins_dir = root.join("plugins");
let mut plugins: HashMap<String, Vec<String>> = HashMap::new();
if let Ok(rd) = fs::read_dir(&plugins_dir) {
for entry in rd.flatten() {
if !entry.path().is_dir() {
continue;
}
let plugin = entry.file_name().to_string_lossy().into_owned();
let mut installed: Vec<String> = Vec::new();
if let Ok(ird) = fs::read_dir(root.join("installs").join(&plugin)) {
for ie in ird.flatten() {
if ie.path().is_dir() {
installed.push(ie.file_name().to_string_lossy().into_owned());
}
}
}
installed.push("system".to_string()); plugins.insert(plugin, installed);
}
}
if plugins.is_empty() {
return hidden();
}
let parse_tool_versions = |file: &Path, versions: &mut HashMap<String, String>| {
let Ok(content) = fs::read_to_string(file) else { return };
for line in content.lines() {
let line = line.trim_end_matches('\r'); let line = line.split('#').next().unwrap_or(""); let words: Vec<&str> = line.split_whitespace().collect();
if words.len() < 2 {
continue; }
let Some(installed) = plugins.get(words[0]) else { continue }; let version = words[1..]
.iter()
.find(|w| installed.iter().any(|i| i == **w))
.copied()
.unwrap_or(words[1]);
versions.entry(words[0].to_string()).or_insert_with(|| version.to_string());
}
};
let mut dirs: Vec<PathBuf> = Vec::new();
let start = cwd();
let mut cur: &Path = &start;
loop {
dirs.push(cur.to_path_buf());
match cur.parent() {
Some(p) => cur = p,
None => break,
}
}
let h = home();
if dirs.last() != Some(&h) {
dirs.push(h.clone()); }
let mut versions: HashMap<String, String> = HashMap::new();
let mut local_versions: HashMap<String, String> = HashMap::new();
let mut has_global = false;
for dir in &dirs {
if *dir == h && !has_global {
has_global = true;
local_versions = std::mem::take(&mut versions);
}
let tv = dir.join(".tool-versions");
if tv.is_file() {
parse_tool_versions(&tv, &mut versions);
}
}
if !has_global {
local_versions = std::mem::take(&mut versions);
}
if let Some(f) = envv("ASDF_DEFAULT_TOOL_VERSIONS_FILENAME") {
let f = PathBuf::from(f);
if f.is_file() {
parse_tool_versions(&f, &mut versions);
}
}
let mut names: Vec<&String> = plugins.keys().collect();
names.sort();
let mut out = Vec::new();
for plugin in names {
let upper = plugin.replace('-', "_").to_ascii_uppercase();
let mut sources = p9k_param_arr("asdf", Some(&upper), "SOURCES");
if sources.is_empty() {
sources = vec!["shell".into(), "local".into(), "global".into()]; }
let has = |w: &str| sources.iter().any(|s| s == w);
let version = if let Some(v) = envv(&format!("ASDF_{upper}_VERSION")) {
if !has("shell") {
continue;
}
v
} else if let Some(v) = local_versions.get(plugin) {
if !has("local") {
continue;
}
v.clone()
} else if let Some(v) = versions.get(plugin) {
if !has("global") {
continue;
}
v.clone()
} else {
continue;
};
if Some(&version) == versions.get(plugin)
&& !p9k_param_bool("asdf", Some(&upper), "PROMPT_ALWAYS_SHOW", false)
{
continue;
}
if version == "system" && !p9k_param_bool("asdf", Some(&upper), "SHOW_SYSTEM", true) {
continue;
}
let mut s = seg(
"asdf",
Some(&upper),
"green",
&color1(),
&format!("{upper}_ICON"),
esc(&version),
);
if s.icon.is_none() {
s.icon = Some(plugin.clone());
}
out.push(s);
}
Some(out)
}
pub fn build_segment(name: &str) -> Option<Vec<Segment>> {
match name {
"direnv" => segment_direnv(),
"asdf" => segment_asdf(),
"virtualenv" => segment_virtualenv(),
"anaconda" => segment_anaconda(),
"pyenv" => segment_pyenv(),
"goenv" => segment_goenv(),
"nodenv" => segment_nodenv(),
"nvm" => segment_nvm(),
"nodeenv" => segment_nodeenv(),
"node_version" => segment_node_version(),
"go_version" => segment_go_version(),
"rust_version" => segment_rust_version(),
"os_icon" => segment_os_icon(),
"java_version" => segment_java_version(),
"package" => segment_package(),
"rbenv" => segment_rbenv(),
"rvm" => segment_rvm(),
"fvm" => segment_fvm(),
"luaenv" => segment_luaenv(),
"jenv" => segment_jenv(),
"plenv" => segment_plenv(),
"phpenv" => segment_phpenv(),
"scalaenv" => segment_scalaenv(),
"haskell_stack" => segment_haskell_stack(),
"kubecontext" => segment_kubecontext(),
"terraform" => segment_terraform(),
"aws" => segment_aws(),
"aws_eb_env" => segment_aws_eb_env(),
"azure" => segment_azure(),
"gcloud" => segment_gcloud(),
"google_app_cred" => segment_google_app_cred(),
"nordvpn" => segment_nordvpn(),
"ranger" => segment_ranger(),
"nnn" => segment_nnn(),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn glob_match_star_patterns() {
assert!(glob_match("*prod*", "us-prod-1"));
assert!(glob_match("*", "anything"));
assert!(glob_match("*", ""));
assert!(!glob_match("*prod*", "staging"));
assert!(glob_match("gke_*", "gke_acct_zone_cluster"));
}
#[test]
fn esc_doubles_percent() {
assert_eq!(esc("50%"), "50%%");
assert_eq!(esc("plain"), "plain");
}
#[test]
fn unknown_segment_is_none() {
assert!(build_segment("no_such_segment_xyz").is_none());
}
#[test]
fn read_pyenv_like_version_file_joins_and_strips() {
let dir = std::env::temp_dir().join("p10k_segments_env_test");
let _ = fs::create_dir_all(&dir);
let f = dir.join("version");
fs::write(&f, "python-3.11.4 extra\n# comment\n2.7.18\n").unwrap();
assert_eq!(
read_pyenv_like_version_file(&f, "python-").as_deref(),
Some("3.11.4:2.7.18")
);
let _ = fs::remove_file(&f);
}
#[test]
fn parse_aws_config_profiles_and_regions() {
let dir = std::env::temp_dir().join("p10k_segments_env_test");
let _ = fs::create_dir_all(&dir);
let f = dir.join("aws_config");
fs::write(
&f,
"[default]\nregion = us-east-1\n[profile prod]\nregion = eu-west-1\n",
)
.unwrap();
let parsed = parse_aws_config(&f);
assert_eq!(
parsed,
vec![
("default".to_string(), "us-east-1".to_string()),
("prod".to_string(), "eu-west-1".to_string()),
]
);
let _ = fs::remove_file(&f);
}
#[test]
fn nodenv_transform_passes_system_and_empty() {
assert_eq!(nodenv_version_transform("system").as_deref(), Some("system"));
assert_eq!(nodenv_version_transform("").as_deref(), Some(""));
assert_eq!(nodenv_version_transform("v99.99.99-does-not-exist"), None);
}
}