pub mod api;
pub mod config;
pub mod custom_fast;
pub mod expansion;
pub mod git;
pub mod gitconfig;
pub mod icons;
pub mod render;
pub mod segments_core;
pub mod segments_env;
pub mod segments_extra;
pub mod segments_powerline;
pub mod segments_sys;
pub mod segments_zshrs;
pub mod shared;
pub mod transient;
pub mod vcs_backends;
pub mod vcs_hooks;
pub mod vcs_other;
pub mod vcs_quilt;
pub mod wizard;
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering};
static ENGINE_ACTIVE: AtomicBool = AtomicBool::new(false);
pub static P10K_ROOT_DIR: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None);
pub fn p10k_root_dir() -> Option<String> {
P10K_ROOT_DIR.lock().unwrap().clone()
}
pub fn engine_active() -> bool {
ENGINE_ACTIVE.load(Ordering::Relaxed)
}
static LINE_FINISHED: AtomicBool = AtomicBool::new(false);
pub fn line_finished() -> bool {
LINE_FINISHED.load(Ordering::Relaxed)
}
pub(crate) fn mark_line_finished() {
LINE_FINISHED.store(true, Ordering::Relaxed);
}
static TTY_STATE: std::sync::Mutex<(String, bool, bool)> =
std::sync::Mutex::new((String::new(), false, false));
fn update_tty_state() {
let tty = crate::ported::params::getsparam("TTY").unwrap_or_default();
let mut st = TTY_STATE.lock().unwrap();
if st.0.is_empty() || (!st.1 && st.0 != tty) {
let max_age = config::p9k_global("NEW_TTY_MAX_AGE_SECONDS", "5")
.trim()
.parse::<f64>()
.unwrap_or(5.0);
st.1 = if max_age < 0.0 {
true
} else {
use std::os::unix::fs::MetadataExt;
std::fs::metadata(&tty).is_ok_and(|m| {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs_f64())
.unwrap_or(0.0);
now - m.ctime() as f64 - (m.ctime_nsec() as f64 / 1e9) < max_age
})
};
st.0 = tty;
} else if is_clear_or_reset(&PREEXEC_CMD.lock().unwrap()) && last_status() == 0 {
st.1 = true; } else if st.1 && st.2 {
st.1 = false; }
st.2 = true;
}
pub fn tty_is_new() -> bool {
TTY_STATE.lock().unwrap().1
}
pub fn refresh_is_precmd() -> bool {
crate::ported::builtins::sched::zleactive.load(Ordering::Relaxed) == 0
}
static EXEC_START_MS: AtomicU64 = AtomicU64::new(0);
static EXEC_LAST_MS: AtomicU64 = AtomicU64::new(u64::MAX);
fn now_ms() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
static PREEXEC_CMD: std::sync::Mutex<String> = std::sync::Mutex::new(String::new());
pub fn note_exec_start(cmd: Option<&str>) {
if engine_active() {
EXEC_START_MS.store(now_ms(), Ordering::Relaxed);
*PREEXEC_CMD.lock().unwrap() = cmd.unwrap_or("").to_string();
TTY_STATE.lock().unwrap().1 = false;
}
}
fn is_clear_or_reset(cmd: &str) -> bool {
let mut words = cmd.split_whitespace();
match words.next() {
Some("reset") => words.next().is_none(),
Some("clear") => words.all(|w| {
let Some(rest) = w.strip_prefix('-') else {
return false;
};
let rest = rest.strip_prefix('x').unwrap_or(rest);
match rest.strip_prefix('T') {
None => rest.is_empty(),
Some(t) => t
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '\'' | '"')),
}
}),
_ => false,
}
}
pub fn note_command_finished(last_status: i64) {
if !engine_active() {
return;
}
LAST_STATUS.store(last_status, Ordering::Relaxed);
let start = EXEC_START_MS.swap(0, Ordering::Relaxed);
if start != 0 {
EXEC_LAST_MS.store(now_ms().saturating_sub(start), Ordering::Relaxed);
}
}
pub fn last_exec_duration() -> Option<std::time::Duration> {
match EXEC_LAST_MS.load(Ordering::Relaxed) {
u64::MAX => None,
ms => Some(std::time::Duration::from_millis(ms)),
}
}
static LAST_STATUS: AtomicI64 = AtomicI64::new(0);
pub(crate) fn state_file(name: &str) -> std::path::PathBuf {
zshrs_home().join(name)
}
pub(crate) fn zshrs_home() -> std::path::PathBuf {
std::env::var_os("ZSHRS_HOME")
.map(std::path::PathBuf::from)
.or_else(|| dirs::home_dir().map(|h| h.join(".zshrs")))
.unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
}
pub(crate) fn write_state_file(path: &std::path::Path, bytes: &[u8]) {
let tmp = path.with_extension(format!("tmp.{}", std::process::id()));
if std::fs::write(&tmp, bytes).is_ok() {
let _ = std::fs::rename(&tmp, path);
}
}
pub fn last_status() -> i64 {
LAST_STATUS.load(Ordering::Relaxed)
}
pub fn maybe_intercept_theme_source(args: &[String]) -> Option<i32> {
let path = args.first()?;
let base = path.rsplit('/').next().unwrap_or(path);
if !matches!(
base,
"powerlevel10k.zsh-theme" | "powerlevel10k.plugin.zsh" | "prompt_powerlevel10k_setup"
) {
return None;
}
ENGINE_ACTIVE.store(true, Ordering::Relaxed);
#[cfg(feature = "recorder")]
if crate::recorder::is_enabled() {
crate::recorder::emit_source(path, crate::recorder::recorder_ctx_global());
}
if let Some(dir) = path.rsplit_once('/').map(|(d, _)| d.to_string()) {
*P10K_ROOT_DIR.lock().unwrap() = Some(dir);
}
tracing::info!(target: "p10k", %path, "native p10k engine activated (theme source intercepted)");
Some(0)
}
pub fn maybe_intercept_command(name: &str, args: &[String]) -> Option<i32> {
if !engine_active() {
return None;
}
match name {
"zshrs-p10k-api" | "p10k" | "p10k-instant-prompt-finalize" => Some(p10k_api(args)),
_ => None,
}
}
thread_local! {
static USER_SEGMENT_SINK: std::cell::RefCell<Option<Vec<render::Segment>>> =
const { std::cell::RefCell::new(None) };
}
fn p10k_api(args: &[String]) -> i32 {
if args.len() == 1 && args[0] == "finalize" {
return 0; }
let Some(cmd) = args.first().map(String::as_str) else {
api::print_usage(api::USAGE, true);
return 1;
};
match cmd {
"segment" => p10k_segment(&args[1..]),
"display" => p10k_display(&args[1..]),
"configure" => {
if args.len() > 1 {
api::print_usage(api::CONFIGURE_USAGE, true); return 1;
}
wizard::run(false)
}
"reload" => {
if args.len() > 1 {
api::print_usage(api::RELOAD_USAGE, true); return 1;
}
api::FORCE_REINIT.store(true, Ordering::Relaxed); 0
}
"help" => {
let sub = args.get(1).map(String::as_str);
let known = matches!(
sub,
None | Some("segment")
| Some("display")
| Some("configure")
| Some("reload")
| Some("finalize")
| Some("help")
);
if known || args.len() == 1 {
api::print_usage(api::help_usage(sub), false);
0
} else {
api::print_usage(api::USAGE, true);
1
}
}
"finalize" => {
api::print_usage(api::FINALIZE_USAGE, true);
1
}
"clear-instant-prompt" => 0,
_ => {
api::print_usage(api::USAGE, true);
1
}
}
}
fn p10k_segment(rest: &[String]) -> i32 {
let mut state: Option<String> = None;
let mut bg = String::new(); let mut bg_set = false;
let mut fg = String::new();
let mut icon = String::new();
let mut cond = true; let mut text = String::new();
let mut refr = false; let mut i = 0;
let mut positional_seen = false;
let arg_at = |idx: usize| rest.get(idx + 1).cloned().unwrap_or_default();
while i < rest.len() {
match rest[i].as_str() {
"-s" => {
state = Some(arg_at(i));
i += 1;
}
"-b" => {
bg = arg_at(i);
bg_set = true;
i += 1;
}
"-f" => {
fg = arg_at(i);
i += 1;
}
"-i" => {
icon = arg_at(i);
i += 1;
}
"-c" => {
cond = !arg_at(i).is_empty();
i += 1;
}
"-t" => {
text = arg_at(i);
i += 1;
}
"-r" => refr = true,
"+r" => refr = false,
"-e" | "+e" => {}
"-h" => {
api::print_usage(api::SEGMENT_USAGE, false);
return 0;
}
s if s.starts_with('-') || s.starts_with('+') => {
api::print_usage(api::SEGMENT_USAGE, true);
return 1;
}
_ => {
positional_seen = true;
}
}
i += 1;
}
if positional_seen {
api::print_usage(api::SEGMENT_USAGE, true);
return 1;
}
let _ = refr;
let _ = bg_set;
USER_SEGMENT_SINK.with(|s| {
let mut slot = s.borrow_mut();
match slot.as_mut() {
Some(v) => {
if cond {
v.push(render::Segment {
name: String::new(), state,
content: text,
icon: if icon.is_empty() { None } else { Some(icon) },
fg,
bg,
});
}
0
}
None => {
api::print_usage(
"%1F[ERROR]%f %Bp10k segment%b: can be called only during prompt rendering.",
true,
);
1
}
}
})
}
fn p10k_display(rest: &[String]) -> i32 {
if rest.is_empty() {
api::print_usage(api::DISPLAY_USAGE, true);
return 1;
}
let mut dump = false; let mut reset = false; let mut i = 0;
while i < rest.len() {
match rest[i].as_str() {
"-a" => dump = true, "-r" => reset = true, "-h" => {
api::print_usage(api::DISPLAY_USAGE, false);
return 0;
}
s if s.starts_with('-') && s.len() > 1 => {
api::print_usage(api::DISPLAY_USAGE, true); return 1;
}
_ => break, }
i += 1;
}
let operands: Vec<&str> = rest[i..].iter().map(String::as_str).collect();
if dump {
let pats = if operands.is_empty() {
vec!["*"]
} else {
operands
};
let pairs = api::display_dump(&pats);
crate::ported::exec::set_array("reply", pairs);
if reset {
preprompt_render(); zle_refresh_if_editing();
}
return 0;
}
if reset && operands.is_empty() {
api::display_reset();
preprompt_render();
zle_refresh_if_editing();
return 0;
}
let mut changed = false;
for op in operands {
let Some((pat, list)) = op.split_once('=') else {
api::print_usage(api::DISPLAY_USAGE, true);
return 1;
};
let states: Vec<&str> = list.split(',').filter(|s| !s.is_empty()).collect();
if api::display_set(pat, &states) {
changed = true;
}
}
if changed {
preprompt_render();
zle_refresh_if_editing();
}
0
}
fn zle_refresh_if_editing() {
if !refresh_is_precmd() {
crate::ported::zle::zle_main::zle_resetprompt();
}
}
fn run_user_segment_fn(base: &str) -> Option<Vec<render::Segment>> {
let fname = format!("prompt_{base}");
crate::ported::utils::getshfunc(&fname)?;
USER_SEGMENT_SINK.with(|s| *s.borrow_mut() = Some(Vec::new()));
let status =
crate::fusevm_bridge::try_with_executor(|exec| exec.execute_script(&fname).unwrap_or(-1))
.unwrap_or(-1);
let segs = USER_SEGMENT_SINK
.with(|s| s.borrow_mut().take())
.unwrap_or_default();
if status != 0 && segs.is_empty() {
tracing::debug!(target: "p10k", %fname, status, "user segment fn failed");
}
let mut segs = segs;
for s in &mut segs {
s.name = base.to_string();
}
Some(segs)
}
static SHOWN_BY_COMMAND: std::sync::Mutex<std::collections::BTreeSet<String>> =
std::sync::Mutex::new(std::collections::BTreeSet::new());
static ZLE_SIGNATURE: std::sync::Mutex<String> = std::sync::Mutex::new(String::new());
fn show_on_command_param(base: &str) -> String {
format!("{}_SHOW_ON_COMMAND", base.replace('-', "_").to_ascii_uppercase())
}
fn has_show_on_command(base: &str) -> bool {
let name = format!("POWERLEVEL9K_{}", show_on_command_param(base));
crate::ported::params::getsparam(&name).is_some() || crate::ported::params::getaparam(&name).is_some()
}
fn run_user_hook(name: &str) {
if crate::ported::utils::getshfunc(name).is_some() {
let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.execute_script(name));
}
}
pub(crate) fn run_post_prompt_hook() {
run_user_hook("p10k-on-post-prompt");
}
fn parse_commands(buffer: &str) -> Vec<String> {
static LAST: std::sync::Mutex<Option<(String, Vec<String>)>> = std::sync::Mutex::new(None);
if let Some((b, c)) = LAST.lock().unwrap().as_ref() {
if b == buffer {
return c.clone();
}
}
let Some(root) = p10k_root_dir() else {
return Vec::new();
};
let quote = |t: &str| format!("'{}'", t.replace('\'', "'\\''"));
let max = config::p9k_global("COMMANDS_MAX_TOKEN_COUNT", "64");
let script = format!(
"(( $+__p9k_intro )) || typeset -g __p9k_intro='emulate -L zsh -o no_hist_expand -o extended_glob -o no_prompt_bang -o prompt_percent -o no_prompt_subst -o no_aliases -o no_bg_nice -o typeset_silent -o no_rematch_pcre; local -a match mbegin mend reply; local -i MBEGIN MEND OPTIND; local MATCH REPLY OPTARG'\n\
(( $+functions[_p9k_parse_buffer] )) || source {}\n\
_p9k_parse_buffer {} {}",
quote(&format!("{root}/internal/parser.zsh")),
quote(buffer),
max.trim().parse::<u64>().unwrap_or(64)
);
let _ = crate::fusevm_bridge::try_with_executor(|exec| exec.execute_script(&script));
let cmds = crate::ported::params::getaparam("P9K_COMMANDS").unwrap_or_default();
*LAST.lock().unwrap() = Some((buffer.to_string(), cmds.clone()));
cmds
}
fn zle_signature(shown: &std::collections::HashSet<String>) -> String {
use crate::ported::zle::zle_main::{INSMODE, REGION_ACTIVE};
let mut names: Vec<&String> = shown.iter().collect();
names.sort();
format!(
"{}/{}/{}/{}",
crate::ported::zle::zle_keymap::curkeymapname(),
REGION_ACTIVE.load(Ordering::Relaxed) != 0,
INSMODE.load(Ordering::Relaxed),
names.iter().map(|n| n.as_str()).collect::<Vec<_>>().join(",")
)
}
pub fn on_zle_redraw() {
use crate::ported::params::{getsparam, setaparam};
if !engine_active() {
return;
}
let mut elems = config::p9k_global_arr("LEFT_PROMPT_ELEMENTS");
elems.extend(config::p9k_global_arr("RIGHT_PROMPT_ELEMENTS"));
let bases: Vec<String> = elems
.iter()
.map(|e| render::is_joined_name(e).0.to_string())
.filter(|b| has_show_on_command(b))
.collect();
let has_post_widget = crate::ported::utils::getshfunc("p10k-on-post-widget").is_some();
let mut shown = std::collections::HashSet::new();
if !bases.is_empty() || has_post_widget {
let buffer = format!(
"{}{}",
getsparam("PREBUFFER").unwrap_or_default(),
getsparam("BUFFER").unwrap_or_default()
);
let cmds = if buffer.is_empty() { Vec::new() } else { parse_commands(&buffer) };
for base in &bases {
let alt = config::p9k_global_arr(&show_on_command_param(base)).join("|");
let pat = format!("(|*[/])({alt})");
if cmds.iter().any(|c| shared::glob_name_matches(&pat, c)) {
shown.insert(base.clone());
}
}
if has_post_widget {
setaparam("P9K_COMMANDS", cmds);
run_user_hook("p10k-on-post-widget");
}
}
let sig = zle_signature(&shown);
if *ZLE_SIGNATURE.lock().unwrap() == sig {
return;
}
*ZLE_SIGNATURE.lock().unwrap() = sig;
*SHOWN_BY_COMMAND.lock().unwrap() = shown.into_iter().collect();
preprompt_render();
crate::ported::zle::zle_main::zle_resetprompt();
}
type Inflight = std::collections::HashMap<
usize,
crossbeam_channel::Receiver<(Option<Vec<render::Segment>>, u128)>,
>;
fn needs_main_thread(base: &str) -> bool {
base == "dir"
|| base.starts_with("custom_")
|| ["zshrs_", "stryke", "vimlrs_", "elisprs_", "awkrs_"]
.iter()
.any(|p| base.starts_with(p))
}
fn build_builtin(base: &str) -> Option<Vec<render::Segment>> {
segments_core::build_segment(base)
.or_else(|| segments_env::build_segment(base))
.or_else(|| segments_sys::build_segment(base))
.or_else(|| segments_extra::build_segment(base))
.or_else(|| segments_powerline::build_segment(base))
.or_else(|| segments_zshrs::build_segment(base))
}
fn dispatch_segments(pool: Option<&crate::worker::WorkerPool>, elems: &[String]) -> Inflight {
let mut inflight = Inflight::new();
let Some(pool) = pool else {
return inflight;
};
for (idx, name) in elems.iter().enumerate() {
if name == "newline" {
continue;
}
let base = render::is_joined_name(name).0;
if needs_main_thread(base) {
continue;
}
if has_show_on_command(base) && !SHOWN_BY_COMMAND.lock().unwrap().contains(base) {
continue;
}
if !expansion::show_on_upglob(base) {
continue;
}
let base = base.to_string();
inflight.insert(
idx,
pool.submit_with_result(move || {
let t0 = std::time::Instant::now();
let built = build_builtin(&base);
(built, t0.elapsed().as_millis())
}),
);
}
inflight
}
pub fn preprompt_render() {
if !engine_active() {
return;
}
let render_t0 = std::time::Instant::now();
thread_local! {
static SEG_MS: std::cell::RefCell<Vec<(String, u128)>> =
const { std::cell::RefCell::new(Vec::new()) };
}
SEG_MS.with(|v| v.borrow_mut().clear());
struct RenderTimer(std::time::Instant);
impl Drop for RenderTimer {
fn drop(&mut self) {
let ms = self.0.elapsed().as_millis();
let mut segs = SEG_MS.with(|v| v.borrow().clone());
segs.sort_by(|a, b| b.1.cmp(&a.1));
let brk = segs
.iter()
.filter(|(_, m)| *m > 0)
.map(|(n, m)| format!("{n}={m}"))
.collect::<Vec<_>>()
.join(" ");
if ms > 100 {
tracing::info!(target: "p10k", ms, segments = %brk, "slow preprompt_render");
} else {
tracing::debug!(target: "p10k", ms, segments = %brk, "preprompt_render");
}
}
}
let _t = RenderTimer(render_t0);
if refresh_is_precmd() {
LINE_FINISHED.store(false, Ordering::Relaxed); update_tty_state();
static INIT_DONE: AtomicBool = AtomicBool::new(false);
if !INIT_DONE.swap(true, Ordering::Relaxed) {
run_user_hook("p10k-on-init");
}
SHOWN_BY_COMMAND.lock().unwrap().clear();
run_user_hook("p10k-on-pre-prompt");
*ZLE_SIGNATURE.lock().unwrap() = zle_signature(&Default::default());
}
let left_elems = config::p9k_global_arr("LEFT_PROMPT_ELEMENTS");
let right_elems = config::p9k_global_arr("RIGHT_PROMPT_ELEMENTS");
let pool = crate::async_precmd::session_pool().filter(|p| p.queue_depth() == 0);
let mut left_inflight = dispatch_segments(pool.as_deref(), &left_elems);
let mut right_inflight = dispatch_segments(pool.as_deref(), &right_elems);
let split_lines = |elems: &[String], inflight: &mut Inflight| -> Vec<Vec<render::Segment>> {
let mut lines: Vec<Vec<render::Segment>> = vec![Vec::new()];
for (idx, name) in elems.iter().enumerate() {
if name == "newline" {
lines.push(Vec::new());
continue;
}
let (base, joined) = render::is_joined_name(name);
if has_show_on_command(base) && !SHOWN_BY_COMMAND.lock().unwrap().contains(base) {
tracing::debug!(target: "p10k", %name, "SHOW_ON_COMMAND: no matching command — hidden");
continue;
}
if !expansion::show_on_upglob(base) {
tracing::debug!(target: "p10k", %name, "SHOW_ON_UPGLOB: no parent match — hidden");
continue;
}
let seg_t0 = std::time::Instant::now();
let (built, ms) = match inflight.remove(&idx).map(|rx| rx.recv()) {
Some(Ok((built, ms))) => (built.or_else(|| run_user_segment_fn(base)), ms),
_ => {
let built = build_builtin(base).or_else(|| run_user_segment_fn(base));
(built, seg_t0.elapsed().as_millis())
}
};
SEG_MS.with(|v| v.borrow_mut().push((base.to_string(), ms)));
match built {
Some(mut segs) => {
if joined {
for s in &mut segs {
s.name = name.clone();
}
}
lines.last_mut().expect("lines never empty").extend(segs);
}
None => {
tracing::debug!(target: "p10k", %name, "segment not implemented — skipped")
}
}
}
lines
};
let left_lines = split_lines(&left_elems, &mut left_inflight);
let right_lines = split_lines(&right_elems, &mut right_inflight);
let element_names = |elems: &[String]| -> Vec<Vec<String>> {
let mut lines: Vec<Vec<String>> = vec![Vec::new()];
for name in elems {
if name == "newline" {
lines.push(Vec::new());
} else {
lines
.last_mut()
.expect("lines never empty")
.push(render::is_joined_name(name).0.to_string());
}
}
lines
};
let (left_names, right_names) = render::align_lines(element_names(&left_elems), element_names(&right_elems));
api::set_layout(left_names, right_names);
let (prompt, rprompt) = render::render_prompt(&left_lines, &right_lines);
crate::ported::params::setsparam("PROMPT", &prompt);
crate::ported::params::setsparam("RPROMPT", &rprompt);
}
#[cfg(test)]
mod tty_tests {
use super::is_clear_or_reset;
#[test]
fn clear_and_reset_forms_match() {
for cmd in ["clear", " clear ", "reset", "clear -x", "clear -T xterm", "clear -Txterm-256color", "clear -xTvt100 -x"] {
let expect = cmd != "clear -T xterm";
assert_eq!(is_clear_or_reset(cmd), expect, "{cmd:?}");
}
}
#[test]
fn other_commands_do_not_match() {
for cmd in ["", "clearx", "clear foo", "reset now", "clear -y", "echo clear", "clear;ls", "clear -T'a b'"] {
assert!(!is_clear_or_reset(cmd), "{cmd:?}");
}
}
}