use crate::ported::lib::dict::mergeargs;
use crate::ported::lib::overrides::{parse_override_var, parsedotval_tuple};
use serde_json::{Map, Value};
use std::collections::HashSet;
use std::sync::Mutex;
use std::sync::OnceLock;
pub fn used_powerlines() -> &'static Mutex<HashSet<u64>> {
static M: OnceLock<Mutex<HashSet<u64>>> = OnceLock::new();
M.get_or_init(|| Mutex::new(HashSet::new()))
}
pub fn shutdown<F>(mut shutdown_each: F)
where
F: FnMut(u64),
{
let ids: Vec<u64> = used_powerlines()
.lock()
.unwrap_or_else(|e| e.into_inner())
.iter()
.copied()
.collect();
for id in ids {
shutdown_each(id);
}
}
pub fn get_var_config(value: Option<Value>) -> Option<Map<String, Value>> {
let v = value?;
if let Some(obj) = v.as_object() {
let pairs: Vec<(String, Value)> = obj
.iter()
.map(|(k, v)| {
let v_str = v.as_str().unwrap_or("").to_string();
parsedotval_tuple(k, &v_str)
})
.collect();
return mergeargs(pairs, false);
}
if let Some(s) = v.as_str() {
return mergeargs(parse_override_var(s), false);
}
None
}
#[derive(Debug, Clone, Default)]
pub struct Args {
pub last_pipe_status: Vec<i32>,
pub last_exit_code: i32,
}
impl Args {
pub fn ext() -> Vec<&'static str> {
vec!["shell"]
}
pub const RENDERER_MODULE: &'static str = ".zsh";
pub fn new() -> Self {
Self::default()
}
}
pub fn config_path_from_var(value: Option<Value>) -> Option<Vec<String>> {
let v = value?;
if let Some(s) = v.as_str() {
return Some(
s.split(':')
.filter(|p| !p.is_empty())
.map(|p| p.to_string())
.collect(),
);
}
if let Some(arr) = v.as_array() {
return Some(
arr.iter()
.filter_map(|x| x.as_str().map(String::from))
.collect(),
);
}
None
}
pub fn string(bytes: &[u8]) -> String {
String::from_utf8_lossy(bytes).to_string()
}
pub fn args_config_override(value: Option<Value>) -> Option<Map<String, Value>> {
get_var_config(value)
}
pub fn args_theme_override(value: Option<Value>) -> Option<Map<String, Value>> {
get_var_config(value)
}
pub fn args_config_path(value: Option<Value>) -> Option<Vec<String>> {
let v = value?;
match v {
Value::String(s) => Some(
s.split(':')
.filter(|p| !p.is_empty())
.map(String::from)
.collect(),
),
Value::Array(arr) => Some(
arr.into_iter()
.filter_map(|v| v.as_str().map(String::from))
.collect(),
),
_ => None,
}
}
pub fn args_jobnum(value: Option<i32>) -> Option<i32> {
value
}
pub struct Environment;
impl Environment {
pub fn get<F>(get_value: F, default: Option<String>) -> Option<String>
where
F: FnOnce() -> Option<String>,
{
get_value().or(default)
}
pub fn contains<F>(get_value: F) -> bool
where
F: FnOnce() -> Option<String>,
{
get_value().is_some()
}
}
pub fn zsh_expand_fallback_steps(s: &str) -> (String, &'static str, &'static str, Option<String>) {
let eval_cmd = format!("local _POWERLINE_REPLY=\"{}\"", s);
(eval_cmd, "_POWERLINE_REPLY", "_POWERLINE_REPLY", None)
}
pub fn set_prompt_zpyvar_name(psvar: &str) -> String {
format!("ZPYTHON_POWERLINE_{}", psvar)
}
pub fn build_segment_info(
parser_state: &str,
shortened_path: &str,
mode: Option<&str>,
default_mode: Option<&str>,
client_id: u64,
local_theme: Option<&str>,
) -> Map<String, Value> {
let mut info = Map::new();
info.insert("client_id".to_string(), Value::from(client_id));
info.insert(
"parser_state".to_string(),
Value::String(parser_state.into()),
);
info.insert(
"shortened_path".to_string(),
Value::String(shortened_path.into()),
);
info.insert(
"mode".to_string(),
mode.map(|s| Value::String(s.into())).unwrap_or(Value::Null),
);
info.insert(
"default_mode".to_string(),
default_mode
.map(|s| Value::String(s.into()))
.unwrap_or(Value::Null),
);
info.insert(
"local_theme".to_string(),
local_theme
.map(|s| Value::String(s.into()))
.unwrap_or(Value::Null),
);
info
}
pub struct Prompt {
pub side: String,
pub above: bool,
pub savedpsvar: Option<String>,
pub savedps: Option<String>,
pub theme: Option<String>,
}
impl Prompt {
pub fn del_restore(&self) -> Option<(String, String)> {
match (&self.savedpsvar, &self.savedps) {
(Some(var), Some(ps)) if !ps.is_empty() => Some((var.clone(), ps.clone())),
_ => None,
}
}
pub fn new(
side: impl Into<String>,
theme: Option<String>,
savedpsvar: Option<String>,
savedps: Option<String>,
above: bool,
) -> Self {
Self {
side: side.into(),
above,
savedpsvar,
savedps,
theme,
}
}
}
pub struct ZshPowerline {
pub args: Args,
pub id: u64,
}
impl Default for ZshPowerline {
fn default() -> Self {
Self::new()
}
}
impl ZshPowerline {
fn next_id() -> u64 {
static C: OnceLock<Mutex<u64>> = OnceLock::new();
let mut slot = C
.get_or_init(|| Mutex::new(0))
.lock()
.unwrap_or_else(|e| e.into_inner());
*slot += 1;
*slot
}
pub fn new() -> Self {
Self {
args: Args::new(),
id: Self::next_id(),
}
}
pub fn precmd(&mut self, pipe_status: Vec<i32>, last_exit_code: i32) {
self.args.last_pipe_status = pipe_status;
self.args.last_exit_code = last_exit_code;
}
pub fn do_setup(&self) -> Vec<(&'static str, &'static str, Option<&'static str>, bool)> {
let prompts = vec![
("PS1", "left", None, true),
("RPS1", "right", None, false),
("PS2", "left", Some("continuation"), false),
("RPS2", "right", Some("continuation"), false),
("PS3", "left", Some("select"), false),
];
used_powerlines()
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(self.id);
prompts
}
}
pub fn setup_entry() -> ZshPowerline {
ZshPowerline::new()
}
pub fn setup(_zsh_globals: Option<&Map<String, Value>>) -> ZshPowerline {
setup_entry()
}
pub fn config_override(value: Option<Value>) -> Option<Map<String, Value>> {
args_config_override(value)
}
pub fn theme_override(value: Option<Value>) -> Option<Map<String, Value>> {
args_theme_override(value)
}
pub fn config_path(value: Option<Value>) -> Option<Vec<String>> {
args_config_path(value)
}
pub fn jobnum(value: Option<i32>) -> Option<i32> {
args_jobnum(value)
}
pub fn zsh_expand(s: &str) -> (String, &'static str, &'static str, Option<String>) {
zsh_expand_fallback_steps(s)
}
pub fn init() -> ZshPowerline {
ZshPowerline::new()
}
pub fn set_prompt(psvar: &str) -> (String, String) {
let zpyvar = set_prompt_zpyvar_name(psvar);
let new_psvar = format!("${{{}}}", zpyvar);
(zpyvar, new_psvar)
}
pub fn reload<F>(mut reload_each: F)
where
F: FnMut(u64),
{
let ids: Vec<u64> = used_powerlines()
.lock()
.unwrap_or_else(|e| e.into_inner())
.iter()
.copied()
.collect();
for id in ids {
reload_each(id);
}
}
pub fn reload_config<F>(mut reload_each: F)
where
F: FnMut(u64),
{
let ids: Vec<u64> = used_powerlines()
.lock()
.unwrap_or_else(|e| e.into_inner())
.iter()
.copied()
.collect();
for id in ids {
reload_each(id);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
macro_rules! lock_globals {
() => {{
TEST_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|e| e.into_inner())
}};
}
fn reset_used() {
used_powerlines()
.lock()
.unwrap_or_else(|e| e.into_inner())
.clear();
}
#[test]
fn args_ext_matches_upstream() {
assert_eq!(Args::ext(), vec!["shell"]);
}
#[test]
fn args_renderer_module_matches_upstream() {
assert_eq!(Args::RENDERER_MODULE, ".zsh");
}
#[test]
fn args_new_defaults_to_empty_pipe_status_and_zero_exit_code() {
let a = Args::new();
assert!(a.last_pipe_status.is_empty());
assert_eq!(a.last_exit_code, 0);
}
#[test]
fn get_var_config_none_returns_none() {
assert!(get_var_config(None).is_none());
}
#[test]
fn get_var_config_dict_parses_via_parsedotval() {
let mut inner = Map::new();
inner.insert("foo".to_string(), Value::String("bar".into()));
let r = get_var_config(Some(Value::Object(inner)));
assert!(r.is_some());
let m = r.unwrap();
assert_eq!(m.get("foo"), Some(&Value::String("bar".into())));
}
#[test]
fn get_var_config_string_parses_via_parse_override_var() {
let r = get_var_config(Some(Value::String("common.term_truecolor=true".into())));
assert!(r.is_some());
let m = r.unwrap();
let common = m.get("common").and_then(|v| v.as_object()).unwrap();
assert_eq!(common.get("term_truecolor"), Some(&Value::Bool(true)));
}
#[test]
fn get_var_config_unknown_type_returns_none() {
assert!(get_var_config(Some(Value::from(42))).is_none());
assert!(get_var_config(Some(Value::Null)).is_none());
}
#[test]
fn config_path_from_var_none_returns_none() {
assert!(config_path_from_var(None).is_none());
}
#[test]
fn config_path_from_var_string_splits_on_colon() {
let r = config_path_from_var(Some(Value::String("/etc/p:/usr/share".into())));
assert_eq!(
r,
Some(vec!["/etc/p".to_string(), "/usr/share".to_string()])
);
}
#[test]
fn config_path_from_var_string_filters_empty_entries() {
let r = config_path_from_var(Some(Value::String("::/a::/b:".into())));
assert_eq!(r, Some(vec!["/a".to_string(), "/b".to_string()]));
}
#[test]
fn config_path_from_var_list_preserves_entries() {
let arr = Value::Array(vec![
Value::String("/p1".into()),
Value::String("/p2".into()),
]);
let r = config_path_from_var(Some(arr));
assert_eq!(r, Some(vec!["/p1".to_string(), "/p2".to_string()]));
}
#[test]
fn string_decodes_bytes_to_utf8() {
assert_eq!(string(b"hello"), "hello");
let r = string(&[b'a', 0xff, b'b']);
assert!(r.contains('a'));
assert!(r.contains('b'));
}
#[test]
fn environment_get_returns_value_when_present() {
let r = Environment::get(|| Some("alice".to_string()), None);
assert_eq!(r, Some("alice".to_string()));
}
#[test]
fn environment_get_returns_default_when_absent() {
let r = Environment::get(|| None, Some("default".to_string()));
assert_eq!(r, Some("default".to_string()));
}
#[test]
fn environment_contains_true_when_present() {
assert!(Environment::contains(|| Some("x".to_string())));
}
#[test]
fn environment_contains_false_when_absent() {
assert!(!Environment::contains(|| None));
}
#[test]
fn set_prompt_zpyvar_name_prepends_zpython_powerline() {
assert_eq!(set_prompt_zpyvar_name("PS1"), "ZPYTHON_POWERLINE_PS1");
assert_eq!(set_prompt_zpyvar_name("RPS2"), "ZPYTHON_POWERLINE_RPS2");
}
#[test]
fn build_segment_info_contains_all_expected_keys() {
let info = build_segment_info(
"%_state",
"~/p",
Some("normal"),
Some("normal"),
1,
Some("continuation"),
);
assert!(info.contains_key("parser_state"));
assert!(info.contains_key("shortened_path"));
assert!(info.contains_key("mode"));
assert!(info.contains_key("default_mode"));
assert!(info.contains_key("client_id"));
assert!(info.contains_key("local_theme"));
assert_eq!(info["parser_state"], "%_state");
assert_eq!(info["client_id"], 1);
}
#[test]
fn build_segment_info_none_mode_becomes_null() {
let info = build_segment_info("", "", None, None, 1, None);
assert_eq!(info["mode"], Value::Null);
assert_eq!(info["default_mode"], Value::Null);
assert_eq!(info["local_theme"], Value::Null);
}
#[test]
fn prompt_new_stores_state() {
let p = Prompt::new(
"left",
Some("theme1".to_string()),
Some("PS1".to_string()),
None,
true,
);
assert_eq!(p.side, "left");
assert!(p.above);
assert_eq!(p.savedpsvar, Some("PS1".to_string()));
assert!(p.savedps.is_none());
assert_eq!(p.theme, Some("theme1".to_string()));
}
#[test]
fn zsh_powerline_new_assigns_unique_id() {
let _g = lock_globals!();
let a = ZshPowerline::new();
let b = ZshPowerline::new();
assert_ne!(a.id, b.id);
}
#[test]
fn zsh_powerline_precmd_sets_args() {
let mut p = ZshPowerline::new();
p.precmd(vec![0, 1, 2], 42);
assert_eq!(p.args.last_pipe_status, vec![0, 1, 2]);
assert_eq!(p.args.last_exit_code, 42);
}
#[test]
fn zsh_powerline_do_setup_emits_five_prompt_pairs() {
let p = ZshPowerline::new();
let prompts = p.do_setup();
assert_eq!(prompts.len(), 5);
let psvars: Vec<&str> = prompts.iter().map(|t| t.0).collect();
assert_eq!(psvars, vec!["PS1", "RPS1", "PS2", "RPS2", "PS3"]);
}
#[test]
fn zsh_powerline_do_setup_marks_above_only_for_ps1() {
let p = ZshPowerline::new();
let prompts = p.do_setup();
assert!(prompts[0].3); for t in &prompts[1..] {
assert!(!t.3);
}
}
#[test]
fn zsh_powerline_do_setup_registers_in_used_powerlines() {
let _g = lock_globals!();
reset_used();
let p = ZshPowerline::new();
let _ = p.do_setup();
let set = used_powerlines().lock().unwrap_or_else(|e| e.into_inner());
assert!(set.contains(&p.id));
}
#[test]
fn shutdown_iterates_registered_powerlines() {
let _g = lock_globals!();
reset_used();
let p = ZshPowerline::new();
let id = p.id;
let _ = p.do_setup();
let mut shutdown_calls: Vec<u64> = Vec::new();
shutdown(|ident| shutdown_calls.push(ident));
assert!(shutdown_calls.contains(&id));
}
#[test]
fn reload_iterates_registered_powerlines() {
let _g = lock_globals!();
reset_used();
let p = ZshPowerline::new();
let id = p.id;
let _ = p.do_setup();
let mut reload_calls: Vec<u64> = Vec::new();
reload(|ident| reload_calls.push(ident));
assert!(reload_calls.contains(&id));
}
#[test]
fn reload_config_iterates_registered_powerlines() {
let _g = lock_globals!();
reset_used();
let p = ZshPowerline::new();
let id = p.id;
let _ = p.do_setup();
let mut reload_calls: Vec<u64> = Vec::new();
reload_config(|ident| reload_calls.push(ident));
assert!(reload_calls.contains(&id));
}
#[test]
fn setup_entry_returns_fresh_powerline() {
let p = setup_entry();
assert!(p.id > 0);
}
#[test]
fn args_config_override_delegates_to_get_var_config() {
let cfg = serde_json::json!({"foo": "bar"});
let r = args_config_override(Some(cfg)).unwrap();
assert_eq!(r.get("foo"), Some(&Value::String("bar".into())));
}
#[test]
fn args_config_override_none_input_returns_none() {
assert!(args_config_override(None).is_none());
}
#[test]
fn args_theme_override_delegates_to_get_var_config() {
let cfg = serde_json::json!({"key": "value"});
let r = args_theme_override(Some(cfg)).unwrap();
assert_eq!(r.get("key"), Some(&Value::String("value".into())));
}
#[test]
fn args_config_path_splits_string_on_colon() {
let v = Value::String("/etc/powerline:/home/user/.config/powerline".into());
let r = args_config_path(Some(v)).unwrap();
assert_eq!(
r,
vec![
"/etc/powerline".to_string(),
"/home/user/.config/powerline".to_string()
]
);
}
#[test]
fn args_config_path_filters_empty_segments() {
let v = Value::String(":/etc/powerline::/home/user:".into());
let r = args_config_path(Some(v)).unwrap();
assert_eq!(
r,
vec!["/etc/powerline".to_string(), "/home/user".to_string()]
);
}
#[test]
fn args_config_path_passes_array_through() {
let v = Value::Array(vec![Value::String("/a".into()), Value::String("/b".into())]);
let r = args_config_path(Some(v)).unwrap();
assert_eq!(r, vec!["/a".to_string(), "/b".to_string()]);
}
#[test]
fn args_config_path_none_input_returns_none() {
assert!(args_config_path(None).is_none());
}
#[test]
fn args_jobnum_passes_through() {
assert_eq!(args_jobnum(Some(3)), Some(3));
assert!(args_jobnum(None).is_none());
}
#[test]
fn zsh_expand_fallback_builds_local_var_command() {
let (eval_cmd, get_key, set_key, set_val) = zsh_expand_fallback_steps("${(%):-%~}");
assert_eq!(eval_cmd, "local _POWERLINE_REPLY=\"${(%):-%~}\"");
assert_eq!(get_key, "_POWERLINE_REPLY");
assert_eq!(set_key, "_POWERLINE_REPLY");
assert!(set_val.is_none());
}
#[test]
fn prompt_del_restore_returns_var_value_pair() {
let p = Prompt::new(
"left",
None,
Some("PS1".to_string()),
Some("$ ".to_string()),
false,
);
let r = p.del_restore().unwrap();
assert_eq!(r.0, "PS1");
assert_eq!(r.1, "$ ");
}
#[test]
fn prompt_del_restore_returns_none_when_no_savedps() {
let p = Prompt::new("left", None, Some("PS1".to_string()), None, false);
assert!(p.del_restore().is_none());
}
#[test]
fn prompt_del_restore_returns_none_when_savedps_empty() {
let p = Prompt::new(
"left",
None,
Some("PS1".to_string()),
Some("".to_string()),
false,
);
assert!(p.del_restore().is_none());
}
}