use crate::ported::bindings::tmux::TmuxVersionInfo;
use crate::ported::config::{POWERLINE_ROOT, TMUX_CONFIG_DIRECTORY};
use crate::ported::lib::shell::which;
use regex::Regex;
use std::path::PathBuf;
use std::sync::OnceLock;
#[allow(non_snake_case)]
pub fn CONFIG_FILE_NAME() -> &'static Regex {
static R: OnceLock<Regex> = OnceLock::new();
R.get_or_init(|| {
Regex::new(
r"^powerline_tmux_(?P<major>\d+)\.(?P<minor>\d+)(?P<suffix>[a-z]+)?(?:_(?P<mod>plus|minus))?\.conf$",
)
.unwrap()
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ConfigMatcher {
Exact,
Plus,
Minus,
}
impl ConfigMatcher {
pub fn applies(self, file_version: &TmuxVersionInfo, tmux_version: &TmuxVersionInfo) -> bool {
match self {
ConfigMatcher::Exact => {
file_version.major == tmux_version.major && file_version.minor == tmux_version.minor
}
ConfigMatcher::Plus => {
(file_version.major, file_version.minor) <= (tmux_version.major, tmux_version.minor)
}
ConfigMatcher::Minus => {
(file_version.major, file_version.minor) >= (tmux_version.major, tmux_version.minor)
}
}
}
pub fn priority(self) -> i32 {
match self {
ConfigMatcher::Exact => 3,
ConfigMatcher::Plus => 2,
ConfigMatcher::Minus => 1,
}
}
}
#[derive(Debug, Clone)]
pub struct TmuxConfigFile {
pub path: PathBuf,
pub matcher: ConfigMatcher,
pub priority: i32,
pub file_version: TmuxVersionInfo,
}
pub fn list_all_tmux_configs() -> Vec<TmuxConfigFile> {
let dir = TMUX_CONFIG_DIRECTORY();
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return Vec::new(),
};
let mut out: Vec<TmuxConfigFile> = Vec::new();
for entry in entries.flatten() {
let fname = entry.file_name();
let fname_str = match fname.to_str() {
Some(s) => s,
None => continue,
};
let captures = match CONFIG_FILE_NAME().captures(fname_str) {
Some(c) => c,
None => continue,
};
if captures.name("suffix").is_some() {
continue;
}
let major: f64 = match captures.name("major").and_then(|m| m.as_str().parse().ok()) {
Some(n) => n,
None => continue,
};
let minor: i32 = match captures.name("minor").and_then(|m| m.as_str().parse().ok()) {
Some(n) => n,
None => continue,
};
let mod_str = captures.name("mod").map(|m| m.as_str());
let matcher = match mod_str {
None => ConfigMatcher::Exact,
Some("plus") => ConfigMatcher::Plus,
Some("minus") => ConfigMatcher::Minus,
Some(_) => continue,
};
out.push(TmuxConfigFile {
path: entry.path(),
matcher,
priority: matcher.priority(),
file_version: TmuxVersionInfo {
major,
minor,
suffix: None,
},
});
}
out
}
pub fn get_tmux_configs(version: &TmuxVersionInfo) -> Vec<(PathBuf, i64)> {
let mut out = Vec::new();
for cfg in list_all_tmux_configs() {
if cfg.matcher.applies(&cfg.file_version, version) {
let sort_key = (cfg.priority as i64)
+ (cfg.file_version.minor as i64) * 10
+ (cfg.file_version.major as i64) * 10_000;
out.push((cfg.path, sort_key));
}
}
out
}
#[derive(Debug, Clone)]
pub struct EmptyArgs {
pub ext: Vec<String>,
pub side: String,
pub config_path: Option<String>,
}
impl EmptyArgs {
pub fn new(ext: &str, _config_path: Option<&str>) -> Self {
EmptyArgs {
ext: vec![ext.to_string()],
side: "left".to_string(),
config_path: None,
}
}
}
#[allow(non_snake_case)]
pub fn TMUX_VAR_RE() -> &'static Regex {
static R: OnceLock<Regex> = OnceLock::new();
R.get_or_init(|| Regex::new(r"\$(_POWERLINE_\w+)").unwrap())
}
pub fn check_command(cmd: &str) -> Option<String> {
if which(cmd).is_some() {
Some(cmd.to_string())
} else {
None
}
}
pub fn deduce_command() -> Option<String> {
if let Some(c) = check_command("powerline") {
return Some(c);
}
let p = POWERLINE_ROOT().join("scripts").join("powerline");
if let Some(c) = check_command(&p.to_string_lossy()) {
return Some(c);
}
if which("sh").is_some() && which("sed").is_some() && which("socat").is_some() {
let p = POWERLINE_ROOT().join("client").join("powerline.sh");
if let Some(c) = check_command(&p.to_string_lossy()) {
return Some(c);
}
}
let p = POWERLINE_ROOT().join("client").join("powerline.py");
if let Some(c) = check_command(&p.to_string_lossy()) {
return Some(c);
}
if let Some(c) = check_command("powerline-render") {
return Some(c);
}
let p = POWERLINE_ROOT().join("scripts").join("powerline-render");
check_command(&p.to_string_lossy())
}
pub fn set_tmux_environment_nosource(
tmux_environ: &mut std::collections::HashMap<String, String>,
varname: &str,
value: &str,
_remove: bool,
) {
tmux_environ.insert(varname.to_string(), value.to_string());
}
pub fn replace_cb(
tmux_environ: &std::collections::HashMap<String, String>,
capture: &str,
) -> Option<String> {
tmux_environ.get(capture).cloned()
}
pub fn replace_env(s: &str, tmux_environ: &std::collections::HashMap<String, String>) -> String {
TMUX_VAR_RE()
.replace_all(s, |caps: ®ex::Captures| {
let varname = caps.get(1).map(|m| m.as_str()).unwrap_or("");
tmux_environ
.get(varname)
.cloned()
.unwrap_or_else(|| caps.get(0).unwrap().as_str().to_string())
})
.into_owned()
}
pub fn source_tmux_files(
tmux_version: &crate::ported::bindings::config::TmuxVersionInfo,
) -> Vec<std::path::PathBuf> {
let mut files =
vec![crate::ported::config::TMUX_CONFIG_DIRECTORY().join("powerline-base.conf")];
for (fname, _priority) in sorted_tmux_configs(tmux_version) {
files.push(fname);
}
files
}
pub fn init_tmux_environment(
colorscheme: &crate::ported::colorscheme::Colorscheme,
theme: &crate::ported::theme::Theme,
renderer: &crate::ported::renderers::tmux::TmuxRenderer,
term_truecolor: bool,
) -> Vec<(String, String)> {
use crate::ported::renderers::tmux::{attrs_to_tmux_attrs, ColorSpec};
let mut env: Vec<(String, String)> = Vec::new();
let get_highlighting = |group: &str| -> Option<serde_json::Map<String, serde_json::Value>> {
colorscheme
.get_highlighting(&[group.to_string()], None, None)
.ok()
};
let to_spec = |v: Option<&serde_json::Value>| -> Option<ColorSpec> {
let v = v?;
if v.as_bool() == Some(false) {
return None;
}
let arr = v.as_array()?;
let cterm = arr.first().and_then(|c| c.as_u64()).unwrap_or(0) as u16;
let truecolor = arr.get(1).and_then(|c| c.as_u64()).map(|n| n as u32);
Some(ColorSpec { cterm, truecolor })
};
const COLOR_GROUPS: &[(&str, &str)] = &[
("_POWERLINE_BACKGROUND_COLOR", "background"),
("_POWERLINE_ACTIVE_WINDOW_STATUS_COLOR", "active_window_status"),
("_POWERLINE_WINDOW_STATUS_COLOR", "window_status"),
("_POWERLINE_ACTIVITY_STATUS_COLOR", "activity_status"),
("_POWERLINE_BELL_STATUS_COLOR", "bell_status"),
("_POWERLINE_WINDOW_COLOR", "window"),
("_POWERLINE_WINDOW_DIVIDER_COLOR", "window:divider"),
("_POWERLINE_WINDOW_CURRENT_COLOR", "window:current"),
("_POWERLINE_WINDOW_NAME_COLOR", "window_name"),
("_POWERLINE_SESSION_COLOR", "session"),
];
for (varname, hl_group) in COLOR_GROUPS {
let hl = match get_highlighting(hl_group) {
Some(h) => h,
None => continue,
};
let fg = to_spec(hl.get("fg"));
let bg = to_spec(hl.get("bg"));
let attrs = hl.get("attrs").and_then(|v| v.as_u64()).map(|n| n as u32);
let styled = renderer.hlstyle(fg, bg, attrs);
let stripped = if styled.starts_with("#[") && styled.ends_with(']') {
styled[2..styled.len() - 1].to_string()
} else {
styled
};
env.push((varname.to_string(), stripped));
}
const DIVIDER_GROUPS: &[(&str, &str, &str)] = &[
(
"_POWERLINE_WINDOW_CURRENT_HARD_DIVIDER_COLOR",
"window",
"window:current",
),
(
"_POWERLINE_WINDOW_CURRENT_HARD_DIVIDER_NEXT_COLOR",
"window:current",
"window",
),
(
"_POWERLINE_SESSION_HARD_DIVIDER_NEXT_COLOR",
"session",
"background",
),
];
for (varname, prev_group, next_group) in DIVIDER_GROUPS {
let prev = match get_highlighting(prev_group) {
Some(h) => h,
None => continue,
};
let next = match get_highlighting(next_group) {
Some(h) => h,
None => continue,
};
let fg = to_spec(prev.get("bg"));
let bg = to_spec(next.get("bg"));
let styled = renderer.hlstyle(fg, bg, Some(0));
let stripped = if styled.starts_with("#[") && styled.ends_with(']') {
styled[2..styled.len() - 1].to_string()
} else {
styled
};
env.push((varname.to_string(), stripped));
}
const PER_ATTR: &[(&str, &str, &str)] = &[
("_POWERLINE_ACTIVE_WINDOW_FG", "fg", "active_window_status"),
("_POWERLINE_WINDOW_STATUS_FG", "fg", "window_status"),
("_POWERLINE_ACTIVITY_STATUS_FG", "fg", "activity_status"),
("_POWERLINE_ACTIVITY_STATUS_ATTR", "attrs", "activity_status"),
("_POWERLINE_BELL_STATUS_FG", "fg", "bell_status"),
("_POWERLINE_BELL_STATUS_ATTR", "attrs", "bell_status"),
("_POWERLINE_BACKGROUND_FG", "fg", "background"),
("_POWERLINE_BACKGROUND_BG", "bg", "background"),
("_POWERLINE_SESSION_FG", "fg", "session"),
("_POWERLINE_SESSION_BG", "bg", "session"),
("_POWERLINE_SESSION_ATTR", "attrs", "session"),
("_POWERLINE_SESSION_PREFIX_FG", "fg", "session:prefix"),
("_POWERLINE_SESSION_PREFIX_BG", "bg", "session:prefix"),
("_POWERLINE_SESSION_PREFIX_ATTR", "attrs", "session:prefix"),
];
for (varname, attr, group) in PER_ATTR {
let hl = match get_highlighting(group) {
Some(h) => h,
None => continue,
};
if *attr == "attrs" {
let raw_attrs = hl.get("attrs").and_then(|v| v.as_u64()).map(|n| n as u32);
let names = attrs_to_tmux_attrs(raw_attrs);
env.push((varname.to_string(), names.join("]#[")));
let legacy: Vec<&String> = names.iter().filter(|n| !n.starts_with("no")).collect();
let legacy_val = if legacy.is_empty() {
"none".to_string()
} else {
legacy
.into_iter()
.cloned()
.collect::<Vec<_>>()
.join(",")
};
env.push((format!("{}_LEGACY", varname), legacy_val));
} else {
let arr = hl.get(*attr).and_then(|v| v.as_array());
let cterm = arr
.and_then(|a| a.first())
.and_then(|c| c.as_u64())
.unwrap_or(0);
let truecolor = arr.and_then(|a| a.get(1)).and_then(|c| c.as_u64());
let value = if term_truecolor {
if let Some(t) = truecolor {
format!("#{:06x}", t)
} else {
format!("colour{}", cterm)
}
} else {
format!("colour{}", cterm)
};
env.push((varname.to_string(), value));
}
}
if let Some(left) = theme.dividers.get("left").and_then(|v| v.as_object()) {
let hard = left.get("hard").and_then(|v| v.as_str()).unwrap_or(" ");
let soft = left.get("soft").and_then(|v| v.as_str()).unwrap_or(" ");
env.push(("_POWERLINE_LEFT_HARD_DIVIDER".to_string(), hard.to_string()));
env.push(("_POWERLINE_LEFT_SOFT_DIVIDER".to_string(), soft.to_string()));
let width = crate::ported::renderer::strwidth(hard).max(1);
env.push((
"_POWERLINE_LEFT_HARD_DIVIDER_SPACES".to_string(),
" ".repeat(width),
));
}
env
}
pub fn get_highlighting<R>(
group: &str,
get_highlighting_fn: R,
) -> serde_json::Map<String, serde_json::Value>
where
R: FnOnce(&[&str], Option<&str>) -> serde_json::Map<String, serde_json::Value>,
{
get_highlighting_fn(&[group], None)
}
pub fn tmux_setup(source: Option<bool>) -> (std::collections::HashMap<String, String>, bool) {
let do_source = source.unwrap_or(true);
(std::collections::HashMap::new(), do_source)
}
pub fn source_tmux_file_nosource(
fname: &std::path::Path,
tmux_environ: &std::collections::HashMap<String, String>,
) -> Vec<Vec<String>> {
let content = match std::fs::read_to_string(fname) {
Ok(s) => s,
Err(_) => return Vec::new(),
};
let mut commands: Vec<Vec<String>> = Vec::new();
for line in content.lines() {
if let Some(args) = parse_tmux_file_line(line) {
let mut substituted: Vec<String> = Vec::with_capacity(args.len());
for (i, arg) in args.iter().enumerate() {
if i == 0 {
substituted.push(arg.clone());
} else {
substituted.push(replace_env(arg, tmux_environ));
}
}
commands.push(substituted);
}
}
commands
}
pub fn parse_tmux_file_line(line: &str) -> Option<Vec<String>> {
let trimmed = line.trim_end_matches('\n');
if trimmed.starts_with('#') || trimmed.is_empty() {
return None;
}
Some(shlex_split(trimmed))
}
fn shlex_split(s: &str) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
let mut current = String::new();
let mut chars = s.chars().peekable();
let mut in_single = false;
let mut in_double = false;
while let Some(c) = chars.next() {
match c {
'\\' if !in_single => {
if let Some(next) = chars.next() {
current.push(next);
}
}
'\'' if !in_double => {
in_single = !in_single;
}
'"' if !in_single => {
in_double = !in_double;
}
' ' | '\t' if !in_single && !in_double => {
if !current.is_empty() {
out.push(std::mem::take(&mut current));
}
}
_ => current.push(c),
}
}
if !current.is_empty() {
out.push(current);
}
out
}
pub fn sorted_tmux_configs(version: &TmuxVersionInfo) -> Vec<(PathBuf, i64)> {
let mut entries = get_tmux_configs(version);
entries.sort_by_key(|(_, priority)| *priority);
entries
}
pub fn uses_check_env_vars(
component: &str,
shell: Option<&str>,
environ: &std::collections::HashMap<String, String>,
) -> bool {
let component_upper = component.to_uppercase();
let shells: Vec<&str> = match shell {
Some(s) => vec![s, "shell"],
None => vec!["shell"],
};
for sh in shells {
let varname = format!("POWERLINE_NO_{}_{}", sh.to_uppercase(), component_upper);
if environ
.get(&varname)
.map(|s| !s.is_empty())
.unwrap_or(false)
{
return true;
}
}
false
}
pub fn uses_component_exit_code(
config: &serde_json::Map<String, serde_json::Value>,
component: &str,
) -> i32 {
let components = config
.get("ext")
.and_then(|v| v.as_object())
.and_then(|m| m.get("shell"))
.and_then(|v| v.as_object())
.and_then(|m| m.get("components"))
.and_then(|v| v.as_array());
let component_in = match components {
Some(arr) => arr.iter().any(|v| v.as_str() == Some(component)),
None => component == "tmux" || component == "prompt",
};
if component_in {
0
} else {
1
}
}
pub fn shell_command() -> Option<String> {
deduce_command()
}
pub fn get_main_config<F>(
search_paths: &[std::path::PathBuf],
load_fn: F,
) -> Result<serde_json::Map<String, serde_json::Value>, String>
where
F: Fn(&std::path::Path) -> Result<serde_json::Value, String>,
{
for root in search_paths {
let candidate = root.join("config.json");
if candidate.is_file() {
match load_fn(&candidate) {
Ok(v) => {
if let serde_json::Value::Object(m) = v {
return Ok(m);
} else {
return Err(format!("{} root is not a JSON object", candidate.display()));
}
}
Err(e) => return Err(e),
}
}
}
Err(format!(
"Could not find config.json in any of {} search paths",
search_paths.len()
))
}
pub fn create_powerline_logger(
main_config: &serde_json::Map<String, serde_json::Value>,
) -> crate::ported::PowerlineLogger {
let empty = serde_json::Map::new();
let common_in = main_config
.get("common")
.and_then(|v| v.as_object())
.unwrap_or(&empty);
let common = crate::ported::finish_common_config("utf-8", common_in);
crate::ported::create_logger(&common, "")
}
pub fn uses(
component: &str,
shell: Option<&str>,
environ: &std::collections::HashMap<String, String>,
main_config: &serde_json::Map<String, serde_json::Value>,
) -> Result<i32, String> {
if component.is_empty() {
return Err("Must specify component".to_string());
}
if uses_check_env_vars(component, shell, environ) {
return Ok(1);
}
Ok(uses_component_exit_code(main_config, component))
}
#[cfg(test)]
mod tests {
use super::*;
fn ver(major: f64, minor: i32) -> TmuxVersionInfo {
TmuxVersionInfo {
major,
minor,
suffix: None,
}
}
#[test]
fn config_file_name_matches_standard_format() {
let re = CONFIG_FILE_NAME();
assert!(re.is_match("powerline_tmux_2.1.conf"));
assert!(re.is_match("powerline_tmux_1.8_plus.conf"));
assert!(re.is_match("powerline_tmux_1.8_minus.conf"));
assert!(!re.is_match("powerline_tmux_2.1.txt"));
assert!(!re.is_match("powerline_2.1.conf"));
}
#[test]
fn exact_matcher_requires_same_major_minor() {
let m = ConfigMatcher::Exact;
assert!(m.applies(&ver(2.0, 1), &ver(2.0, 1)));
assert!(!m.applies(&ver(2.0, 1), &ver(2.0, 2)));
assert!(!m.applies(&ver(2.0, 1), &ver(3.0, 1)));
}
#[test]
fn plus_matcher_applies_when_file_lte_tmux() {
let m = ConfigMatcher::Plus;
assert!(m.applies(&ver(1.0, 8), &ver(1.0, 8)));
assert!(m.applies(&ver(1.0, 8), &ver(2.0, 1)));
assert!(!m.applies(&ver(2.0, 0), &ver(1.0, 9)));
}
#[test]
fn minus_matcher_applies_when_file_gte_tmux() {
let m = ConfigMatcher::Minus;
assert!(m.applies(&ver(1.0, 8), &ver(1.0, 8)));
assert!(m.applies(&ver(2.0, 1), &ver(1.0, 9)));
assert!(!m.applies(&ver(1.0, 8), &ver(2.0, 1)));
}
#[test]
fn priority_order_matches_upstream() {
assert_eq!(ConfigMatcher::Exact.priority(), 3);
assert_eq!(ConfigMatcher::Plus.priority(), 2);
assert_eq!(ConfigMatcher::Minus.priority(), 1);
}
#[test]
fn empty_args_init_stores_ext_as_single_element_list() {
let a = EmptyArgs::new("tmux", None);
assert_eq!(a.ext, vec!["tmux".to_string()]);
}
#[test]
fn empty_args_init_defaults_side_to_left() {
let a = EmptyArgs::new("tmux", None);
assert_eq!(a.side, "left");
}
#[test]
fn empty_args_init_pins_config_path_to_none() {
let a = EmptyArgs::new("tmux", Some("/etc/powerline"));
assert!(a.config_path.is_none());
}
#[test]
fn tmux_var_re_matches_dollar_powerline_var() {
let re = TMUX_VAR_RE();
assert!(re.is_match("$_POWERLINE_FOO"));
assert!(re.is_match("foo $_POWERLINE_BAR_X bar"));
assert!(!re.is_match("_POWERLINE_NOPE"));
assert!(!re.is_match("$POWERLINE_NO_UNDER"));
}
#[test]
fn tmux_var_re_captures_group_after_dollar() {
let re = TMUX_VAR_RE();
let cap = re.captures("$_POWERLINE_FG").unwrap();
assert_eq!(cap.get(1).unwrap().as_str(), "_POWERLINE_FG");
}
#[test]
fn check_command_returns_some_for_real_binary() {
let r = check_command("sh");
assert_eq!(r, Some("sh".to_string()));
}
#[test]
fn check_command_returns_none_for_missing_binary() {
let r = check_command("definitely-not-on-this-system-xyz-abc");
assert!(r.is_none());
}
#[test]
fn deduce_command_returns_some_or_none() {
let r = deduce_command();
if let Some(s) = r {
assert!(!s.is_empty());
}
}
#[test]
fn replace_env_substitutes_known_var() {
let mut env = std::collections::HashMap::new();
env.insert("_POWERLINE_FG".to_string(), "#abcdef".to_string());
let r = replace_env("$_POWERLINE_FG", &env);
assert_eq!(r, "#abcdef");
}
#[test]
fn replace_env_passes_through_unknown_var() {
let env = std::collections::HashMap::new();
let r = replace_env("$_POWERLINE_MISSING", &env);
assert_eq!(r, "$_POWERLINE_MISSING");
}
#[test]
fn replace_env_substitutes_multiple_in_one_string() {
let mut env = std::collections::HashMap::new();
env.insert("_POWERLINE_FG".to_string(), "fg".to_string());
env.insert("_POWERLINE_BG".to_string(), "bg".to_string());
let r = replace_env("$_POWERLINE_FG/$_POWERLINE_BG", &env);
assert_eq!(r, "fg/bg");
}
#[test]
fn replace_env_leaves_non_dollar_powerline_text_alone() {
let env = std::collections::HashMap::new();
let r = replace_env("plain text", &env);
assert_eq!(r, "plain text");
}
#[test]
fn parse_tmux_file_line_skips_comments() {
assert!(parse_tmux_file_line("# comment").is_none());
assert!(parse_tmux_file_line("#another").is_none());
}
#[test]
fn parse_tmux_file_line_skips_blank_lines() {
assert!(parse_tmux_file_line("\n").is_none());
assert!(parse_tmux_file_line("").is_none());
}
#[test]
fn parse_tmux_file_line_splits_simple_args() {
let r = parse_tmux_file_line("set -g status on").unwrap();
assert_eq!(r, vec!["set", "-g", "status", "on"]);
}
#[test]
fn parse_tmux_file_line_handles_quoted_args() {
let r = parse_tmux_file_line("set status-left \"a b c\"").unwrap();
assert_eq!(r, vec!["set", "status-left", "a b c"]);
}
#[test]
fn sorted_tmux_configs_returns_entries_in_ascending_priority_order() {
let version = TmuxVersionInfo {
major: 2.0,
minor: 1,
suffix: None,
};
let entries = sorted_tmux_configs(&version);
let mut prev = i64::MIN;
for (_, priority) in &entries {
assert!(*priority >= prev);
prev = *priority;
}
}
#[test]
fn uses_check_env_vars_returns_true_for_powerline_no_var() {
let mut env = std::collections::HashMap::new();
env.insert("POWERLINE_NO_BASH_PROMPT".to_string(), "1".to_string());
assert!(uses_check_env_vars("prompt", Some("bash"), &env));
}
#[test]
fn uses_check_env_vars_checks_shell_fallback() {
let mut env = std::collections::HashMap::new();
env.insert("POWERLINE_NO_SHELL_TMUX".to_string(), "1".to_string());
assert!(uses_check_env_vars("tmux", Some("zsh"), &env));
assert!(uses_check_env_vars("tmux", None, &env));
}
#[test]
fn uses_check_env_vars_returns_false_when_unset() {
let env = std::collections::HashMap::new();
assert!(!uses_check_env_vars("prompt", Some("bash"), &env));
}
#[test]
fn uses_check_env_vars_returns_false_when_var_is_empty() {
let mut env = std::collections::HashMap::new();
env.insert("POWERLINE_NO_BASH_PROMPT".to_string(), "".to_string());
assert!(!uses_check_env_vars("prompt", Some("bash"), &env));
}
#[test]
fn uses_component_exit_code_returns_0_when_component_in_config() {
let cfg: serde_json::Map<String, serde_json::Value> =
serde_json::from_str(r#"{"ext": {"shell": {"components": ["tmux", "prompt"]}}}"#)
.unwrap();
assert_eq!(uses_component_exit_code(&cfg, "tmux"), 0);
assert_eq!(uses_component_exit_code(&cfg, "prompt"), 0);
}
#[test]
fn uses_component_exit_code_returns_1_when_component_not_in_config() {
let cfg: serde_json::Map<String, serde_json::Value> =
serde_json::from_str(r#"{"ext": {"shell": {"components": ["prompt"]}}}"#).unwrap();
assert_eq!(uses_component_exit_code(&cfg, "tmux"), 1);
}
#[test]
fn uses_component_exit_code_default_components_when_missing() {
let cfg = serde_json::Map::new();
assert_eq!(uses_component_exit_code(&cfg, "tmux"), 0);
assert_eq!(uses_component_exit_code(&cfg, "prompt"), 0);
assert_eq!(uses_component_exit_code(&cfg, "other"), 1);
}
#[test]
fn shell_command_returns_deduce_command_result() {
assert_eq!(shell_command(), deduce_command());
}
#[test]
fn set_tmux_environment_nosource_inserts_entry() {
let mut env = std::collections::HashMap::new();
set_tmux_environment_nosource(&mut env, "_POWERLINE_X", "y", true);
assert_eq!(env.get("_POWERLINE_X"), Some(&"y".to_string()));
}
#[test]
fn replace_cb_returns_value_for_known_key() {
let mut env = std::collections::HashMap::new();
env.insert("_POWERLINE_FG".to_string(), "white".to_string());
assert_eq!(replace_cb(&env, "_POWERLINE_FG"), Some("white".to_string()));
assert!(replace_cb(&env, "_POWERLINE_BG").is_none());
}
#[test]
fn get_main_config_returns_err_when_no_config_found() {
let r = get_main_config(
&[std::path::PathBuf::from("/nonexistent_xxx")],
|_| unreachable!(),
);
assert!(r.is_err());
assert!(r.unwrap_err().contains("Could not find config.json"));
}
#[test]
fn get_main_config_returns_loaded_object() {
let tmp = std::env::temp_dir().join("powerliners_test_get_main_config");
std::fs::create_dir_all(&tmp).unwrap();
std::fs::write(tmp.join("config.json"), r#"{"common":{},"ext":{}}"#).unwrap();
let r = get_main_config(std::slice::from_ref(&tmp), |path| {
let s = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
serde_json::from_str(&s).map_err(|e| e.to_string())
});
assert!(r.is_ok());
let obj = r.unwrap();
assert!(obj.contains_key("common"));
assert!(obj.contains_key("ext"));
std::fs::remove_dir_all(&tmp).ok();
}
#[test]
fn uses_requires_non_empty_component() {
let env = std::collections::HashMap::new();
let cfg = serde_json::Map::new();
let r = uses("", Some("zsh"), &env, &cfg);
assert!(r.is_err());
assert_eq!(r.unwrap_err(), "Must specify component");
}
#[test]
fn uses_returns_1_when_env_var_set() {
let mut env = std::collections::HashMap::new();
env.insert("POWERLINE_NO_ZSH_TMUX".to_string(), "1".to_string());
let cfg = serde_json::Map::new();
assert_eq!(uses("tmux", Some("zsh"), &env, &cfg).unwrap(), 1);
}
#[test]
fn uses_returns_0_when_component_in_default_components() {
let env = std::collections::HashMap::new();
let cfg = serde_json::Map::new();
assert_eq!(uses("tmux", None, &env, &cfg).unwrap(), 0);
assert_eq!(uses("prompt", None, &env, &cfg).unwrap(), 0);
assert_eq!(uses("nonexistent", None, &env, &cfg).unwrap(), 1);
}
#[test]
fn create_powerline_logger_constructs_logger() {
let cfg = serde_json::Map::new();
let _logger = create_powerline_logger(&cfg);
}
#[test]
fn source_tmux_files_includes_powerline_base() {
let v = ver(2.0, 0);
let files = source_tmux_files(&v);
let base = files.iter().find(|p| {
p.file_name()
.map(|s| s == "powerline-base.conf")
.unwrap_or(false)
});
assert!(base.is_some());
}
#[test]
fn tmux_setup_default_source_flag_is_true() {
let (env, do_source) = tmux_setup(None);
assert!(do_source);
assert!(env.is_empty());
}
#[test]
fn tmux_setup_explicit_source_false_returns_false() {
let (_env, do_source) = tmux_setup(Some(false));
assert!(!do_source);
}
#[test]
fn init_tmux_environment_emits_color_dividers_and_per_attrs() {
use crate::ported::colorscheme::Colorscheme;
use crate::ported::renderers::tmux::TmuxRenderer;
use crate::ported::theme::Theme;
use serde_json::{json, Value};
let cs_json = json!({
"groups": {
"background": {"fg": "white", "bg": "black"},
"active_window_status":{"fg": "white", "bg": "gray0"},
"window_status": {"fg": "gray8", "bg": "gray0"},
"activity_status": {"fg": "gray8", "bg": "gray0", "attrs": ["bold"]},
"bell_status": {"fg": "red", "bg": "gray0"},
"window": {"fg": "gray8", "bg": "gray0"},
"window:divider": {"fg": "gray5", "bg": "gray0"},
"window:current": {"fg": "white", "bg": "gray0"},
"window_name": {"fg": "white", "bg": "gray0", "attrs": ["bold"]},
"session": {"fg": "black", "bg": "white", "attrs": ["bold"]},
"session:prefix": {"fg": "white", "bg": "red", "attrs": ["bold"]},
}
});
let colors_json = json!({
"colors": {
"white": 231, "black": 16, "red": 1, "gray0": 233, "gray5": 241, "gray8": 247,
},
"gradients": {}
});
let cs = Colorscheme::new(
cs_json.as_object().unwrap(),
colors_json.as_object().unwrap(),
);
let mut theme = Theme::default();
let mut left = serde_json::Map::new();
left.insert("hard".to_string(), Value::String("\u{e0b0}".into()));
left.insert("soft".to_string(), Value::String("\u{e0b1}".into()));
theme
.dividers
.insert("left".to_string(), Value::Object(left));
let renderer = TmuxRenderer::new(false);
let env = init_tmux_environment(&cs, &theme, &renderer, false);
let names: Vec<&String> = env.iter().map(|(k, _)| k).collect();
assert!(names.contains(&&"_POWERLINE_BACKGROUND_COLOR".to_string()));
assert!(names.contains(&&"_POWERLINE_SESSION_COLOR".to_string()));
assert!(names.contains(&&"_POWERLINE_WINDOW_CURRENT_HARD_DIVIDER_COLOR".to_string()));
assert!(names.contains(&&"_POWERLINE_SESSION_FG".to_string()));
assert!(names.contains(&&"_POWERLINE_SESSION_BG".to_string()));
assert!(names.contains(&&"_POWERLINE_SESSION_ATTR".to_string()));
assert!(names.contains(&&"_POWERLINE_SESSION_ATTR_LEGACY".to_string()));
assert!(names.contains(&&"_POWERLINE_LEFT_HARD_DIVIDER".to_string()));
assert!(names.contains(&&"_POWERLINE_LEFT_SOFT_DIVIDER".to_string()));
assert!(names.contains(&&"_POWERLINE_LEFT_HARD_DIVIDER_SPACES".to_string()));
let session_fg = env
.iter()
.find(|(k, _)| k == "_POWERLINE_SESSION_FG")
.map(|(_, v)| v.clone())
.unwrap();
assert!(
session_fg.starts_with("colour"),
"session_fg = {}",
session_fg
);
}
#[test]
fn get_highlighting_dispatches_to_resolver_with_single_group() {
let r = get_highlighting("background", |groups, mode| {
assert_eq!(groups, &["background"]);
assert!(mode.is_none());
let mut m = serde_json::Map::new();
m.insert("fg".to_string(), serde_json::json!([15, 0xffffff]));
m
});
assert!(r.contains_key("fg"));
}
#[test]
fn source_tmux_file_nosource_skips_comments_and_blanks() {
let tmp = std::env::temp_dir().join("powerliners_test_source_tmux_nosource.conf");
std::fs::write(&tmp, "# comment\n\nset-option -g status on\n").unwrap();
let env = std::collections::HashMap::new();
let commands = source_tmux_file_nosource(&tmp, &env);
assert_eq!(commands.len(), 1);
assert_eq!(commands[0][0], "set-option");
std::fs::remove_file(&tmp).ok();
}
}