use crate::ported::params::getsparam;
const PADDED_KEYS: [&str; 7] = [
"LEFT_SEGMENT_END_SEPARATOR", "MULTILINE_LAST_PROMPT_PREFIX", "VCS_TAG_ICON", "VCS_BOOKMARK_ICON", "VCS_COMMIT_ICON", "VCS_BRANCH_ICON", "VCS_REMOTE_BRANCH_ICON", ];
fn icon_raw(name: &str) -> Option<&'static str> {
let glyph = match name {
"RULER_CHAR" => "\u{2500}", "LEFT_SEGMENT_SEPARATOR" => "\u{E0B0}", "RIGHT_SEGMENT_SEPARATOR" => "\u{E0B2}", "LEFT_SEGMENT_END_SEPARATOR" => " ", "LEFT_SUBSEGMENT_SEPARATOR" => "\u{E0B1}", "RIGHT_SUBSEGMENT_SEPARATOR" => "\u{E0B3}", "CARRIAGE_RETURN_ICON" => "\u{21B5}", "ROOT_ICON" => "\u{E614}", "SUDO_ICON" => "\u{F09C} ", "RUBY_ICON" => "\u{F219} ", "AWS_ICON" => "\u{F270} ", "AWS_EB_ICON" => "\u{F1BD}", "BACKGROUND_JOBS_ICON" => "\u{F013} ", "TEST_ICON" => "\u{F188} ", "TODO_ICON" => "\u{2611}", "BATTERY_ICON" => "\u{F240} ", "DISK_ICON" => "\u{F0A0} ", "OK_ICON" => "\u{F00C} ", "FAIL_ICON" => "\u{F00D}", "SYMFONY_ICON" => "\u{E757}", "NODE_ICON" => "\u{E617} ", "NODEJS_ICON" => "\u{E617} ", "MULTILINE_FIRST_PROMPT_PREFIX" => "\u{256D}\u{2500}", "MULTILINE_NEWLINE_PROMPT_PREFIX" => "\u{251C}\u{2500}", "MULTILINE_LAST_PROMPT_PREFIX" => "\u{2570}\u{2500} ", "APPLE_ICON" => "\u{F179}", "WINDOWS_ICON" => "\u{F17A} ", "FREEBSD_ICON" => "\u{F30C} ", "ANDROID_ICON" => "\u{F17B}", "LINUX_ARCH_ICON" => "\u{F303}", "LINUX_KALI_ICON" => "\u{F17C}", "LINUX_ENDEAVOUROS_ICON" => "\u{F17C}", "LINUX_ROCKY_ICON" => "\u{F17C}", "LINUX_ALMALINUX_ICON" => "\u{F17C}", "LINUX_GUIX_ICON" => "\u{F325} ", "LINUX_NEON_ICON" => "\u{F17C}", "CHEZMOI_ICON" => "\u{F015} ", "YAZI_ICON" => "\u{F00B} ", "LF_ICON" => "lf", "ARCH_ICON" => "\u{E266}", "HISTORY_ICON" => "\u{F1DA} ", "LINUX_CENTOS_ICON" => "\u{F304} ", "LINUX_COREOS_ICON" => "\u{F305} ", "LINUX_DEBIAN_ICON" => "\u{F306}", "LINUX_RASPBIAN_ICON" => "\u{F315}", "LINUX_ELEMENTARY_ICON" => "\u{F309} ", "LINUX_FEDORA_ICON" => "\u{F30A} ", "LINUX_GENTOO_ICON" => "\u{F30D} ", "LINUX_MAGEIA_ICON" => "\u{F310}", "LINUX_MINT_ICON" => "\u{F30E} ", "LINUX_NIXOS_ICON" => "\u{F313} ", "LINUX_MANJARO_ICON" => "\u{F312} ", "LINUX_DEVUAN_ICON" => "\u{F307} ", "LINUX_ALPINE_ICON" => "\u{F300} ", "LINUX_AOSC_ICON" => "\u{F301} ", "LINUX_OPENSUSE_ICON" => "\u{F314} ", "LINUX_SABAYON_ICON" => "\u{F317} ", "LINUX_SLACKWARE_ICON" => "\u{F319} ", "LINUX_VOID_ICON" => "\u{F17C}", "LINUX_ARTIX_ICON" => "\u{F17C}", "LINUX_UBUNTU_ICON" => "\u{F31B} ", "LINUX_RHEL_ICON" => "\u{F316} ", "LINUX_AMZN_ICON" => "\u{F270} ", "LINUX_ICON" => "\u{F17C}", "SUNOS_ICON" => "\u{F185} ", "HOME_ICON" => "\u{F015} ", "HOME_SUB_ICON" => "\u{F07C} ", "FOLDER_ICON" => "\u{F115} ", "ETC_ICON" => "\u{F013} ", "NETWORK_ICON" => "\u{F50D} ", "LOAD_ICON" => "\u{F080} ", "SWAP_ICON" => "\u{F464} ", "RAM_ICON" => "\u{F0E4} ", "SERVER_ICON" => "\u{F0AE} ", "VCS_UNTRACKED_ICON" => "\u{F059} ", "VCS_UNSTAGED_ICON" => "\u{F06A} ", "VCS_STAGED_ICON" => "\u{F055} ", "VCS_STASH_ICON" => "\u{F01C} ", "VCS_INCOMING_CHANGES_ICON" => "\u{F01A} ", "VCS_OUTGOING_CHANGES_ICON" => "\u{F01B} ", "VCS_TAG_ICON" => "\u{F02B} ", "VCS_BOOKMARK_ICON" => "\u{F461} ", "VCS_COMMIT_ICON" => "\u{E729} ", "VCS_BRANCH_ICON" => "\u{F126} ", "VCS_REMOTE_BRANCH_ICON" => "\u{E728} ", "VCS_LOADING_ICON" => "", "VCS_GIT_ICON" => "\u{F1D3} ", "VCS_GIT_GITHUB_ICON" => "\u{F113} ", "VCS_GIT_BITBUCKET_ICON" => "\u{E703} ", "VCS_GIT_GITLAB_ICON" => "\u{F296} ", "VCS_GIT_AZURE_ICON" => "\u{FD03} ", "VCS_GIT_ARCHLINUX_ICON" => "\u{F303} ", "VCS_GIT_CODEBERG_ICON" => "\u{F1D3} ", "VCS_GIT_DEBIAN_ICON" => "\u{F306} ", "VCS_GIT_FREEBSD_ICON" => "\u{F30C} ", "VCS_GIT_FREEDESKTOP_ICON" => "\u{F296} ", "VCS_GIT_GNOME_ICON" => "\u{F296} ", "VCS_GIT_GNU_ICON" => "\u{E779} ", "VCS_GIT_KDE_ICON" => "\u{F296} ", "VCS_GIT_LINUX_ICON" => "\u{F17C} ", "VCS_GIT_GITEA_ICON" => "\u{F1D3} ", "VCS_GIT_SOURCEHUT_ICON" => "\u{F1DB} ", "VCS_HG_ICON" => "\u{F0C3} ", "VCS_SVN_ICON" => "\u{E72D}", "RUST_ICON" => "\u{E7A8}", "PYTHON_ICON" => "\u{E73C} ", "SWIFT_ICON" => "\u{E755}", "GO_ICON" => "\u{E626}", "GOLANG_ICON" => "\u{E626}", "PUBLIC_IP_ICON" => "\u{F0AC} ", "LOCK_ICON" => "\u{F023}", "NORDVPN_ICON" => "\u{F023}", "EXECUTION_TIME_ICON" => "\u{F252} ", "SSH_ICON" => "\u{F489} ", "VPN_ICON" => "\u{F023}", "KUBERNETES_ICON" => "\u{2388}", "DROPBOX_ICON" => "\u{F16B} ", "DATE_ICON" => "\u{F073} ", "TIME_ICON" => "\u{F017} ", "JAVA_ICON" => "\u{E738}", "LARAVEL_ICON" => "\u{E73F}", "RANGER_ICON" => "\u{F00B} ", "MIDNIGHT_COMMANDER_ICON" => "mc", "VIM_ICON" => "\u{E62B}", "TERRAFORM_ICON" => "\u{F1BB} ", "PROXY_ICON" => "\u{2194}", "DOTNET_ICON" => "\u{E77F}", "DOTNET_CORE_ICON" => "\u{E77F}", "AZURE_ICON" => "\u{FD03}", "DIRENV_ICON" => "\u{25BC}", "FLUTTER_ICON" => "F", "GCLOUD_ICON" => "\u{F7B7}", "LUA_ICON" => "\u{E620}", "PERL_ICON" => "\u{E769}", "NNN_ICON" => "nnn", "XPLR_ICON" => "xplr", "TIMEWARRIOR_ICON" => "\u{F49B}", "TASKWARRIOR_ICON" => "\u{F4A0} ", "NIX_SHELL_ICON" => "\u{F313} ", "WIFI_ICON" => "\u{F1EB} ", "ERLANG_ICON" => "\u{E7B1} ", "ELIXIR_ICON" => "\u{E62D}", "POSTGRES_ICON" => "\u{E76E}", "PHP_ICON" => "\u{E608}", "HASKELL_ICON" => "\u{E61F}", "PACKAGE_ICON" => "\u{F8D6}", "JULIA_ICON" => "\u{E624}", "SCALA_ICON" => "\u{E737}", "TOOLBOX_ICON" => "\u{E20F} ", _ => return None,
};
Some(glyph)
}
fn icon_padding_none() -> bool {
getsparam("POWERLEVEL9K_ICON_PADDING").as_deref() == Some("none")
}
fn icon_mode() -> String {
match getsparam("POWERLEVEL9K_MODE") {
Some(m) if !m.is_empty() => m,
_ if codeset_is_utf8() => String::new(),
_ => "ascii".to_string(),
}
}
fn codeset_is_utf8() -> bool {
let c = unsafe { std::ffi::CStr::from_ptr(libc::nl_langinfo(libc::CODESET)) };
let name = c.to_string_lossy().to_ascii_lowercase();
name == "utf-8" || name == "utf8"
}
fn codepoint_var(name: &str) -> String {
getsparam(name).unwrap_or_default()
}
fn parse_icon_value(rest: &str, s: &str, q: &str) -> Option<String> {
let chars: Vec<char> = rest.chars().collect();
let mut i = 0;
let mut raw = String::new();
while i < chars.len() {
match chars[i] {
'\'' => {
let j = chars[i + 1..].iter().position(|&c| c == '\'')? + i + 1;
raw.extend(&chars[i + 1..j]);
i = j + 1;
}
'"' => {
let j = chars[i + 1..].iter().position(|&c| c == '"')? + i + 1;
let body: String = chars[i + 1..j].iter().collect();
raw.push_str(&expand_dq(&body, s, q));
i = j + 1;
}
'$' => {
match chars.get(i + 1) {
Some('s') => raw.push_str(s),
Some('q') => raw.push_str(q),
_ => return None,
}
i += 2;
}
'#' => break,
c if c.is_whitespace() => i += 1,
_ => return None,
}
}
Some(unescape_g(&raw))
}
fn expand_dq(body: &str, s: &str, q: &str) -> String {
let mut out = String::new();
let mut rest = body;
while let Some(start) = rest.find("${") {
out.push_str(&rest[..start]);
let after = &rest[start + 2..];
let Some(end) = after.find('}') else { break };
let inner = &after[..end];
if let Some((name, tail)) = inner.split_once(":+") {
let val = codepoint_var(name);
if !val.is_empty() {
let tail = tail
.replace("\\\\", "\\")
.replace(&format!("${name}"), &val)
.replace("$s", s)
.replace("$q", q);
out.push_str(&tail);
}
}
rest = &after[end + 1..];
}
out.push_str(rest);
out
}
fn mode_branch(lines: &[&str], case_at: usize, mode: &str) -> Option<(usize, usize)> {
let end = case_at + lines[case_at..].iter().position(|l| l.trim() == "esac")?;
let mut chosen = None;
let mut default = None;
let mut i = case_at + 1;
while i < end {
let l = lines[i];
if l.starts_with(" ") && !l.starts_with(" ") && l.trim_end().ends_with(')') {
let mut j = i + 1;
while j < end && lines[j].trim() != ";;" {
j += 1;
}
let pats = l.trim().trim_end_matches(')');
if pats.trim() == "*" {
default = Some((i + 1, j));
} else if pats.split('|').any(|p| p.trim().trim_matches('\'') == mode) {
chosen = Some((i + 1, j));
}
i = j + 1;
} else {
i += 1;
}
}
chosen.or(default)
}
fn load_theme_icons(mode: &str, legacy: bool, padding_none: bool) -> Option<std::collections::HashMap<String, String>> {
let root = crate::p10k::p10k_root_dir()?;
let text = std::fs::read_to_string(format!("{root}/internal/icons.zsh")).ok()?;
let lines: Vec<&str> = text.lines().collect();
let (s, q) = if legacy { ("", " ") } else { (" ", "") }; let cases: Vec<usize> = lines
.iter()
.enumerate()
.filter(|(_, l)| l.trim() == "case $POWERLEVEL9K_MODE in")
.map(|(i, _)| i)
.collect();
let (a, b) = mode_branch(&lines, *cases.first()?, mode)?;
let mut map = std::collections::HashMap::new();
let mut in_table = false;
for l in &lines[a..b] {
let t = l.trim();
if t.starts_with("icons=(") {
in_table = true;
} else if in_table && t == ")" {
break;
} else if in_table && !t.is_empty() && !t.starts_with('#') {
let Some((key, rest)) = t.split_once(char::is_whitespace) else {
continue;
};
if let Some(v) = parse_icon_value(rest, s, q) {
map.insert(key.to_string(), v);
}
}
}
if map.is_empty() {
return None;
}
if let Some(&second) = cases.get(1) {
if let Some((a, b)) = mode_branch(&lines, second, mode) {
for l in &lines[a..b] {
let t = l.trim();
if let Some(body) = t.strip_prefix("icons[") {
if let Some((key, val)) = body.split_once("]=") {
if let Some(v) = parse_icon_value(val, s, q) {
map.insert(key.to_string(), v);
}
}
}
}
}
}
if padding_none && mode != "ascii" {
for v in map.values_mut() {
*v = v.trim_end_matches(' ').to_string();
}
for key in PADDED_KEYS {
map.entry(key.to_string()).or_default().push(' ');
}
}
Some(map)
}
fn theme_icons() -> Option<&'static std::collections::HashMap<String, String>> {
use std::sync::{Mutex, OnceLock};
type Cache = Mutex<std::collections::HashMap<String, Option<&'static std::collections::HashMap<String, String>>>>;
static CACHE: OnceLock<Cache> = OnceLock::new();
let mode = icon_mode();
let legacy = getsparam("POWERLEVEL9K_LEGACY_ICON_SPACING").as_deref() == Some("true");
let padding_none = icon_padding_none();
let sig = format!("{mode}/{legacy}/{padding_none}");
let cache = CACHE.get_or_init(Default::default);
if let Some(hit) = cache.lock().ok().and_then(|c| c.get(&sig).copied()) {
return hit;
}
let table = load_theme_icons(&mode, legacy, padding_none).map(|m| &*Box::leak(Box::new(m)));
if table.is_none() {
tracing::debug!("p10k icons: cannot read internal/icons.zsh for mode {mode:?}");
}
if let Ok(mut c) = cache.lock() {
c.insert(sig, table);
}
table
}
pub fn icon(name: &str) -> &'static str {
let mode = icon_mode();
let legacy = getsparam("POWERLEVEL9K_LEGACY_ICON_SPACING").as_deref() == Some("true");
let baked = matches!(mode.as_str(), "nerdfont-complete" | "nerdfont-fontconfig")
&& (!legacy || icon_padding_none());
if !baked {
if let Some(table) = theme_icons() {
return match table.get(name) {
Some(v) => v.as_str(),
None => {
tracing::debug!("p10k icons: unknown icon key {name}");
""
}
};
}
}
baked_icon(name)
}
fn baked_icon(name: &str) -> &'static str {
let raw = match icon_raw(name) {
Some(r) => r,
None => {
tracing::debug!("p10k icons: unknown icon key {name}");
return "";
}
};
if !icon_padding_none() {
return raw; }
if PADDED_KEYS.contains(&name) {
raw
} else {
raw.trim_end_matches(' ')
}
}
pub fn icon_resolved(name: &str) -> String {
if let Some(user) = getsparam(&format!("POWERLEVEL9K_{name}")) {
let decoded = unescape_g(&user);
let mut chars = decoded.chars();
if chars.next() == Some('\u{8}') && chars.next().is_some() && chars.next().is_none() {
return format!("%{{{decoded}%}}");
}
return decoded;
}
icon(name).to_string()
}
fn unescape_g(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut it = s.chars().peekable();
while let Some(c) = it.next() {
if c != '\\' {
out.push(c);
continue;
}
match it.next() {
Some('a') => out.push('\u{7}'),
Some('b') => out.push('\u{8}'),
Some('e') | Some('E') => out.push('\u{1B}'),
Some('f') => out.push('\u{C}'),
Some('n') => out.push('\n'),
Some('r') => out.push('\r'),
Some('t') => out.push('\t'),
Some('v') => out.push('\u{B}'),
Some('\\') => out.push('\\'),
Some('x') => push_hex(&mut out, &mut it, 2, "\\x"),
Some('u') => push_hex(&mut out, &mut it, 4, "\\u"),
Some('U') => push_hex(&mut out, &mut it, 8, "\\U"),
Some('0') => {
let mut val: u32 = 0;
let mut n = 0;
while n < 3 {
match it.peek() {
Some(d @ '0'..='7') => {
val = val * 8 + (*d as u32 - '0' as u32);
it.next();
n += 1;
}
_ => break,
}
}
out.push(char::from_u32(val).unwrap_or('\u{0}'));
}
Some(other) => {
out.push('\\');
out.push(other);
}
None => out.push('\\'),
}
}
out
}
fn push_hex(
out: &mut String,
it: &mut std::iter::Peekable<std::str::Chars<'_>>,
max: usize,
intro: &str,
) {
let mut val: u32 = 0;
let mut n = 0;
while n < max {
match it.peek().and_then(|d| d.to_digit(16)) {
Some(d) => {
val = val * 16 + d;
it.next();
n += 1;
}
None => break,
}
}
if n == 0 {
out.push_str(intro);
} else {
match char::from_u32(val) {
Some(ch) => out.push(ch),
None => out.push_str(intro),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn branch_icon_raw_has_single_trailing_space() {
assert_eq!(icon_raw("VCS_BRANCH_ICON"), Some("\u{F126} "));
}
#[test]
fn unknown_key_is_empty() {
assert_eq!(icon("NO_SUCH_ICON_KEY"), "");
}
#[test]
fn unescape_g_handles_unicode_and_octal() {
assert_eq!(unescape_g("\\uE0B0"), "\u{E0B0}");
assert_eq!(unescape_g("\\U0001F331"), "\u{1F331}");
assert_eq!(unescape_g("\\x41"), "A");
assert_eq!(unescape_g("\\0101"), "A");
assert_eq!(unescape_g("plain"), "plain");
assert_eq!(unescape_g("\\q"), "\\q"); }
#[test]
fn padded_keys_end_in_exactly_one_space() {
for key in PADDED_KEYS {
let raw = icon_raw(key).unwrap_or_default();
assert!(raw.ends_with(' '), "{key} missing trailing space");
assert!(!raw.ends_with(" "), "{key} has multiple trailing spaces");
}
}
}