use crate::extensions::p10k::config::{p9k_global, p9k_global_arr, p9k_param};
use crate::extensions::p10k::git;
use crate::extensions::p10k::icons;
use crate::extensions::p10k::render::Segment;
use crate::extensions::p10k::shared::{color1, color2, env_or_param, global_bool, global_int, esc_pct, decode_g, seg_icon, apply_visual_identifier, apply_content_expansion, make_segment};
use crate::ported::params::{getsparam, pipestatgetfn};
use crate::ported::utils::getkeystring;
use std::path::Path;
use std::sync::atomic::Ordering;
const MARK_ELIDE: char = '\u{1}';
const MARK_ANCHOR: char = '\u{2}';
const MARK_UNIQ: char = '\u{3}';
pub fn build_segment(name: &str) -> Option<Vec<Segment>> {
match name {
"dir" => Some(dir_segments()), "status" => Some(status_segments()), "context" => Some(context_segments()), "ssh" => Some(ssh_segments()), "root_indicator" => Some(root_indicator_segments()), "history" => Some(history_segments()), "prompt_char" => Some(prompt_char_segments()), "ip" => Some(ip_segments()), "dir_writable" => Some(dir_writable_segments()), "background_jobs" => Some(background_jobs_segments()), "vcs" => Some(vcs_segments()), n if n.starts_with("custom_") => {
Some(custom_segments(&n["custom_".len()..]))
}
_ => None,
}
}
fn bgesc(c: &str) -> String {
if c.is_empty() {
"%k".to_string()
} else {
format!("%K{{{c}}}")
}
}
fn fgesc(c: &str) -> String {
if c.is_empty() {
"%f".to_string()
} else {
format!("%F{{{c}}}")
}
}
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 {
env_or_param("HOME")
}
pub(crate) fn is_ssh() -> bool {
if !env_or_param("SSH_CLIENT").is_empty()
|| !env_or_param("SSH_TTY").is_empty()
|| !env_or_param("SSH_CONNECTION").is_empty()
{
return true;
}
static VIA_WHO: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*VIA_WHO.get_or_init(ssh_via_who)
}
fn ssh_via_who() -> bool {
#[cfg(any(target_os = "macos", all(target_os = "linux", target_env = "gnu")))]
{
let tty = unsafe {
let p = libc::ttyname(libc::STDIN_FILENO);
(!p.is_null()).then(|| std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned())
}
.unwrap_or_else(|| env_or_param("TTY"));
let tty = tty.strip_prefix("/dev/").unwrap_or(&tty);
return utmpx_host_for_tty(tty).is_some_and(|h| looks_remote(&h));
}
#[cfg(not(any(target_os = "macos", all(target_os = "linux", target_env = "gnu"))))]
ssh_via_who_subprocess()
}
#[cfg(not(any(target_os = "macos", all(target_os = "linux", target_env = "gnu"))))]
fn ssh_via_who_subprocess() -> bool {
use std::process::{Command, Stdio};
if crate::extensions::p10k::segments_sys::cmd_on_path("who").is_none() {
return false;
}
let who = |args: &[&str]| {
Command::new("who")
.args(args)
.stderr(Stdio::null())
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).into_owned())
};
let w = match who(&["-m"]) {
Some(out) => out.trim_end_matches('\n').to_string(),
None => {
let tty = env_or_param("TTY");
let tty = tty.strip_prefix("/dev/").unwrap_or(&tty).to_string();
who(&[])
.unwrap_or_default()
.lines()
.filter(|l| {
let f: Vec<&str> = l.split_whitespace().collect();
f.len() > 2 && f[1..f.len() - 1].contains(&tty.as_str())
})
.collect::<Vec<_>>()
.join(" ")
}
};
looks_remote(&w)
}
fn looks_remote(w: &str) -> bool {
let ipv6 = "(([0-9a-fA-F]+:)|:){2,}[0-9a-fA-F]+";
let ipv4 = r"([0-9]{1,3}\.){3}[0-9]+";
let hostname = r"([.][^. ]+){2}";
regex::Regex::new(&format!(r"\(?({ipv4}|{ipv6}|{hostname})\)?$"))
.is_ok_and(|re| re.is_match(w))
}
#[cfg(any(target_os = "macos", all(target_os = "linux", target_env = "gnu")))]
fn utmpx_host_for_tty(tty: &str) -> Option<String> {
let c_str = |buf: &[libc::c_char]| -> String {
let bytes: Vec<u8> = buf.iter().map(|&c| c as u8).take_while(|&b| b != 0).collect();
String::from_utf8_lossy(&bytes).into_owned()
};
let mut host = None;
unsafe {
libc::setutxent();
loop {
let ent = libc::getutxent();
if ent.is_null() {
break;
}
let e = &*ent;
if e.ut_type == libc::USER_PROCESS && c_str(&e.ut_line) == tty {
host = Some(c_str(&e.ut_host));
break;
}
}
libc::endutxent();
}
host
}
fn is_root() -> bool {
unsafe { libc::geteuid() == 0 }
}
fn path_writable(path: &str) -> bool {
match std::ffi::CString::new(path) {
Ok(c) => unsafe { libc::access(c.as_ptr(), libc::W_OK) == 0 },
Err(_) => false,
}
}
fn job_count() -> i32 {
let mut numjobs = 0i32;
if let Some(tab_lock) = crate::ported::jobs::JOBTAB.get() {
if let Ok(tab) = tab_lock.lock() {
let max = crate::ported::jobs::MAXJOB
.get()
.and_then(|m| m.lock().ok().map(|g| *g))
.unwrap_or(0);
let mut j = 1usize;
while j <= max && j < tab.len() {
let jb = &tab[j];
if jb.stat != 0
&& !jb.procs.is_empty()
&& (jb.stat & crate::ported::zsh_h::STAT_NOPRINT) == 0
{
numjobs += 1;
}
j += 1;
}
}
}
numjobs
}
fn context_segments() -> Vec<Segment> {
let ssh = is_ssh();
let default_user = env_or_param("DEFAULT_USER");
let user = getsparam("USERNAME").unwrap_or_default();
let always_show_context = global_bool("ALWAYS_SHOW_CONTEXT", false); let always_show_user = global_bool("ALWAYS_SHOW_USER", false);
let mut content = String::new();
if !always_show_context && !default_user.is_empty() && !ssh && user == default_user {
if !always_show_user {
return vec![];
}
content = esc_pct(&user); }
let sudo = !env_or_param("SUDO_COMMAND").is_empty();
let state = if is_root() {
"ROOT"
} else if ssh && sudo {
"REMOTE_SUDO"
} else if ssh {
"REMOTE"
} else if sudo {
"SUDO"
} else {
"DEFAULT"
};
let text = if content.is_empty() {
let per_state = getsparam(&format!("POWERLEVEL9K_CONTEXT_{state}_TEMPLATE"));
match per_state {
Some(t) => decode_g(&t), None => decode_g(&p9k_global("CONTEXT_TEMPLATE", "%n@%m")), }
} else {
content
};
vec![make_segment(
"context",
Some(state),
color1(),
"yellow",
"",
text,
)]
}
fn ssh_segments() -> Vec<Segment> {
if !is_ssh() {
return vec![];
}
vec![make_segment(
"ssh",
None,
color1(),
"yellow",
"SSH_ICON",
String::new(),
)]
}
fn root_indicator_segments() -> Vec<Segment> {
if !is_root() {
return vec![];
}
vec![make_segment(
"root_indicator",
None,
color1(),
"yellow",
"ROOT_ICON",
String::new(),
)]
}
fn history_segments() -> Vec<Segment> {
vec![make_segment(
"history",
None,
"grey50",
color1(),
"",
"%h".to_string(),
)]
}
fn exit2str(code: i32) -> String {
if code > 128 && !global_bool("STATUS_HIDE_SIGNAME", false) {
let num = code - 128;
if let Some(name) = crate::ported::signals_h::sigs_name(num) {
if global_bool("STATUS_VERBOSE_SIGNAME", true) {
return format!("SIG{name}({num})"); }
return name.to_string();
}
}
code.to_string()
}
fn status_segments() -> Vec<Segment> {
let status = crate::p10k::last_status();
let pipestatus: Vec<i32> = pipestatgetfn()
.iter()
.filter_map(|s| s.parse::<i32>().ok())
.collect();
let mut state = if status != 0 { "ERROR" } else { "OK" }.to_string();
if global_bool("STATUS_EXTENDED_STATES", false) {
if status != 0 {
if pipestatus.len() > 1 {
state.push_str("_PIPE"); } else if status > 128 {
state.push_str("_SIGNAL"); }
} else if pipestatus.iter().any(|&c| c != 0) {
state.push_str("_PIPE"); }
}
if !global_bool(&format!("STATUS_{state}"), true) {
return vec![];
}
let mut text = if global_bool("STATUS_SHOW_PIPESTATUS", true) && !pipestatus.is_empty() {
pipestatus
.iter()
.map(|&c| exit2str(c))
.collect::<Vec<_>>()
.join("|")
} else {
exit2str(status as i32)
};
if status != 0 {
if !global_bool("STATUS_CROSS", false) && global_bool("STATUS_VERBOSE", true) {
vec![make_segment(
"status",
Some(&state),
"red",
"yellow1",
"CARRIAGE_RETURN_ICON",
text,
)]
} else {
vec![make_segment(
"status",
Some(&state),
color1(),
"red",
"FAIL_ICON",
String::new(),
)]
}
} else if global_bool("STATUS_VERBOSE", true) || global_bool("STATUS_OK_IN_NON_VERBOSE", false)
{
if state == "OK" {
text.clear(); }
vec![make_segment(
"status",
Some(&state),
color1(),
"green",
"OK_ICON",
text,
)]
} else {
vec![]
}
}
fn prompt_char_segments() -> Vec<Segment> {
let status = crate::p10k::last_status();
let ok = status == 0;
let keymap_name = crate::ported::zle::zle_keymap::curkeymapname().clone();
let region_active = crate::ported::zle::zle_main::REGION_ACTIVE.load(Ordering::Relaxed) != 0;
let overwrite = crate::ported::zle::zle_main::INSMODE.load(Ordering::Relaxed) == 0;
let keymap = match keymap_name.as_str() {
"vicmd" if region_active => "VIVIS", "vicmd" => "VICMD", "vivis" | "vivli" => "VIVIS", _ if overwrite && global_bool("PROMPT_CHAR_OVERWRITE_STATE", false) => "VIOWR", _ => "VIINS", };
let state = format!("{}_{}", if ok { "OK" } else { "ERROR" }, keymap);
let default_fg = if ok { "76" } else { "196" };
let glyph = match keymap {
"VICMD" => "\u{276E}", "VIVIS" => "\u{2164}", "VIOWR" => "\u{25B6}", _ => "\u{276F}", };
vec![make_segment(
"prompt_char",
Some(&state),
color1(),
default_fg,
"",
glyph.to_string(),
)]
}
fn up_ipv4_interfaces() -> Vec<(String, String)> {
let mut out: Vec<(String, String)> = Vec::new();
let mut ifap: *mut libc::ifaddrs = std::ptr::null_mut();
if unsafe { libc::getifaddrs(&mut ifap) } != 0 {
tracing::warn!(target: "p10k", "getifaddrs failed for ip segment");
return out;
}
let mut cur = ifap;
while !cur.is_null() {
let ifa = unsafe { &*cur };
cur = ifa.ifa_next;
if ifa.ifa_flags & (libc::IFF_UP as u32) == 0 {
continue;
}
if ifa.ifa_addr.is_null() {
continue;
}
let sa = unsafe { &*ifa.ifa_addr };
if i32::from(sa.sa_family) != libc::AF_INET {
continue;
}
let sin = unsafe { &*(ifa.ifa_addr as *const libc::sockaddr_in) };
let ip = std::net::Ipv4Addr::from(u32::from_be(sin.sin_addr.s_addr)).to_string();
let name = unsafe { std::ffi::CStr::from_ptr(ifa.ifa_name) }
.to_string_lossy()
.into_owned();
if !out.iter().any(|(n, _)| *n == name) {
out.push((name, ip));
}
}
unsafe { libc::freeifaddrs(ifap) };
out
}
fn ip_segments() -> Vec<Segment> {
let pattern = p9k_global("IP_INTERFACE", "");
if pattern.is_empty() {
return vec![];
}
let re = match regex::Regex::new(&format!("^({pattern})$")) {
Ok(r) => r,
Err(e) => {
tracing::warn!(target: "p10k", %pattern, %e, "bad POWERLEVEL9K_IP_INTERFACE regex");
return vec![];
}
};
let ip = up_ipv4_interfaces()
.into_iter()
.find(|(name, _)| re.is_match(name))
.map(|(_, ip)| ip);
let Some(ip) = ip else {
return vec![];
};
let iface = up_ipv4_interfaces()
.into_iter()
.find(|(name, _)| re.is_match(name))
.map(|(name, _)| name)
.unwrap_or_default();
publish_ip_status(&iface, &ip);
vec![make_segment(
"ip",
None,
"cyan",
color1(),
"NETWORK_ICON",
ip,
)]
}
static IP_LAST_SAMPLE: std::sync::Mutex<Option<(String, u64, u64, f64)>> =
std::sync::Mutex::new(None);
fn publish_ip_status(iface: &str, ip: &str) {
use crate::ported::params::setsparam;
let _ = setsparam("P9K_IP_IP", ip); let _ = setsparam("P9K_IP_INTERFACE", iface);
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs_f64())
.unwrap_or(0.0);
let (rx, tx) = iface_bytes(iface).unwrap_or((0, 0));
let (rx_rate, tx_rate) = {
let mut last = IP_LAST_SAMPLE.lock().unwrap();
let rates = match last.as_ref() {
Some((liface, lrx, ltx, lts)) if liface == iface => {
let t = now - lts; if t <= 0.0 {
("0 B/s".to_string(), "0 B/s".to_string()) } else {
(
rate_str(rx.saturating_sub(*lrx) as f64 / t), rate_str(tx.saturating_sub(*ltx) as f64 / t), )
}
}
_ => ("0 B/s".to_string(), "0 B/s".to_string()), };
*last = Some((iface.to_string(), rx, tx, now));
rates
};
let _ = setsparam("P9K_IP_RX_RATE", &rx_rate); let _ = setsparam("P9K_IP_TX_RATE", &tx_rate); let _ = setsparam("P9K_IP_RX_BYTES", &rx.to_string());
let _ = setsparam("P9K_IP_TX_BYTES", &tx.to_string());
}
fn rate_str(bytes_per_sec: f64) -> String {
let (val, suffix) = human_readable_bytes(bytes_per_sec);
if suffix == 'B' {
format!("{val} B/s")
} else {
format!("{val} {suffix}iB/s")
}
}
fn human_readable_bytes(n: f64) -> (String, char) {
const SUF: [char; 9] = ['B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; let mut n = n;
let mut suf = 'B';
for (i, s) in SUF.iter().enumerate() {
suf = *s;
if n < 1024.0 || i == SUF.len() - 1 {
break;
}
n /= 1024.0;
}
let raw = if n >= 100.0 {
format!("{n:.0}.")
} else if n >= 10.0 {
format!("{n:.1}")
} else {
format!("{n:.2}")
};
let trimmed = raw.trim_end_matches('0').trim_end_matches('.').to_string();
let trimmed = if trimmed.is_empty() {
"0".to_string()
} else {
trimmed
};
(trimmed, suf)
}
#[cfg(target_os = "linux")]
fn iface_bytes(iface: &str) -> Option<(u64, u64)> {
let rd = |kind: &str| -> Option<u64> {
std::fs::read_to_string(format!("/sys/class/net/{iface}/statistics/{kind}_bytes"))
.ok()?
.trim()
.parse()
.ok()
};
Some((rd("rx")?, rd("tx")?))
}
#[cfg(not(target_os = "linux"))]
fn iface_bytes(iface: &str) -> Option<(u64, u64)> {
let mut ifap: *mut libc::ifaddrs = std::ptr::null_mut();
if unsafe { libc::getifaddrs(&mut ifap) } != 0 {
return None;
}
let mut out = None;
let mut cur = ifap;
while !cur.is_null() {
let ifa = unsafe { &*cur };
cur = ifa.ifa_next;
if ifa.ifa_addr.is_null() {
continue;
}
let sa = unsafe { &*ifa.ifa_addr };
if i32::from(sa.sa_family) != libc::AF_LINK {
continue;
}
let name = unsafe { std::ffi::CStr::from_ptr(ifa.ifa_name) }.to_string_lossy();
if name != iface || ifa.ifa_data.is_null() {
continue;
}
let d = unsafe { &*(ifa.ifa_data as *const libc::if_data) };
out = Some((d.ifi_ibytes as u64, d.ifi_obytes as u64));
break;
}
unsafe { libc::freeifaddrs(ifap) };
out
}
fn dir_writable_segments() -> Vec<Segment> {
if path_writable(&cwd()) {
return vec![];
}
vec![make_segment(
"dir_writable",
Some("FORBIDDEN"),
"red",
"yellow1",
"LOCK_ICON",
String::new(),
)]
}
fn background_jobs_segments() -> Vec<Segment> {
let count = job_count();
if count == 0 {
return vec![];
}
let msg = if global_bool("BACKGROUND_JOBS_VERBOSE", true) {
if global_bool("BACKGROUND_JOBS_VERBOSE_ALWAYS", false) || count != 1 {
count.to_string()
} else {
String::new()
}
} else {
String::new()
};
vec![make_segment(
"background_jobs",
None,
color1(),
"cyan",
"BACKGROUND_JOBS_ICON",
msg,
)]
}
fn custom_command_output(cmd: &str) -> Option<String> {
let first_word = cmd.split_whitespace().next().unwrap_or("");
let first_word = first_word.trim_matches(|c| c == '\'' || c == '"');
if !first_word.is_empty()
&& crate::ported::hashtable::shfunctab_lock()
.read()
.map(|t| t.get(first_word).is_none())
.unwrap_or(true)
&& crate::extensions::p10k::segments_sys::cmd_on_path(first_word).is_none()
&& !crate::ported::builtin::createbuiltintable().contains_key(first_word)
{
return None;
}
Some(crate::ported::exec::run_command_substitution(cmd))
}
fn custom_segments(name: &str) -> Vec<Segment> {
let upper = name.to_ascii_uppercase();
let cmd = match getsparam(&format!("POWERLEVEL9K_CUSTOM_{upper}")) {
Some(c) if !c.is_empty() => c,
_ => return vec![],
};
let content = match crate::extensions::p10k::custom_fast::eval(&cmd) {
Some(c) => c,
None => match custom_command_output(&cmd) {
Some(c) => c,
None => return vec![],
},
};
let content = content.trim().to_string();
if content.is_empty() {
return vec![];
}
let seg_name = format!("custom_{name}");
let icon_key = format!("CUSTOM_{upper}_ICON");
vec![make_segment(
&seg_name,
None,
color2(),
color1(),
&icon_key,
content,
)]
}
pub(super) fn vcs_state_default_bg(state: &str) -> &'static str {
match state {
"CLEAN" | "UNTRACKED" => "2",
"MODIFIED" | "CONFLICTED" => "3",
"LOADING" => "8",
_ => "2",
}
}
fn vcs_part_fg(state: &str, part: &str) -> Option<String> {
getsparam(&format!("POWERLEVEL9K_VCS_{state}_{part}FORMAT_FOREGROUND"))
.or_else(|| getsparam(&format!("POWERLEVEL9K_VCS_{part}FORMAT_FOREGROUND")))
}
fn vcs_state_for(gs: &git::GitStatus) -> &'static str {
if gs.conflicted > 0 && global_bool("VCS_CONFLICTED_STATE", false) {
"CONFLICTED" } else if gs.staged != 0 || gs.unstaged != 0 {
"MODIFIED" } else if gs.untracked != 0 {
"UNTRACKED" } else {
"CLEAN" }
}
fn shorten_branch(branch: &str) -> String {
let sl = getsparam("POWERLEVEL9K_VCS_SHORTEN_LENGTH").and_then(|v| v.parse::<usize>().ok());
let minl =
getsparam("POWERLEVEL9K_VCS_SHORTEN_MIN_LENGTH").and_then(|v| v.parse::<usize>().ok());
let strategy = p9k_global("VCS_SHORTEN_STRATEGY", "");
if let (Some(sl), Some(minl)) = (sl, minl) {
let n = branch.chars().count();
if n > minl
&& n > sl
&& (strategy == "truncate_middle" || strategy == "truncate_from_right")
{
let delim = decode_g(&p9k_global("VCS_SHORTEN_DELIMITER", "\u{2026}"));
let head: String = branch.chars().take(sl).collect();
let mut out = format!("{}{delim}", esc_pct(&head)); if strategy == "truncate_middle" {
let tail: String = branch.chars().skip(n.saturating_sub(sl)).collect();
out.push_str(&esc_pct(&tail)); }
return out;
}
}
esc_pct(branch) }
fn publish_vcs_status(gs: &git::GitStatus) {
let has = |n: i64| if n > 0 { 1 } else { 0 };
let has_dirty = |n: i64| if gs.dirty_unknown { -1 } else { has(n) };
let (encoding, summary) = git::commit_message(Path::new(&gs.workdir), &gs.commit);
for (k, v) in [
("VCS_STATUS_LOCAL_BRANCH", gs.branch.clone()),
("VCS_STATUS_REMOTE_BRANCH", gs.remote_branch.clone()),
("VCS_STATUS_TAG", gs.tag.clone()),
("VCS_STATUS_COMMIT", gs.commit.clone()),
("VCS_STATUS_REMOTE_URL", gs.remote_url.clone()),
("VCS_STATUS_WORKDIR", gs.workdir.clone()),
("VCS_STATUS_ACTION", gs.action.clone()),
("VCS_STATUS_COMMITS_AHEAD", gs.ahead.to_string()),
("VCS_STATUS_COMMITS_BEHIND", gs.behind.to_string()),
("VCS_STATUS_REMOTE_NAME", gs.remote_name.clone()),
("VCS_STATUS_PUSH_REMOTE_NAME", gs.push_remote_name.clone()),
("VCS_STATUS_PUSH_REMOTE_URL", gs.push_remote_url.clone()),
("VCS_STATUS_PUSH_COMMITS_AHEAD", gs.push_ahead.to_string()),
("VCS_STATUS_PUSH_COMMITS_BEHIND", gs.push_behind.to_string()),
("VCS_STATUS_INDEX_SIZE", gs.index_size.to_string()),
("VCS_STATUS_NUM_STAGED_NEW", gs.staged_new.to_string()),
("VCS_STATUS_NUM_STAGED_DELETED", gs.staged_deleted.to_string()),
("VCS_STATUS_NUM_UNSTAGED_DELETED", gs.unstaged_deleted.to_string()),
("VCS_STATUS_NUM_SKIP_WORKTREE", gs.skip_worktree.to_string()),
("VCS_STATUS_NUM_ASSUME_UNCHANGED", gs.assume_unchanged.to_string()),
("VCS_STATUS_COMMIT_ENCODING", encoding),
("VCS_STATUS_COMMIT_SUMMARY", summary),
("VCS_STATUS_STASHES", gs.stashes.to_string()),
("VCS_STATUS_NUM_STAGED", gs.staged.to_string()),
("VCS_STATUS_NUM_UNSTAGED", gs.unstaged.to_string()),
("VCS_STATUS_NUM_UNTRACKED", gs.untracked.to_string()),
("VCS_STATUS_NUM_CONFLICTED", gs.conflicted.to_string()),
("VCS_STATUS_HAS_STAGED", has(gs.staged).to_string()),
("VCS_STATUS_HAS_UNSTAGED", has_dirty(gs.unstaged).to_string()),
("VCS_STATUS_HAS_UNTRACKED", has_dirty(gs.untracked).to_string()),
("VCS_STATUS_HAS_CONFLICTED", has_dirty(gs.conflicted).to_string()),
] {
let _ = crate::ported::params::setsparam(k, &v);
}
}
const VCS_REMOTE_DOMAINS: [(&str, &str); 15] = [
("archlinux.org", "VCS_GIT_ARCHLINUX_ICON"),
("dev.azure.com|visualstudio.com", "VCS_GIT_AZURE_ICON"),
("bitbucket.org", "VCS_GIT_BITBUCKET_ICON"),
("codeberg.org", "VCS_GIT_CODEBERG_ICON"),
("debian.org", "VCS_GIT_DEBIAN_ICON"),
("freebsd.org", "VCS_GIT_FREEBSD_ICON"),
("freedesktop.org", "VCS_GIT_FREEDESKTOP_ICON"),
("gitea.com|gitea.io", "VCS_GIT_GITEA_ICON"),
("github.com", "VCS_GIT_GITHUB_ICON"),
("gitlab.com", "VCS_GIT_GITLAB_ICON"),
("gnome.org", "VCS_GIT_GNOME_ICON"),
("gnu.org", "VCS_GIT_GNU_ICON"),
("kde.org", "VCS_GIT_KDE_ICON"),
("kernel.org", "VCS_GIT_LINUX_ICON"),
("sr.ht", "VCS_GIT_SOURCEHUT_ICON"),
];
fn remote_url_matches_domain(url: &str, domains: &str) -> bool {
let url = url.to_lowercase();
let mut bases: Vec<&str> = vec![url.as_str()];
if let Some((scheme, rest)) = url.split_once("://") {
let mut sc = scheme.chars();
let ok = sc.next().is_some_and(|c| c.is_ascii_alphanumeric())
&& sc.all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '.' | '-'));
if ok {
bases.push(rest);
}
}
bases.iter().any(|base| {
let mut starts = vec![0usize];
for (i, c) in base.char_indices() {
if matches!(c, ':' | '/' | '?' | '#') {
break;
}
if c == '.' || c == '@' {
starts.push(i + 1);
}
}
starts.into_iter().any(|st| {
domains.split('|').any(|d| {
base[st..]
.strip_prefix(d)
.is_some_and(|tail| tail.is_empty() || tail.starts_with(['/', ':', '?', '#']))
})
})
})
}
fn vcs_remote_icon_key(url: &str) -> String {
let user = p9k_global_arr("VCS_GIT_REMOTE_ICONS");
if user.is_empty() {
for (domains, key) in VCS_REMOTE_DOMAINS {
if remote_url_matches_domain(url, domains) {
return key.to_string();
}
}
return "VCS_GIT_ICON".to_string();
}
for pair in user.chunks(2) {
let mut pat = pair[0].clone();
crate::ported::glob::tokenize(&mut pat);
if let Some(prog) = crate::ported::pattern::patcompile(&pat, 0, None) {
if crate::ported::pattern::pattry(&prog, url) {
return pair.get(1).cloned().unwrap_or_default();
}
}
}
String::new()
}
fn vcs_segments() -> Vec<Segment> {
let mut backends = p9k_global_arr("VCS_BACKENDS");
if backends.is_empty() {
backends.push("git".to_string()); }
let others: Vec<String> = backends.iter().filter(|b| *b != "git").cloned().collect();
if !backends.iter().any(|b| b == "git") {
return super::vcs_other::vcs_info_segments(&others);
}
let cwd = cwd();
let mut gs = match git::git_status_for(Path::new(&cwd)) {
Some(g) => g,
None => return super::vcs_other::vcs_info_segments(&others),
};
let disabled = getsparam("POWERLEVEL9K_VCS_DISABLED_WORKDIR_PATTERN").unwrap_or_default();
if !disabled.is_empty() && glob_match(&disabled, &gs.workdir, &home_dir()) {
return super::vcs_other::vcs_info_segments(&others);
}
let hooks = {
let h = p9k_global_arr("VCS_GIT_HOOKS");
if h.is_empty() {
vec![
"vcs-detect-changes".to_string(),
"git-untracked".to_string(),
"git-aheadbehind".to_string(),
"git-stash".to_string(),
"git-remotebranch".to_string(),
"git-tagname".to_string(),
]
} else {
h
}
};
let hook = |n: &str| hooks.iter().any(|h| h == n);
if !hook("git-untracked") {
gs.untracked = 0; }
if !hook("git-aheadbehind") {
gs.ahead = 0; gs.behind = 0;
}
if !hook("git-stash") {
gs.stashes = 0; }
if !hook("git-remotebranch") {
gs.remote_branch.clear(); }
if !hook("git-tagname") {
gs.tag.clear(); }
let ahead_max = global_int("VCS_COMMITS_AHEAD_MAX_NUM", -1); let behind_max = global_int("VCS_COMMITS_BEHIND_MAX_NUM", -1); if ahead_max >= 0 && gs.ahead > ahead_max {
gs.ahead = ahead_max;
}
if behind_max >= 0 && gs.behind > behind_max {
gs.behind = behind_max;
}
let state = vcs_state_for(&gs);
publish_vcs_status(&gs);
let icon = if hook("vcs-detect-changes") {
let icon_key = vcs_remote_icon_key(&gs.remote_url);
apply_visual_identifier("vcs", Some(state), seg_icon("vcs", Some(state), &icon_key))
} else {
None
};
if global_bool("VCS_DISABLE_GITSTATUS_FORMATTING", false) {
let bg = p9k_param(
"vcs",
Some(state),
"BACKGROUND",
vcs_state_default_bg(state),
);
let fg = p9k_param("vcs", Some(state), "FOREGROUND", color1());
return vec![Segment {
name: "vcs".to_string(),
state: Some(state.to_string()),
content: String::new(),
icon,
fg,
bg,
}];
}
let mut parts: Vec<(&'static str, String)> = Vec::new();
let mut ws = "";
if global_bool("SHOW_CHANGESET", false) || gs.branch.is_empty() {
let hash_len = global_int("CHANGESET_HASH_LENGTH", 8).max(0) as usize; let commit: String = gs.commit.chars().take(hash_len).collect();
let commit = if commit.is_empty() {
"HEAD".to_string() } else {
commit
};
let icon = seg_icon("vcs", Some(state), "VCS_COMMIT_ICON"); parts.push(("COMMIT", format!("{icon}{commit}")));
ws = " ";
}
if !gs.branch.is_empty() {
let mut b = ws.to_string();
if !global_bool("HIDE_BRANCH_ICON", false) {
b.push_str(&seg_icon("vcs", Some(state), "VCS_BRANCH_ICON")); }
b.push_str(&shorten_branch(&gs.branch));
parts.push(("BRANCH", b)); }
if !global_bool("VCS_HIDE_TAGS", false) && !gs.tag.is_empty() {
let icon = seg_icon("vcs", Some(state), "VCS_TAG_ICON"); parts.push(("TAG", format!(" {icon}{}", esc_pct(&gs.tag))));
}
if !gs.action.is_empty() {
parts.push(("ACTION", format!(" | {}", esc_pct(&gs.action))));
} else {
if !gs.remote_branch.is_empty() && gs.remote_branch != gs.branch {
let icon = seg_icon("vcs", Some(state), "VCS_REMOTE_BRANCH_ICON"); parts.push((
"REMOTE_BRANCH",
format!(" {icon}{}", esc_pct(&gs.remote_branch)),
));
}
if gs.staged > 0 || gs.unstaged > 0 || gs.untracked > 0 {
let dirty = seg_icon("vcs", Some(state), "VCS_DIRTY_ICON"); parts.push(("DIRTY", dirty));
if gs.staged > 0 {
let mut t = seg_icon("vcs", Some(state), "VCS_STAGED_ICON"); if global_int("VCS_STAGED_MAX_NUM", 1) != 1 {
t.push_str(&gs.staged.to_string()); }
parts.push(("STAGED", format!(" {t}")));
}
if gs.unstaged > 0 {
let mut t = seg_icon("vcs", Some(state), "VCS_UNSTAGED_ICON"); if global_int("VCS_UNSTAGED_MAX_NUM", 1) != 1 {
t.push_str(&gs.unstaged.to_string()); }
parts.push(("UNSTAGED", format!(" {t}")));
}
if gs.untracked > 0 {
let mut t = seg_icon("vcs", Some(state), "VCS_UNTRACKED_ICON"); if global_int("VCS_UNTRACKED_MAX_NUM", 1) != 1 {
t.push_str(&gs.untracked.to_string()); }
parts.push(("UNTRACKED", format!(" {t}")));
}
}
if gs.behind > 0 {
let mut t = seg_icon("vcs", Some(state), "VCS_INCOMING_CHANGES_ICON");
if behind_max != 1 {
t.push_str(&gs.behind.to_string()); }
parts.push(("INCOMING_CHANGES", format!(" {t}")));
}
if gs.ahead > 0 {
let mut t = seg_icon("vcs", Some(state), "VCS_OUTGOING_CHANGES_ICON");
if ahead_max != 1 {
t.push_str(&gs.ahead.to_string()); }
parts.push(("OUTGOING_CHANGES", format!(" {t}")));
}
if gs.stashes > 0 {
let icon = seg_icon("vcs", Some(state), "VCS_STASH_ICON");
parts.push(("STASH", format!(" {icon}{}", gs.stashes)));
}
}
let bg = p9k_param(
"vcs",
Some(state),
"BACKGROUND",
vcs_state_default_bg(state),
);
let fg = p9k_param("vcs", Some(state), "FOREGROUND", color1());
let any_styled = parts.iter().any(|(p, _)| vcs_part_fg(state, p).is_some());
let content = if any_styled {
let base_style = format!("%b{}", bgesc(&bg)); parts
.iter()
.map(|(p, text)| {
let part_fg = vcs_part_fg(state, p).unwrap_or_else(|| fg.clone());
format!("{base_style}{}{text}", fgesc(&part_fg)) })
.collect::<String>()
} else {
parts.iter().map(|(_, t)| t.as_str()).collect::<String>()
};
vec![Segment {
name: "vcs".to_string(),
state: Some(state.to_string()),
content,
icon,
fg,
bg,
}]
}
fn contract_home(cwd: &str, home: &str) -> String {
if home.is_empty() || home == "/" {
return cwd.to_string();
}
if cwd == home {
return "~".to_string();
}
if let Some(rest) = cwd.strip_prefix(home) {
if rest.starts_with('/') {
return format!("~{rest}");
}
}
cwd.to_string()
}
fn shorten_delim_len(delim: &str) -> usize {
let d = global_int("SHORTEN_DELIMITER_LENGTH", -1); if d >= 0 {
d as usize
} else {
delim.chars().count()
}
}
fn shorten_default(parts: &mut Vec<String>, shortenlen: i64) {
if shortenlen <= 0 {
return;
}
let sl = shortenlen as usize;
let mut len = parts.len();
if parts.first().is_some_and(|p| p.is_empty()) {
len -= 1;
}
if len > sl {
let keep_from = parts.len() - sl;
parts.splice(0..keep_from, [MARK_ELIDE.to_string()]);
}
}
fn shorten_to_last(parts: &mut Vec<String>, p: &str, mut shortenlen: i64) -> bool {
if shortenlen <= 0 {
shortenlen = 1; }
let sl = shortenlen as usize;
if parts.len() > sl + 1 || (!p.starts_with('/') && parts.len() > sl) {
let keep_from = parts.len().saturating_sub(sl);
parts.drain(0..keep_from);
return true; }
false
}
fn shorten_first_and_last(parts: &mut [String], p: &str, shortenlen: i64) {
if shortenlen <= 0 {
return;
}
let sl = shortenlen as usize;
let mut i = sl + 1;
if p.starts_with('/') {
i += 1;
}
while i <= parts.len().saturating_sub(sl) {
parts[i - 1] = MARK_ELIDE.to_string();
i += 1;
}
}
fn shorten_middle_or_right(parts: &mut [String], shortenlen: i64, delim: &str, middle: bool) {
if shortenlen <= 0 {
return;
}
let pref = shortenlen as usize;
let suf = if middle { pref } else { 0 }; let d = shorten_delim_len(delim);
let n = parts.len();
for part in parts.iter_mut().take(n.saturating_sub(1)).skip(1) {
let chars: Vec<char> = part.chars().collect();
if chars.len() > pref + suf + d {
let mut out: String = chars[..pref].iter().collect();
out.push(MARK_ELIDE);
out.extend(&chars[chars.len() - suf..]);
*part = out;
}
}
}
fn shorten_absolute(parts: &mut Vec<String>, p: &str, shortenlen: i64, delim: &str) {
let plen = p.chars().count() as i64;
if shortenlen <= 0 || plen <= shortenlen {
return;
}
let dl = shorten_delim_len(delim) as i64;
if plen <= shortenlen + dl {
return; }
let mut n = shortenlen as usize; let mut i = parts.len(); loop {
let dir: Vec<char> = parts[i - 1].chars().collect();
let len = dir.len() + usize::from(i > 1);
if len <= n {
n -= len; i -= 1; if i == 0 {
break;
}
} else {
let tail: String = if n == 0 {
String::new()
} else {
dir[dir.len() - n..].iter().collect()
};
parts[i - 1] = format!("{MARK_ELIDE}{tail}");
parts.drain(0..i - 1);
break;
}
}
}
fn glob_match(pattern: &str, s: &str, home: &str) -> bool {
for alt in pattern.split('|') {
let expanded = if alt == "~" {
home.to_string()
} else if let Some(rest) = alt.strip_prefix("~/") {
format!("{home}/{rest}")
} else {
alt.to_string()
};
if expanded == s {
return true;
}
let mut expanded = expanded;
crate::ported::glob::tokenize(&mut expanded);
if let Some(prog) = crate::ported::pattern::patcompile(&expanded, 0, None) {
if crate::ported::pattern::pattry(&prog, s) {
return true;
}
}
}
false
}
fn dir_param_exists(state: Option<&str>, param: &str) -> bool {
if getsparam(&format!("POWERLEVEL9K_DIR_{param}")).is_some() {
return true;
}
if let Some(st) = state {
if getsparam(&format!("POWERLEVEL9K_DIR_{st}_{param}")).is_some() {
return true;
}
}
false
}
fn dir_classes() -> Vec<(String, String, String)> {
let user = p9k_global_arr("DIR_CLASSES");
if !user.is_empty() {
return user
.chunks(3)
.filter(|c| c.len() == 3)
.map(|c| {
(
c[0].clone(),
c[1].to_ascii_uppercase(),
decode_g(&c[2]), )
})
.collect();
}
vec![
(
"/etc|/etc/*".to_string(),
"ETC".to_string(),
seg_icon("dir", Some("ETC"), "ETC_ICON"),
),
(
"~".to_string(),
"HOME".to_string(),
seg_icon("dir", Some("HOME"), "HOME_ICON"),
),
(
"~/*".to_string(),
"HOME_SUBFOLDER".to_string(),
seg_icon("dir", Some("HOME_SUBFOLDER"), "HOME_SUB_ICON"),
),
(
"*".to_string(),
"DEFAULT".to_string(),
seg_icon("dir", Some("DEFAULT"), "FOLDER_ICON"),
),
]
}
#[derive(Debug, Clone)]
pub struct DirUnique {
pub variants: Vec<String>,
pub saved: Vec<i64>,
}
thread_local! {
static DIR_UNIQUE: std::cell::RefCell<Vec<DirUnique>> =
const { std::cell::RefCell::new(Vec::new()) };
}
pub fn take_dir_unique() -> Vec<DirUnique> {
DIR_UNIQUE.with(|d| std::mem::take(&mut *d.borrow_mut()))
}
fn term_has_href() -> bool {
match getsparam("VTE_VERSION").and_then(|v| v.parse::<i64>().ok()) {
Some(v) => !((1..=4602).contains(&v) || v == 4801),
None => true,
}
}
fn url_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for b in s.bytes() {
if b.is_ascii_alphanumeric() || b"/:_.-!'()~".contains(&b) {
out.push(b as char);
} else {
out.push_str(&format!("%%{b:02X}"));
}
}
out
}
fn dynamic_named_parts(p: &str, cwd: &str) -> Option<Vec<String>> {
let mut funcs = vec!["zsh_directory_name".to_string()];
funcs.extend(crate::ported::params::getaparam("zsh_directory_name_functions").unwrap_or_default());
for func in funcs {
if crate::ported::utils::getshfunc(&func).is_none() {
continue;
}
let Some(reply) = crate::ported::utils::subst_string_by_func(&func, Some("d"), cwd) else {
continue;
};
let Some(name) = reply.first() else { continue };
let head = format!("~[{name}]");
if let Some(rest) = p.strip_prefix(&head) {
let mut parts = vec![head.clone()];
parts.extend(rest.split('/').filter(|c| !c.is_empty()).map(String::from));
return Some(parts);
}
}
None
}
fn dir_has_entry_matching(dir: &str, pat: &str) -> bool {
let mut t = pat.to_string();
crate::ported::glob::tokenize(&mut t);
let Some(prog) = crate::ported::pattern::patcompile(&t, 0, None) else {
return false;
};
std::fs::read_dir(dir).is_ok_and(|rd| {
rd.flatten()
.any(|e| crate::ported::pattern::pattry(&prog, &e.file_name().to_string_lossy()))
})
}
fn dir_head(dir: &str) -> String {
match dir.rfind('/') {
Some(0) => "/".to_string(),
Some(i) => dir[..i].to_string(),
None => ".".to_string(),
}
}
const DEFAULT_FOLDER_MARKER: &str = "(.bzr|.citc|.git|.hg|.node-version|.python-version|.ruby-version|.shorten_folder_marker|.svn|.terraform|CVS|Cargo.toml|composer.json|go.mod|package.json)";
fn shorten_folder_marker(parts: &mut Vec<String>, cwd: &str) {
let marker = p9k_global("SHORTEN_FOLDER_MARKER", DEFAULT_FOLDER_MARKER);
if marker.is_empty() {
return;
}
let mut dir = cwd.to_string();
let mut m: Vec<usize> = Vec::new(); let mut i = parts.len().saturating_sub(1);
while i > 1 {
dir = dir_head(&dir);
if dir_has_entry_matching(&dir, &marker) {
m.push(i);
}
i -= 1;
}
m.push(1);
for k in 0..m.len() - 1 {
if m[k] - m[k + 1] > 2 {
parts.splice(m[k + 1]..m[k] - 1, [MARK_ELIDE.to_string()]);
}
}
}
fn dir_package_name(cwd: &str, nparts: usize) -> Option<(usize, String)> {
use std::sync::{Mutex, OnceLock};
static CACHE: OnceLock<Mutex<std::collections::HashMap<String, (std::time::SystemTime, String)>>> =
OnceLock::new();
crate::extensions::p10k::segments_sys::cmd_on_path("jq")?;
let pats = match crate::ported::params::getaparam("POWERLEVEL9K_DIR_PACKAGE_FILES") {
Some(v) => v,
None => match getsparam("POWERLEVEL9K_DIR_PACKAGE_FILES") {
Some(s) => vec![s],
None => vec!["package.json".to_string(), "composer.json".to_string()],
},
};
if pats.is_empty() {
return None;
}
let cache = CACHE.get_or_init(|| Mutex::new(std::collections::HashMap::new()));
let mut dir = cwd.to_string();
for levels in (1..=nparts).rev() {
for pat in &pats {
let mut t = pat.clone();
crate::ported::glob::tokenize(&mut t);
let Some(prog) = crate::ported::pattern::patcompile(&t, 0, None) else {
continue;
};
let mut names: Vec<String> = std::fs::read_dir(&dir)
.map(|rd| {
rd.flatten()
.map(|e| e.file_name().to_string_lossy().into_owned())
.filter(|n| crate::ported::pattern::pattry(&prog, n))
.collect()
})
.unwrap_or_default();
names.sort();
for n in names {
let file = format!("{dir}/{n}");
let Ok(mtime) = std::fs::metadata(&file).and_then(|m| m.modified()) else {
continue;
};
let hit = cache.lock().ok().and_then(|c| match c.get(&file) {
Some((t, name)) if *t == mtime => Some(name.clone()),
_ => None,
});
let name = match hit {
Some(name) => name,
None => {
let name = std::fs::File::open(&file)
.ok()
.and_then(|f| {
std::process::Command::new("jq")
.args(["-j", ".name | select(. != null)"])
.stdin(f)
.stderr(std::process::Stdio::null())
.output()
.ok()
})
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).into_owned())
.unwrap_or_default();
if let Ok(mut c) = cache.lock() {
c.insert(file.clone(), (mtime, name.clone()));
}
name
}
};
if !name.is_empty() {
return Some((levels, name));
}
}
}
dir = dir_head(&dir);
}
None
}
fn unique_dir_prefix(parent: &str, prefix: &str) -> bool {
let Ok(rd) = std::fs::read_dir(if parent.is_empty() { "/" } else { parent }) else {
return false;
};
let mut n = 0;
for e in rd.flatten() {
if !e.file_name().to_string_lossy().starts_with(prefix) {
continue;
}
if std::fs::metadata(e.path()).is_ok_and(|m| m.is_dir()) {
n += 1;
if n > 1 {
return false;
}
}
}
n == 1
}
fn unique_prefix_len(parent: &str, rsub: &str, d: usize) -> usize {
use std::sync::{Mutex, OnceLock};
static CACHE: OnceLock<Mutex<std::collections::HashMap<String, (std::time::SystemTime, usize)>>> =
OnceLock::new();
let cache = CACHE.get_or_init(|| Mutex::new(std::collections::HashMap::new()));
let key = format!("{parent}\0{rsub}\0{d}");
let mtime = std::fs::metadata(if parent.is_empty() { "/" } else { parent })
.and_then(|m| m.modified())
.ok();
if let Some(mt) = mtime {
if let Some((t, j)) = cache.lock().ok().and_then(|c| c.get(&key).cloned()) {
if t == mt {
return j;
}
}
}
let chars: Vec<char> = rsub.chars().collect();
let mut j = chars.iter().position(|&c| c != '.').map_or(chars.len() + 1, |i| i + 1);
while j + d < chars.len() {
let prefix: String = chars[..j].iter().collect();
if unique_dir_prefix(parent, &prefix) {
break;
}
j += 1;
}
if let (Some(mt), Ok(mut c)) = (mtime, cache.lock()) {
c.insert(key, (mt, j));
}
j
}
fn shorten_to_unique(parts: &mut Vec<String>, p: &str, cwd: &str) -> (Vec<(usize, String, i64)>, bool) {
let delim = match getsparam("POWERLEVEL9K_SHORTEN_DELIMITER") {
Some(d) => decode_g(&d),
None => "*".to_string(),
};
let mut shortenlen = match getsparam("POWERLEVEL9K_SHORTEN_DIR_LENGTH") {
Some(v) if !v.is_empty() => v.trim().parse::<i64>().unwrap_or(0),
_ => 1,
};
if shortenlen < 0 {
shortenlen = 1;
}
let sl = shortenlen as usize;
let mut tbm = p9k_global("DIR_TRUNCATE_BEFORE_MARKER", "");
if tbm == "first" || tbm == "last" {
tbm.push_str(":0");
}
let tbm_ok = tbm.split_once(':').is_some_and(|(w, n)| {
(w == "first" || w == "last")
&& n.strip_prefix('-').unwrap_or(n).chars().all(|c| c.is_ascii_digit())
&& !n.strip_prefix('-').unwrap_or(n).is_empty()
});
let folder_marker = p9k_global("SHORTEN_FOLDER_MARKER", DEFAULT_FOLDER_MARKER);
if !tbm_ok || folder_marker.is_empty() {
tbm.clear(); }
let n = parts.len();
let mut i = 2usize; let mut e = n as i64 - shortenlen;
let mut orig: Vec<String> = Vec::new();
if !tbm.is_empty() {
e += shortenlen;
orig.push(parts.get(1).cloned().unwrap_or_default()); let take = sl.min(n);
orig.extend(parts[n - take..].iter().cloned());
} else if p.starts_with('/') {
i += 1; }
let rtail_len = if i <= n {
parts[i - 1..].join("/").chars().count()
} else {
0
};
let cwd_chars: Vec<char> = cwd.chars().collect();
let keep = cwd_chars.len().saturating_sub(rtail_len + 1);
let mut parent: String = cwd_chars[..keep].iter().collect();
if i >= 2 && i - 2 < n && !parts[i - 2].is_empty() {
parts[i - 2].push(MARK_ANCHOR);
}
let d = shorten_delim_len(&delim);
let mut steps: Vec<(usize, String, i64)> = Vec::new();
while (i as i64) <= e && i <= n {
let sub = parts[i - 1].clone();
let dir = format!("{parent}/{sub}");
if !folder_marker.is_empty() && dir_has_entry_matching(&dir, &folder_marker) {
parts[i - 1].push(MARK_ANCHOR); } else {
let j = unique_prefix_len(&parent, &sub, d);
let chars: Vec<char> = sub.chars().collect();
let tail: String = chars.iter().skip(j).collect();
let saved = str_width(&tail) as i64 - d as i64; if saved > 0 {
let prefix: String = chars.iter().take(j).collect();
steps.push((i - 1, format!("{MARK_UNIQ}{prefix}{MARK_ELIDE}{MARK_UNIQ}"), saved));
}
}
parent.push('/');
parent.push_str(&sub);
i += 1;
}
let mut fake_first = false;
if !tbm.is_empty() {
let (which, off) = tbm.split_once(':').unwrap_or(("first", "0"));
let off: i64 = off.parse().unwrap_or(0);
let is_anchor = |s: &String| s.ends_with(MARK_ANCHOR);
let e2 = if which == "last" {
parts.iter().rposition(is_anchor).map_or(0, |x| x as i64 + 1) + off
} else {
parts.iter().skip(1).position(is_anchor).map_or(parts.len() as i64 + 1, |x| x as i64 + 2) + off
};
if e2 > 1 && e2 as usize <= parts.len() {
let cut = e2 as usize - 1;
parts.drain(0..cut);
steps.retain_mut(|(idx, _, _)| {
if *idx < cut {
false
} else {
*idx -= cut;
true
}
});
fake_first = true;
} else if p.starts_with('/') && p.len() > 1 && parts.len() > 1 {
parts[1] = format!("{}{MARK_ANCHOR}", orig[0]);
steps.retain(|(idx, _, _)| *idx != 1);
}
let cnt = parts.len().min(sl);
for back in (1..=cnt).rev() {
if back > orig.len() {
continue;
}
let at = parts.len() - back;
parts[at] = format!("{}{MARK_ANCHOR}", orig[orig.len() - back]);
steps.retain(|(idx, _, _)| *idx != at);
}
} else {
for part in parts.iter_mut().skip(i - 1) {
part.push(MARK_ANCHOR);
}
}
(steps, fake_first)
}
fn str_width(s: &str) -> usize {
s.chars()
.map(|c| crate::ported::zsh_h::WCWIDTH(c).max(0) as usize)
.sum()
}
fn dir_segments() -> Vec<Segment> {
let cwd = cwd();
let home = home_dir();
let mut dynamic_parts: Option<Vec<String>> = None;
let p = if global_bool("DIR_PATH_ABSOLUTE", false) {
cwd.clone()
} else if crate::ported::options::opt_state_get("autonamedirs").unwrap_or(false) {
contract_home(&cwd, &home)
} else {
let abbrev = crate::ported::prompt::promptpath(&cwd, 0, true, &home);
if abbrev.starts_with("~[") {
dynamic_parts = dynamic_named_parts(&abbrev, &cwd);
if dynamic_parts.is_some() {
abbrev
} else {
cwd.clone() }
} else {
abbrev
}
};
let mut parts: Vec<String> = dynamic_parts.unwrap_or_else(|| p.split('/').map(String::from).collect());
let mut fake_first = false; let mut unique_steps: Vec<(usize, String, i64)> = Vec::new();
let shortenlen = global_int("SHORTEN_DIR_LENGTH", -1);
let mut delim = match getsparam("POWERLEVEL9K_SHORTEN_DELIMITER") {
Some(d) => decode_g(&d),
None => "\u{2026}".to_string(),
};
let strategy = p9k_global("SHORTEN_STRATEGY", "");
match strategy.as_str() {
"truncate_absolute" | "truncate_absolute_chars" => {
shorten_absolute(&mut parts, &p, shortenlen, &delim); }
"truncate_with_package_name" | "truncate_middle" | "truncate_from_right" => {
if strategy == "truncate_with_package_name" {
if let Some((levels, name)) = dir_package_name(&cwd, parts.len()) {
parts.splice(0..levels, [name]); fake_first = true; }
}
shorten_middle_or_right(
&mut parts,
shortenlen,
&delim,
strategy == "truncate_middle", );
}
"truncate_to_last" => {
fake_first = shorten_to_last(&mut parts, &p, shortenlen); }
"truncate_to_first_and_last" => {
shorten_first_and_last(&mut parts, &p, shortenlen); }
"truncate_to_unique" => {
if getsparam("POWERLEVEL9K_SHORTEN_DELIMITER").is_none() {
delim = "*".to_string(); }
let (st, ff) = shorten_to_unique(&mut parts, &p, &cwd);
unique_steps = st;
fake_first = ff;
}
"truncate_with_folder_marker" => {
shorten_folder_marker(&mut parts, &cwd); }
_ => shorten_default(&mut parts, shortenlen), }
let show_writable = match p9k_global("DIR_SHOW_WRITABLE", "").as_str() {
"true" => 1, "v2" => 2, "v3" => 3, _ => 0, };
let mut w = i32::from(show_writable != 0 && !path_writable(&cwd)); if w != 0 && show_writable > 2 && !Path::new(&cwd).exists() {
w = 2; }
let mut state: Option<String> = None;
let mut icon = String::new();
for (pat, class, glyph) in dir_classes() {
if glob_match(&pat, &cwd, &home) {
if !class.is_empty() {
state = Some(class); }
icon = glyph; break;
}
}
if w != 0 {
if show_writable == 1 {
state = Some("NOT_WRITABLE".to_string()); } else if w == 2 {
state = Some(match state {
Some(s) => format!("{s}_NON_EXISTENT"), None => "NON_EXISTENT".to_string(),
});
} else {
state = Some(match state {
Some(s) => format!("{s}_NOT_WRITABLE"), None => "NOT_WRITABLE".to_string(),
});
}
icon = seg_icon("dir", state.as_deref(), "LOCK_ICON"); }
let state_ref = state.as_deref();
let bg = p9k_param("dir", state_ref, "BACKGROUND", "blue"); let fg = p9k_param("dir", state_ref, "FOREGROUND", color1()); let style = format!("%b{}{}", bgesc(&bg), fgesc(&fg));
let format_parts = |mut parts: Vec<String>| -> String {
for part in parts.iter_mut() {
*part = esc_pct(part);
}
let abbrev = decode_g(&p9k_global("HOME_FOLDER_ABBREVIATION", "~")); if abbrev != "~" && !fake_first && (p == "~" || p.starts_with("~/")) {
parts[0] = abbrev.clone(); if parts[0].contains('%') {
parts[0].push_str(&style); }
} else if global_bool("DIR_OMIT_FIRST_CHARACTER", false)
&& !fake_first
&& parts.len() > 1
&& parts[0].is_empty()
&& !parts[1].is_empty()
{
parts.remove(0); }
let mut last_style = String::new();
if p9k_param("dir", state_ref, "PATH_HIGHLIGHT_BOLD", "") == "true" {
last_style.push_str("%B"); }
if dir_param_exists(state_ref, "PATH_HIGHLIGHT_FOREGROUND") {
let c = p9k_param("dir", state_ref, "PATH_HIGHLIGHT_FOREGROUND", "");
last_style.push_str(&fgesc(&c)); }
if !last_style.is_empty() {
if let Some(last) = parts.last_mut() {
let restyled = last.replace(MARK_ELIDE, &format!("{MARK_ELIDE}{last_style}"));
*last = format!("{last_style}{restyled}{style}");
}
}
let mut anchor_style = String::new();
if p9k_param("dir", state_ref, "ANCHOR_BOLD", "") == "true" {
anchor_style.push_str("%B"); }
if dir_param_exists(state_ref, "ANCHOR_FOREGROUND") {
let c = p9k_param("dir", state_ref, "ANCHOR_FOREGROUND", "");
anchor_style.push_str(&fgesc(&c)); }
if !anchor_style.is_empty() {
let n = parts.len();
for (i, part) in parts.iter_mut().enumerate() {
if let Some(stripped) = part.strip_suffix(MARK_ANCHOR) {
if last_style.is_empty() || i + 1 < n {
*part = format!("{anchor_style}{stripped}{style}");
} else {
*part = stripped.to_string(); }
}
}
} else {
for part in parts.iter_mut() {
if part.ends_with(MARK_ANCHOR) {
part.pop(); }
}
}
if dir_param_exists(state_ref, "SHORTENED_FOREGROUND") {
let sfg = fgesc(&p9k_param("dir", state_ref, "SHORTENED_FOREGROUND", "")); let mut dl = delim.clone();
if dl.contains('%') {
dl.push_str(&style);
dl.push_str(&sfg); }
for part in parts.iter_mut() {
if let Some(i) = part.find(MARK_ELIDE) {
let before = part[..i].to_string();
let after = part[i + MARK_ELIDE.len_utf8()..].to_string();
*part = format!("{sfg}{before}{dl}{after}{style}");
}
*part = part.replace(MARK_UNIQ, ""); }
} else {
let mut dl = delim.clone();
if dl.contains('%') {
dl.push_str(&style); }
for part in parts.iter_mut() {
*part = part.replace(MARK_ELIDE, &dl); *part = part.replace(MARK_UNIQ, ""); }
}
let sep = if cwd == "/" && global_bool("DIR_OMIT_FIRST_CHARACTER", false) {
"/".to_string() } else {
let mut sep = String::new();
if dir_param_exists(state_ref, "PATH_SEPARATOR_FOREGROUND") {
let c = p9k_param("dir", state_ref, "PATH_SEPARATOR_FOREGROUND", "");
sep.push_str(&fgesc(&c)); }
sep.push_str(&decode_g(&p9k_param(
"dir",
state_ref,
"PATH_SEPARATOR",
"/",
))); if sep.contains('%') {
sep.push_str(&style); }
sep
};
let content = parts.join(&sep);
if global_bool("DIR_HYPERLINK", false) && term_has_href() && cwd.starts_with('/') {
return format!(
"%{{\u{1b}]8;;file://{}\u{7}%}}{content}%{{\u{1b}]8;;\u{7}%}}",
url_escape(&cwd)
);
}
content
};
let content = format_parts(parts.clone());
if !unique_steps.is_empty() {
let mut variants = vec![content.clone()];
let mut saved = Vec::new();
let mut cur = parts.clone();
for (idx, short, sv) in &unique_steps {
cur[*idx] = short.clone();
variants.push(format_parts(cur.clone()));
saved.push(*sv);
}
DIR_UNIQUE.with(|d| {
let mut d = d.borrow_mut();
if d.len() >= 8 {
d.clear(); }
d.push(DirUnique { variants, saved });
});
}
let seg_state = state.clone();
let icon = apply_visual_identifier("dir", state_ref, icon);
let content = apply_content_expansion("dir", state_ref, content);
vec![Segment {
name: "dir".to_string(),
state: seg_state,
content,
icon,
fg,
bg,
}]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dir_class_patterns_glob() {
let _g = crate::test_util::global_state_lock();
let home = "/Users/someone";
assert!(glob_match("~", "/Users/someone", home)); assert!(glob_match("~/*", "/Users/someone/Library/Caches", home));
assert!(!glob_match("~/*", "/Users/other/x", home));
assert!(!glob_match("~/*", "/Users/someone", home)); assert!(glob_match("/etc|/etc/*", "/etc/ssh", home));
assert!(glob_match("*", "/opt/homebrew", home)); }
#[test]
fn exit2str_maps_signals_verbosely() {
assert_eq!(exit2str(130), "SIGINT(2)");
assert_eq!(exit2str(129), "SIGHUP(1)");
assert_eq!(exit2str(0), "0");
assert_eq!(exit2str(1), "1");
assert_eq!(exit2str(128), "128");
}
#[test]
fn ip_rate_formatting_matches_p10k() {
assert_eq!(rate_str(0.0), "0 B/s");
assert_eq!(rate_str(512.0), "512 B/s");
assert_eq!(rate_str(1024.0), "1 KiB/s");
assert_eq!(rate_str(1536.0), "1.5 KiB/s");
assert_eq!(rate_str(1024.0 * 1024.0), "1 MiB/s");
assert_eq!(rate_str(200.0 * 1024.0), "200 KiB/s");
}
#[test]
fn publish_vcs_status_sets_params() {
let _g = crate::test_util::global_state_lock();
use crate::ported::params::getsparam;
let gs = git::GitStatus {
branch: "main".into(),
commit: "abc123".into(),
remote_branch: "origin/main".into(),
ahead: 3,
behind: 1,
staged: 2,
unstaged: 4,
untracked: 5,
conflicted: 0,
stashes: 1,
..git::GitStatus::default()
};
publish_vcs_status(&gs);
assert_eq!(
getsparam("VCS_STATUS_LOCAL_BRANCH").as_deref(),
Some("main")
);
assert_eq!(
getsparam("VCS_STATUS_REMOTE_BRANCH").as_deref(),
Some("origin/main")
);
assert_eq!(getsparam("VCS_STATUS_COMMITS_AHEAD").as_deref(), Some("3"));
assert_eq!(getsparam("VCS_STATUS_COMMITS_BEHIND").as_deref(), Some("1"));
assert_eq!(getsparam("VCS_STATUS_NUM_STAGED").as_deref(), Some("2"));
assert_eq!(getsparam("VCS_STATUS_NUM_UNSTAGED").as_deref(), Some("4"));
assert_eq!(getsparam("VCS_STATUS_NUM_UNTRACKED").as_deref(), Some("5"));
assert_eq!(getsparam("VCS_STATUS_STASHES").as_deref(), Some("1"));
assert_eq!(getsparam("VCS_STATUS_HAS_UNSTAGED").as_deref(), Some("1"));
assert_eq!(getsparam("VCS_STATUS_HAS_CONFLICTED").as_deref(), Some("0"));
}
#[test]
fn vcs_state_precedence_matches_p10k() {
let mut gs = git::GitStatus::default();
assert_eq!(vcs_state_for(&gs), "CLEAN");
gs.untracked = 3;
assert_eq!(vcs_state_for(&gs), "UNTRACKED");
gs.unstaged = 1;
assert_eq!(vcs_state_for(&gs), "MODIFIED");
gs.conflicted = 1; assert_eq!(vcs_state_for(&gs), "MODIFIED");
}
#[test]
fn shorten_default_collapses_leading_components() {
let mut parts: Vec<String> = "/usr/local/share/zsh"
.split('/')
.map(String::from)
.collect();
shorten_default(&mut parts, 1);
assert_eq!(parts, vec![MARK_ELIDE.to_string(), "zsh".to_string()]);
let mut parts2: Vec<String> = "/usr/local".split('/').map(String::from).collect();
shorten_default(&mut parts2, -1);
assert_eq!(parts2, vec!["", "usr", "local"]);
}
#[test]
fn shorten_to_last_keeps_tail_and_sets_fake_first() {
let p = "/usr/local/share/zsh";
let mut parts: Vec<String> = p.split('/').map(String::from).collect();
let fake = shorten_to_last(&mut parts, p, 2);
assert!(fake);
assert_eq!(parts, vec!["share", "zsh"]);
}
#[test]
fn shorten_first_and_last_elides_middle() {
let p = "/a/b/c/d/e";
let mut parts: Vec<String> = p.split('/').map(String::from).collect();
shorten_first_and_last(&mut parts, p, 1);
let m = MARK_ELIDE.to_string();
assert_eq!(
parts,
vec![
"".to_string(),
"a".to_string(),
m.clone(),
m.clone(),
m,
"e".to_string(),
]
);
}
#[test]
fn shorten_middle_and_right_squeeze_components() {
let mut parts: Vec<String> =
vec!["".into(), "projects".into(), "deeply".into(), "last".into()];
shorten_middle_or_right(&mut parts, 2, "\u{2026}", false);
assert_eq!(parts[1], format!("pr{MARK_ELIDE}"));
assert_eq!(parts[2], format!("de{MARK_ELIDE}"));
assert_eq!(parts[3], "last");
let mut parts2: Vec<String> = vec!["".into(), "projects".into(), "last".into()];
shorten_middle_or_right(&mut parts2, 2, "\u{2026}", true);
assert_eq!(parts2[1], format!("pr{MARK_ELIDE}ts"));
}
#[test]
fn contract_home_variants() {
assert_eq!(contract_home("/Users/u/x", "/Users/u"), "~/x");
assert_eq!(contract_home("/Users/u", "/Users/u"), "~");
assert_eq!(contract_home("/Users/uv", "/Users/u"), "/Users/uv");
assert_eq!(contract_home("/etc", "/Users/u"), "/etc");
assert_eq!(contract_home("/etc", ""), "/etc");
}
#[test]
fn shorten_absolute_keeps_tail_chars() {
let p = "/usr/local/bin";
let mut parts: Vec<String> = p.split('/').map(String::from).collect();
shorten_absolute(&mut parts, p, 6, "\u{2026}");
assert_eq!(parts, vec![format!("{MARK_ELIDE}al"), "bin".to_string()]);
}
}
#[cfg(test)]
mod remote_host_tests {
use super::looks_remote;
#[test]
fn remote_addresses_and_hostnames() {
assert!(looks_remote("10.0.0.5"));
assert!(looks_remote("(10.0.0.5)"));
assert!(looks_remote("fe80::1ff:fe23:4567:890a"));
assert!(looks_remote("build.example.com"));
}
#[test]
fn local_logins_are_not_remote() {
assert!(!looks_remote(""));
assert!(!looks_remote("localhost"));
assert!(!looks_remote(":0"));
assert!(!looks_remote("console"));
}
}