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::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 color1() -> &'static str {
if p9k_global("COLOR_SCHEME", "dark") == "light" {
"7"
} else {
"0"
}
}
fn color2() -> &'static str {
if p9k_global("COLOR_SCHEME", "dark") == "light" {
"0"
} else {
"7"
}
}
fn env_or_param(name: &str) -> String {
if let Some(v) = getsparam(name) {
return v;
}
std::env::var(name).unwrap_or_default()
}
fn global_bool(name: &str, default: bool) -> bool {
match getsparam(&format!("POWERLEVEL9K_{name}")) {
Some(v) => v == "true",
None => default,
}
}
fn global_int(name: &str, default: i64) -> i64 {
getsparam(&format!("POWERLEVEL9K_{name}"))
.and_then(|v| v.trim().parse::<i64>().ok())
.unwrap_or(default)
}
fn esc_pct(s: &str) -> String {
s.replace('%', "%%")
}
fn decode_g(s: &str) -> String {
getkeystring(s).0
}
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 seg_icon(segment: &str, state: Option<&str>, key: &str) -> String {
let probed = p9k_param(segment, state, key, "\u{1}");
if probed == "\u{1}" {
return icons::icon(key).to_string();
}
let decoded = decode_g(&probed);
let mut ch = decoded.chars();
if ch.next() == Some('\u{8}') && ch.next().is_some() && ch.next().is_none() {
return format!("%{{{decoded}%}}");
}
decoded
}
fn apply_visual_identifier(segment: &str, state: Option<&str>, icon: String) -> Option<String> {
let exp = p9k_param(
segment,
state,
"VISUAL_IDENTIFIER_EXPANSION",
"${P9K_VISUAL_IDENTIFIER}",
);
let resolved = if exp == "${P9K_VISUAL_IDENTIFIER}" {
icon
} else if !exp.contains('$') {
exp } else {
tracing::debug!(
target: "p10k",
segment,
?state,
%exp,
"dynamic VISUAL_IDENTIFIER_EXPANSION unported — using resolved icon"
);
icon
};
if resolved.is_empty() {
None
} else {
Some(resolved)
}
}
fn apply_content_expansion(segment: &str, state: Option<&str>, content: String) -> String {
let exp = p9k_param(segment, state, "CONTENT_EXPANSION", "${P9K_CONTENT}");
if exp == "${P9K_CONTENT}" {
return content;
}
if !exp.contains('$') {
return exp;
}
if exp.contains("${P9K_CONTENT}") {
let out = exp.replace("${P9K_CONTENT}", &content);
if out.contains('$') {
tracing::debug!(
target: "p10k",
segment, ?state, %exp,
"CONTENT_EXPANSION has unevaluated expansions beyond ${{P9K_CONTENT}}"
);
}
return out;
}
tracing::debug!(
target: "p10k",
segment, ?state, %exp,
"dynamic CONTENT_EXPANSION unported — using segment content"
);
content
}
fn param_seg(name: &str) -> &str {
if name == "prompt_char" {
"prompt_prompt_char"
} else {
name
}
}
fn make_segment(
name: &str,
state: Option<&str>,
default_bg: &str,
default_fg: &str,
icon_key: &str,
content: String,
) -> Segment {
let seg = param_seg(name);
let bg = p9k_param(seg, state, "BACKGROUND", default_bg);
let fg = p9k_param(seg, state, "FOREGROUND", default_fg);
let icon_glyph = if icon_key.is_empty() {
String::new()
} else {
seg_icon(seg, state, icon_key)
};
let icon = apply_visual_identifier(seg, state, icon_glyph);
let content = apply_content_expansion(seg, state, content);
Segment {
name: name.to_string(),
state: state.map(|s| s.to_string()),
content,
icon,
fg,
bg,
}
}
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")
}
fn is_ssh() -> bool {
!env_or_param("SSH_CLIENT").is_empty()
|| !env_or_param("SSH_TTY").is_empty()
|| !env_or_param("SSH_CONNECTION").is_empty()
}
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 keymap = match keymap_name.as_str() {
"vicmd" => "VICMD", "vivis" | "vivli" => "VIVIS", _ => "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}", _ => "\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_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 = crate::ported::exec::run_command_substitution(&cmd);
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,
)]
}
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 };
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_ACTION", gs.action.clone()),
("VCS_STATUS_COMMITS_AHEAD", gs.ahead.to_string()),
("VCS_STATUS_COMMITS_BEHIND", gs.behind.to_string()),
("VCS_STATUS_PUSH_COMMITS_AHEAD", "0".to_string()),
("VCS_STATUS_PUSH_COMMITS_BEHIND", "0".to_string()),
("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(gs.unstaged).to_string()),
("VCS_STATUS_HAS_UNTRACKED", has(gs.untracked).to_string()),
("VCS_STATUS_HAS_CONFLICTED", has(gs.conflicted).to_string()),
] {
let _ = crate::ported::params::setsparam(k, &v);
}
}
fn vcs_segments() -> Vec<Segment> {
let mut backends = p9k_global_arr("VCS_BACKENDS");
if backends.is_empty() {
backends.push("git".to_string()); }
if !backends.iter().any(|b| b == "git") {
tracing::debug!(target: "p10k", ?backends, "non-git VCS backends unported");
return vec![];
}
let cwd = cwd();
let disabled = getsparam("POWERLEVEL9K_VCS_DISABLED_WORKDIR_PATTERN").unwrap_or_default();
if !disabled.is_empty() && glob_match(&disabled, &cwd, &home_dir()) {
return vec![];
}
let mut gs = match git::git_status_for(Path::new(&cwd)) {
Some(g) => g,
None => return vec![], };
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);
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());
let icon = apply_visual_identifier(
"vcs",
Some(state),
seg_icon("vcs", Some(state), "VCS_GIT_ICON"),
);
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>()
};
let icon = apply_visual_identifier(
"vcs",
Some(state),
seg_icon("vcs", Some(state), "VCS_GIT_ICON"),
);
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;
}
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"),
),
]
}
fn dir_segments() -> Vec<Segment> {
let cwd = cwd();
let home = home_dir();
let p = if global_bool("DIR_PATH_ABSOLUTE", false) {
cwd.clone()
} else {
let abbrev = crate::ported::prompt::promptpath(&cwd, 0, true, &home);
if abbrev.starts_with("~[") {
cwd.clone() } else {
abbrev
}
};
let mut parts: Vec<String> = p.split('/').map(String::from).collect();
let mut fake_first = false; let shortenlen = global_int("SHORTEN_DIR_LENGTH", -1);
let 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" {
tracing::debug!(
target: "p10k",
"truncate_with_package_name package lookup unported; length squeeze only"
);
}
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" | "truncate_with_folder_marker" => {
tracing::debug!(target: "p10k", %strategy, "dir shorten strategy unported — showing full path");
}
_ => 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));
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) {
tracing::debug!(target: "p10k", "DIR_HYPERLINK unported — plain dir content");
}
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 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()]);
}
}