pub mod bindings;
pub mod colorscheme;
pub mod commands;
pub mod config;
pub mod ipython;
pub mod lemonbar;
pub mod lib;
pub mod lint;
pub mod listers;
pub mod matchers;
pub mod pdb;
pub mod renderer;
pub mod renderers;
pub mod scripts;
pub mod segment;
pub mod segments;
pub mod selectors;
pub mod shell;
pub mod theme;
pub mod version;
pub mod vim;
use crate::ported::config::DEFAULT_SYSTEM_CONFIG_DIR;
use serde_json::{Map, Value};
#[derive(Debug, Clone)]
pub struct NotInterceptedError(pub String);
impl std::fmt::Display for NotInterceptedError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for NotInterceptedError {}
pub fn _config_loader_condition(path: Option<&std::path::Path>) -> Option<std::path::PathBuf> {
let p = path?;
if p.is_file() {
Some(p.to_path_buf())
} else {
None
}
}
pub fn _find_config_files(
search_paths: &[std::path::PathBuf],
config_file: &str,
) -> Result<Vec<std::path::PathBuf>, String> {
let with_ext = format!("{}.json", config_file);
let mut found: Vec<std::path::PathBuf> = Vec::new();
for path in search_paths {
let candidate = path.join(&with_ext);
if candidate.is_file() {
found.push(candidate);
}
}
if found.is_empty() {
let paths_joined: Vec<String> = search_paths
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect();
Err(format!(
"Config file not found in search paths ({}): {}",
paths_joined.join(", "),
with_ext
))
} else {
Ok(found)
}
}
pub struct PowerlineLogger {
pub ext: String,
pub prefix: String,
pub messages: std::sync::Mutex<Vec<(String, String)>>,
}
impl PowerlineLogger {
pub fn new(ext: impl Into<String>, prefix: impl Into<String>) -> Self {
Self {
ext: ext.into(),
prefix: prefix.into(),
messages: std::sync::Mutex::new(Vec::new()),
}
}
pub fn format_message(&self, message: &str, prefix: Option<&str>) -> String {
let effective_prefix = prefix.unwrap_or(&self.prefix);
format!("{}:{}:{}", self.ext, effective_prefix, message)
}
pub fn log(&self, level: &str, message: &str) {
let formatted = self.format_message(message, None);
self.messages
.lock()
.unwrap_or_else(|e| e.into_inner())
.push((level.to_string(), formatted));
}
pub fn debug(&self, message: &str) {
self.log("DEBUG", message);
}
pub fn info(&self, message: &str) {
self.log("INFO", message);
}
pub fn warn(&self, message: &str) {
self.log("WARNING", message);
}
pub fn error(&self, message: &str) {
self.log("ERROR", message);
}
pub fn critical(&self, message: &str) {
self.log("CRITICAL", message);
}
pub fn exception(&self, message: &str) {
self.log("EXCEPTION", message);
}
}
pub fn get_config_paths() -> Vec<std::path::PathBuf> {
let home = std::env::var("HOME").unwrap_or_default();
let config_home =
std::env::var("XDG_CONFIG_HOME").unwrap_or_else(|_| format!("{}/.config", home));
let mut config_paths: Vec<std::path::PathBuf> = vec![std::path::PathBuf::from(format!(
"{}/powerline",
config_home
))];
let config_dirs = std::env::var("XDG_CONFIG_DIRS").unwrap_or_else(|_| {
DEFAULT_SYSTEM_CONFIG_DIR()
.as_ref()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default()
});
let dir_paths: Vec<std::path::PathBuf> = config_dirs
.split(':')
.filter(|p| !p.is_empty())
.map(|d| std::path::PathBuf::from(format!("{}/powerline", d)))
.collect();
for p in dir_paths.into_iter().rev() {
config_paths.insert(0, p);
}
let plugin_path = std::path::PathBuf::from("config_files");
config_paths.insert(0, plugin_path);
config_paths
}
pub fn get_default_theme(is_unicode: bool) -> &'static str {
if is_unicode {
"powerline_terminus"
} else {
"ascii"
}
}
pub fn finish_common_config(
encoding: &str,
common_config: &Map<String, Value>,
) -> Map<String, Value> {
let encoding = encoding.to_lowercase();
let default_top_theme =
get_default_theme(encoding.starts_with("utf") || encoding.starts_with("ucs"));
let mut cfg = common_config.clone();
cfg.entry("default_top_theme")
.or_insert_with(|| Value::String(default_top_theme.into()));
cfg.entry("paths")
.or_insert_with(|| Value::Array(Vec::new()));
cfg.entry("watcher")
.or_insert_with(|| Value::String("auto".into()));
cfg.entry("log_level")
.or_insert_with(|| Value::String("WARNING".into()));
cfg.entry("log_format")
.or_insert_with(|| Value::String("%(asctime)s:%(levelname)s:%(message)s".into()));
cfg.entry("term_truecolor").or_insert(Value::Bool(false));
cfg.entry("term_escape_style")
.or_insert_with(|| Value::String("auto".into()));
cfg.entry("ambiwidth").or_insert(Value::from(1));
cfg.entry("additional_escapes").or_insert(Value::Null);
cfg.entry("reload_config").or_insert(Value::Bool(true));
cfg.entry("interval").or_insert(Value::Null);
cfg.entry("log_file")
.or_insert_with(|| Value::Array(vec![Value::Null]));
let log_file_v = cfg["log_file"].clone();
if !log_file_v.is_array() {
cfg.insert("log_file".to_string(), Value::Array(vec![log_file_v]));
}
let home = std::env::var("HOME").unwrap_or_default();
if let Some(paths) = cfg.get_mut("paths").and_then(|v| v.as_array_mut()) {
for p in paths.iter_mut() {
if let Some(s) = p.as_str() {
let expanded = if s.starts_with('~') {
s.replacen('~', &home, 1)
} else {
s.to_string()
};
*p = Value::String(expanded);
}
}
}
cfg
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
use std::sync::OnceLock;
static TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
macro_rules! lock_env {
() => {{
TEST_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|e| e.into_inner())
}};
}
#[test]
fn not_intercepted_error_implements_error() {
let e = NotInterceptedError("boom".to_string());
assert!(e.to_string().contains("boom"));
let _: &dyn std::error::Error = &e;
}
#[test]
fn config_loader_condition_returns_path_when_exists() {
let cargo_toml = std::env::current_dir().unwrap().join("Cargo.toml");
let r = _config_loader_condition(Some(&cargo_toml));
assert_eq!(r, Some(cargo_toml));
}
#[test]
fn config_loader_condition_returns_none_when_missing() {
let p = std::path::PathBuf::from("/never_exists_path_xyz_99");
let r = _config_loader_condition(Some(&p));
assert!(r.is_none());
}
#[test]
fn config_loader_condition_returns_none_when_none() {
let r = _config_loader_condition(None);
assert!(r.is_none());
}
#[test]
fn find_config_files_error_when_not_found() {
let r = _find_config_files(&[std::path::PathBuf::from("/nowhere")], "missing");
assert!(r.is_err());
assert!(r.unwrap_err().contains("Config file not found"));
}
#[test]
fn find_config_files_appends_json_suffix_to_message() {
let r = _find_config_files(&[std::path::PathBuf::from("/nowhere")], "powerline");
let msg = r.unwrap_err();
assert!(msg.contains("powerline.json"));
}
#[test]
fn get_default_theme_unicode_returns_powerline_terminus() {
assert_eq!(get_default_theme(true), "powerline_terminus");
}
#[test]
fn get_default_theme_non_unicode_returns_ascii() {
assert_eq!(get_default_theme(false), "ascii");
}
#[test]
fn finish_common_config_fills_default_top_theme_for_utf8() {
let r = finish_common_config("UTF-8", &Map::new());
assert_eq!(r["default_top_theme"], "powerline_terminus");
}
#[test]
fn finish_common_config_fills_default_top_theme_for_ucs() {
let r = finish_common_config("UCS-2", &Map::new());
assert_eq!(r["default_top_theme"], "powerline_terminus");
}
#[test]
fn finish_common_config_fills_default_top_theme_for_ascii() {
let r = finish_common_config("latin-1", &Map::new());
assert_eq!(r["default_top_theme"], "ascii");
}
#[test]
fn finish_common_config_fills_all_defaults() {
let r = finish_common_config("UTF-8", &Map::new());
assert_eq!(r["watcher"], "auto");
assert_eq!(r["log_level"], "WARNING");
assert_eq!(r["term_truecolor"], false);
assert_eq!(r["term_escape_style"], "auto");
assert_eq!(r["ambiwidth"], 1);
assert_eq!(r["additional_escapes"], Value::Null);
assert_eq!(r["reload_config"], true);
assert_eq!(r["interval"], Value::Null);
}
#[test]
fn finish_common_config_log_file_defaults_to_list_with_null() {
let r = finish_common_config("UTF-8", &Map::new());
let lf = r["log_file"].as_array().unwrap();
assert_eq!(lf.len(), 1);
assert_eq!(lf[0], Value::Null);
}
#[test]
fn finish_common_config_wraps_non_list_log_file() {
let mut input = Map::new();
input.insert(
"log_file".to_string(),
Value::String("/var/log/p.log".into()),
);
let r = finish_common_config("UTF-8", &input);
let lf = r["log_file"].as_array().unwrap();
assert_eq!(lf.len(), 1);
assert_eq!(lf[0], "/var/log/p.log");
}
#[test]
fn finish_common_config_preserves_supplied_values() {
let mut input = Map::new();
input.insert("watcher".to_string(), Value::String("inotify".into()));
input.insert("term_truecolor".to_string(), Value::Bool(true));
let r = finish_common_config("UTF-8", &input);
assert_eq!(r["watcher"], "inotify");
assert_eq!(r["term_truecolor"], true);
}
#[test]
fn finish_common_config_expands_tilde_in_paths() {
let _g = lock_env!();
unsafe {
std::env::set_var("HOME", "/home/alice");
}
let mut input = Map::new();
input.insert(
"paths".to_string(),
Value::Array(vec![Value::String("~/custom".into())]),
);
let r = finish_common_config("UTF-8", &input);
let paths = r["paths"].as_array().unwrap();
assert_eq!(paths[0], "/home/alice/custom");
unsafe {
std::env::remove_var("HOME");
}
}
#[test]
fn powerline_logger_format_includes_ext_prefix_message() {
let l = PowerlineLogger::new("vim", "powerline");
let s = l.format_message("hello", None);
assert_eq!(s, "vim:powerline:hello");
}
#[test]
fn powerline_logger_format_overrides_prefix() {
let l = PowerlineLogger::new("vim", "default");
let s = l.format_message("hi", Some("seg"));
assert_eq!(s, "vim:seg:hi");
}
#[test]
fn powerline_logger_log_captures_message() {
let l = PowerlineLogger::new("shell", "p");
l.debug("d");
l.info("i");
l.warn("w");
l.error("e");
l.critical("c");
l.exception("ex");
let msgs = l.messages.lock().unwrap();
assert_eq!(msgs.len(), 6);
assert_eq!(msgs[0].0, "DEBUG");
assert_eq!(msgs[5].0, "EXCEPTION");
}
#[test]
fn get_config_paths_includes_plugin_path_first() {
let paths = get_config_paths();
assert_eq!(paths[0].file_name().unwrap(), "config_files");
}
#[test]
fn get_config_paths_includes_xdg_config_home() {
let _g = lock_env!();
unsafe {
std::env::set_var("XDG_CONFIG_HOME", "/custom/cfg");
std::env::remove_var("XDG_CONFIG_DIRS");
}
let paths = get_config_paths();
let has_xdg = paths.iter().any(|p| p.starts_with("/custom/cfg"));
unsafe {
std::env::remove_var("XDG_CONFIG_HOME");
}
assert!(has_xdg);
}
}
#[allow(non_snake_case)]
pub fn LOG_KEYS() -> &'static std::collections::HashSet<&'static str> {
static S: std::sync::OnceLock<std::collections::HashSet<&'static str>> =
std::sync::OnceLock::new();
S.get_or_init(|| {
let mut s = std::collections::HashSet::new();
s.insert("log_format");
s.insert("log_level");
s.insert("log_file");
s.insert("paths");
s
})
}
pub const DEFAULT_UPDATE_INTERVAL: u64 = 2;
pub fn _get_log_keys(
common_config: &serde_json::Map<String, serde_json::Value>,
) -> serde_json::Map<String, serde_json::Value> {
let log_keys = LOG_KEYS();
let mut out = serde_json::Map::new();
for (k, v) in common_config {
if log_keys.contains(k.as_str()) {
out.insert(k.clone(), v.clone());
}
}
out
}
pub fn _generate_change_callback(
lock: std::sync::Arc<std::sync::Mutex<serde_json::Map<String, serde_json::Value>>>,
key: String,
) -> Box<dyn Fn(&str)> {
Box::new(move |_path: &str| {
let mut m = lock.lock().unwrap_or_else(|e| e.into_inner());
m.insert(key.clone(), serde_json::Value::Bool(true));
})
}
#[derive(Debug, Clone)]
pub struct Powerline {
pub ext: String,
pub run_once: bool,
pub had_logger: bool,
pub use_daemon_threads: bool,
pub renderer_module: String,
pub update_interval: u64,
}
impl Powerline {
pub fn init(
ext: &str,
renderer_module: Option<&str>,
run_once: bool,
had_logger: bool,
) -> Self {
Powerline {
ext: ext.to_string(),
run_once,
had_logger,
use_daemon_threads: true,
renderer_module: Self::resolve_renderer_module(ext, renderer_module),
update_interval: DEFAULT_UPDATE_INTERVAL,
}
}
pub fn resolve_renderer_module(ext: &str, renderer_module: Option<&str>) -> String {
match renderer_module {
None => format!("powerline.renderers.{}", ext),
Some("") => format!("powerline.renderers.{}", ext),
Some(name) => {
if !name.contains('.') {
format!("powerline.renderers.{}", name)
} else if name.starts_with('.') {
format!("powerline.renderers.{}{}", ext, name)
} else if name.ends_with('.') {
name.trim_end_matches('.').to_string()
} else {
name.to_string()
}
}
}
}
pub fn setup_components(&self, _components: Option<&[&str]>) {
}
pub fn get_local_themes(
_local_themes: Option<&serde_json::Value>,
) -> Option<serde_json::Value> {
None
}
pub fn do_setup() {
}
pub fn get_config_paths() -> Vec<std::path::PathBuf> {
get_config_paths()
}
pub fn load_main_config<F>(load_fn: F) -> Result<Map<String, Value>, String>
where
F: Fn(&str, &str) -> Result<Map<String, Value>, String>,
{
load_fn("config", "main")
}
pub fn load_colors_config<F>(load_fn: F) -> Result<Map<String, Value>, String>
where
F: Fn(&str, &str) -> Result<Map<String, Value>, String>,
{
load_fn("colors", "colors")
}
pub fn load_colorscheme_config_levels(ext: &str, name: &str) -> (Vec<String>, Vec<usize>) {
let levels = vec![
format!("colorschemes/{}", name),
format!("colorschemes/{}/__main__", ext),
format!("colorschemes/{}/{}", ext, name),
];
(levels, vec![1])
}
pub fn load_theme_config_levels(
theme_levels: &[String],
ext: &str,
name: &str,
) -> (Vec<String>, Vec<usize>) {
let mut levels: Vec<String> = theme_levels.to_vec();
levels.push(format!("themes/{}/{}", ext, name));
(levels, vec![0, 1])
}
pub fn setup(shutdown_event: &std::sync::Arc<std::sync::atomic::AtomicBool>) {
shutdown_event.store(false, std::sync::atomic::Ordering::SeqCst);
Self::do_setup();
}
pub fn shutdown(
shutdown_event: &std::sync::Arc<std::sync::atomic::AtomicBool>,
set_event: bool,
) {
if set_event {
shutdown_event.store(true, std::sync::atomic::Ordering::SeqCst);
}
}
pub fn load_config_instance<F, LF>(
cfg_path: &str,
find_config_files: F,
load_fn: LF,
) -> Result<Map<String, Value>, String>
where
F: Fn(&str) -> Result<Vec<std::path::PathBuf>, String>,
LF: Fn(&std::path::Path) -> Result<Map<String, Value>, String>,
{
load_config(cfg_path, find_config_files, load_fn)
}
pub fn _purge_configs(function_id: u64) -> (u64, u64) {
(function_id, function_id)
}
pub fn load_colorscheme_config<F>(
ext: &str,
name: &str,
load_one: F,
) -> Result<Map<String, Value>, String>
where
F: Fn(&str) -> Result<Map<String, Value>, String>,
{
let (levels, ignore) = Self::load_colorscheme_config_levels(ext, name);
_load_hierarhical_config(&levels, &ignore, load_one)
}
pub fn load_theme_config<F>(
theme_levels: &[String],
ext: &str,
name: &str,
load_one: F,
) -> Result<Map<String, Value>, String>
where
F: Fn(&str) -> Result<Map<String, Value>, String>,
{
let (levels, ignore) = Self::load_theme_config_levels(theme_levels, ext, name);
_load_hierarhical_config(&levels, &ignore, load_one)
}
pub fn powerline_exception(
msg: &str,
explicit_prefix: Option<&str>,
has_pl: bool,
) -> (String, String, bool) {
let prefix = explicit_prefix.unwrap_or("powerline").to_string();
let used_fallback = !has_pl;
(prefix, msg.to_string(), used_fallback)
}
pub fn enter(&self) {
}
pub fn exit(shutdown_event: &std::sync::Arc<std::sync::atomic::AtomicBool>) {
Self::shutdown(shutdown_event, true);
}
pub fn update_renderer<F>(
has_pending_kwargs: bool,
has_existing_renderer: bool,
create_renderer: F,
) -> Result<bool, String>
where
F: FnOnce() -> Result<(), String>,
{
if !has_pending_kwargs {
return Ok(false);
}
match create_renderer() {
Ok(()) => {
Ok(true)
}
Err(e) => {
if has_existing_renderer {
Ok(false)
} else {
Err(format!("Failed to create renderer: {}", e))
}
}
}
}
pub fn render<U, R>(
update_renderer: U,
renderer_render: R,
output_width: bool,
) -> Result<String, (String, Option<usize>)>
where
U: FnOnce() -> Result<(), String>,
R: FnOnce() -> Result<String, String>,
{
let result = update_renderer().and_then(|()| renderer_render());
match result {
Ok(s) => Ok(s),
Err(e) => {
let failed = format!("Failed to render: {}", e);
let width = if output_width {
Some(failed.len())
} else {
None
};
Err((failed, width))
}
}
}
pub fn render_above_lines<U, R>(update_renderer: U, renderer_above: R) -> Vec<String>
where
U: FnOnce() -> Result<(), String>,
R: FnOnce() -> Result<Vec<String>, String>,
{
match update_renderer().and_then(|()| renderer_above()) {
Ok(lines) => lines,
Err(e) => {
vec![format!("Failed to render: {}", e)]
}
}
}
pub fn _load_hierarhical_config_instance<F>(
levels: &[String],
ignore_levels: &[usize],
load_one: F,
) -> Result<Map<String, Value>, String>
where
F: Fn(&str) -> Result<Map<String, Value>, String>,
{
_load_hierarhical_config(levels, ignore_levels, load_one)
}
pub fn create_logger_instance(ext: &str) -> PowerlineLogger {
PowerlineLogger::new(ext, "")
}
pub fn reload<C, S, R>(
shutdown_event: &std::sync::Arc<std::sync::atomic::AtomicBool>,
clear_modules: C,
shutdown_self: S,
reconstruct: R,
) -> Result<(), String>
where
C: FnOnce() -> Result<(), String>,
S: FnOnce(),
R: FnOnce() -> Result<(), String>,
{
clear_modules()?;
Self::shutdown(shutdown_event, true);
shutdown_self();
reconstruct()
}
}
pub fn reraise(exception_msg: &str) -> String {
exception_msg.to_string()
}
pub fn gen_module_attr_getter<F>(module_attr_lookup: F) -> Box<dyn Fn(&str, &str) -> Option<String>>
where
F: Fn(&str, &str) -> Option<String> + Send + Sync + 'static,
{
Box::new(move |module: &str, attr: &str| -> Option<String> {
module_attr_lookup(module, attr)
})
}
pub fn get_fallback_logger() -> PowerlineLogger {
PowerlineLogger::new("powerline", "_fallback_")
}
pub fn _set_log_handlers(common_config: &Map<String, Value>) -> Result<usize, String> {
let log_targets = common_config
.get("log_file")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let mut num_handlers: usize = 0;
for log_target in &log_targets {
if log_target.is_null() {
num_handlers += 1;
continue;
}
if let Some(s) = log_target.as_str() {
let expanded = if let Some(home) = std::env::var_os("HOME") {
if s.starts_with('~') {
s.replacen('~', &home.to_string_lossy(), 1)
} else {
s.to_string()
}
} else {
s.to_string()
};
let log_dir = std::path::Path::new(&expanded)
.parent()
.map(|p| p.to_path_buf());
if let Some(d) = log_dir {
if !d.as_os_str().is_empty() && !d.is_dir() {
let _ = std::fs::create_dir_all(&d);
}
}
num_handlers += 1;
continue;
}
num_handlers += 1;
}
if num_handlers == 0 && !log_targets.is_empty() {
return Err("Failed to set up any handlers".to_string());
}
Ok(num_handlers)
}
pub fn create_logger(_common_config: &Map<String, Value>, ext: &str) -> PowerlineLogger {
PowerlineLogger::new(ext, "")
}
pub fn create_renderer<M, C>(
load_main: bool,
load_colors: bool,
load_colorscheme: bool,
load_theme: bool,
has_existing_renderer: bool,
construct: C,
_load_main_config: M,
) -> Result<bool, String>
where
M: FnOnce() -> Result<Map<String, Value>, String>,
C: FnOnce() -> Result<(), String>,
{
let mut common_config_differs = false;
let ext_config_differs = false;
if load_main {
common_config_differs = true;
}
let create_renderer_flag = load_colors
|| load_colorscheme
|| load_theme
|| common_config_differs
|| ext_config_differs;
if load_colors {
}
if load_colorscheme || load_colors {
}
if load_theme {
}
if create_renderer_flag {
match construct() {
Ok(()) => {
Ok(true)
}
Err(e) => {
if has_existing_renderer {
Ok(false)
} else {
Err(format!("Failed to construct renderer object: {}", e))
}
}
}
} else {
Ok(false)
}
}
pub fn generate_config_finder(
get_paths: impl Fn() -> Vec<std::path::PathBuf>,
) -> Box<dyn Fn(&str) -> Result<Vec<std::path::PathBuf>, String>> {
let config_paths = get_paths();
Box::new(move |cfg_path: &str| _find_config_files(&config_paths, cfg_path))
}
pub fn load_config<FF, LF>(
cfg_path: &str,
find_config_files: FF,
load_fn: LF,
) -> Result<Map<String, Value>, String>
where
FF: Fn(&str) -> Result<Vec<std::path::PathBuf>, String>,
LF: Fn(&std::path::Path) -> Result<Map<String, Value>, String>,
{
let found_files = find_config_files(cfg_path)?;
let mut ret: Option<Map<String, Value>> = None;
for path in &found_files {
let cfg = load_fn(path)?;
match ret.as_mut() {
None => ret = Some(cfg),
Some(acc) => {
crate::ported::lib::dict::mergedicts(acc, cfg, false);
}
}
}
ret.ok_or_else(|| format!("No config files found for {}", cfg_path))
}
pub fn _load_hierarhical_config<F>(
levels: &[String],
ignore_levels: &[usize],
load_one: F,
) -> Result<Map<String, Value>, String>
where
F: Fn(&str) -> Result<Map<String, Value>, String>,
{
let mut config = Map::new();
let mut loaded = 0;
let mut last_err: Option<String> = None;
for (i, cfg_path) in levels.iter().enumerate() {
match load_one(cfg_path) {
Ok(lvl_config) => {
if !ignore_levels.contains(&i) {
loaded += 1;
}
crate::ported::lib::dict::mergedicts(&mut config, lvl_config, false);
}
Err(e) => {
last_err = Some(e);
}
}
}
if loaded == 0 {
return Err(last_err.unwrap_or_else(|| "No config files loaded".to_string()));
}
Ok(config)
}
#[cfg(test)]
mod powerline_class_tests {
use super::*;
#[test]
fn log_keys_has_four_entries() {
let k = LOG_KEYS();
assert_eq!(k.len(), 4);
assert!(k.contains("log_format"));
assert!(k.contains("log_level"));
assert!(k.contains("log_file"));
assert!(k.contains("paths"));
}
#[test]
fn default_update_interval_is_two() {
assert_eq!(DEFAULT_UPDATE_INTERVAL, 2);
}
#[test]
fn get_log_keys_filters_dict_to_log_keys_only() {
let mut cfg = serde_json::Map::new();
cfg.insert(
"log_format".to_string(),
serde_json::Value::String("X".into()),
);
cfg.insert(
"log_level".to_string(),
serde_json::Value::String("DEBUG".into()),
);
cfg.insert("ambiwidth".to_string(), serde_json::Value::from(1));
cfg.insert("term_truecolor".to_string(), serde_json::Value::Bool(true));
let r = _get_log_keys(&cfg);
assert_eq!(r.len(), 2);
assert!(r.contains_key("log_format"));
assert!(r.contains_key("log_level"));
assert!(!r.contains_key("ambiwidth"));
}
#[test]
fn get_log_keys_empty_input_returns_empty() {
let cfg = serde_json::Map::new();
let r = _get_log_keys(&cfg);
assert!(r.is_empty());
}
#[test]
fn get_log_keys_no_log_keys_returns_empty() {
let mut cfg = serde_json::Map::new();
cfg.insert("foo".to_string(), serde_json::Value::Bool(true));
cfg.insert("bar".to_string(), serde_json::Value::from(42));
let r = _get_log_keys(&cfg);
assert!(r.is_empty());
}
#[test]
fn generate_change_callback_sets_key_true_on_invocation() {
let m = std::sync::Arc::new(std::sync::Mutex::new(serde_json::Map::new()));
let cb = _generate_change_callback(m.clone(), "load_main".to_string());
cb("/some/path/config.json");
let map = m.lock().unwrap();
assert_eq!(map.get("load_main"), Some(&serde_json::Value::Bool(true)));
}
#[test]
fn generate_change_callback_overwrites_existing_value() {
let m = std::sync::Arc::new(std::sync::Mutex::new(serde_json::Map::new()));
{
let mut map = m.lock().unwrap();
map.insert("load_theme".to_string(), serde_json::Value::Bool(false));
}
let cb = _generate_change_callback(m.clone(), "load_theme".to_string());
cb("/path/theme.json");
let map = m.lock().unwrap();
assert_eq!(map.get("load_theme"), Some(&serde_json::Value::Bool(true)));
}
#[test]
fn powerline_init_resolves_default_renderer_module() {
let p = Powerline::init("shell", None, false, false);
assert_eq!(p.ext, "shell");
assert_eq!(p.renderer_module, "powerline.renderers.shell");
assert!(!p.run_once);
assert!(!p.had_logger);
assert!(p.use_daemon_threads);
assert_eq!(p.update_interval, 2);
}
#[test]
fn powerline_init_run_once_flag() {
let p = Powerline::init("shell", None, true, true);
assert!(p.run_once);
assert!(p.had_logger);
}
#[test]
fn resolve_renderer_module_undotted_uses_renderers_prefix() {
assert_eq!(
Powerline::resolve_renderer_module("shell", Some("zsh")),
"powerline.renderers.zsh"
);
}
#[test]
fn resolve_renderer_module_leading_dot_appends_to_ext() {
assert_eq!(
Powerline::resolve_renderer_module("shell", Some(".zsh")),
"powerline.renderers.shell.zsh"
);
}
#[test]
fn resolve_renderer_module_trailing_dot_strips_dot() {
assert_eq!(
Powerline::resolve_renderer_module("shell", Some("custom_renderer.")),
"custom_renderer"
);
}
#[test]
fn resolve_renderer_module_dotted_passes_through() {
assert_eq!(
Powerline::resolve_renderer_module("shell", Some("my.custom.module")),
"my.custom.module"
);
}
#[test]
fn resolve_renderer_module_empty_str_uses_default() {
assert_eq!(
Powerline::resolve_renderer_module("vim", Some("")),
"powerline.renderers.vim"
);
}
#[test]
fn setup_components_base_is_no_op() {
let p = Powerline::init("shell", None, false, false);
p.setup_components(Some(&["statusline", "tabline"]));
}
#[test]
fn get_local_themes_base_returns_none() {
assert_eq!(Powerline::get_local_themes(None), None);
assert_eq!(
Powerline::get_local_themes(Some(&serde_json::json!({"x": "y"}))),
None
);
}
#[test]
fn do_setup_base_is_no_op() {
Powerline::do_setup();
}
#[test]
fn powerline_get_config_paths_delegates_to_module_fn() {
let paths_class = Powerline::get_config_paths();
let paths_module = get_config_paths();
assert_eq!(paths_class, paths_module);
}
#[test]
fn get_fallback_logger_returns_powerline_fallback_pair() {
let pl = get_fallback_logger();
assert_eq!(pl.ext, "powerline");
assert_eq!(pl.prefix, "_fallback_");
}
#[test]
fn generate_config_finder_dispatches_to_find_config_files() {
let d = std::env::temp_dir().join(format!(
"powerliners-gcf-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&d).unwrap();
let f = d.join("test.json");
std::fs::write(&f, "{}").unwrap();
let d_clone = d.clone();
let finder = generate_config_finder(move || vec![d_clone.clone()]);
let result = finder("test").unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0], f);
std::fs::remove_dir_all(&d).ok();
}
#[test]
fn generate_config_finder_returns_err_when_missing() {
let finder = generate_config_finder(|| vec![std::path::PathBuf::from("/never/exists/x")]);
assert!(finder("nonexistent").is_err());
}
#[test]
fn load_config_loads_single_file() {
let d = std::env::temp_dir().join(format!(
"powerliners-lc-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&d).unwrap();
let f = d.join("config.json");
std::fs::write(&f, r#"{"key": "value"}"#).unwrap();
let d_clone = d.clone();
let finder = move |p: &str| _find_config_files(std::slice::from_ref(&d_clone), p);
let result = load_config("config", finder, |p| {
let raw = std::fs::read_to_string(p).map_err(|e| e.to_string())?;
let v: Value = serde_json::from_str(&raw).map_err(|e| e.to_string())?;
v.as_object()
.cloned()
.ok_or_else(|| "not an object".to_string())
})
.unwrap();
assert_eq!(result.get("key"), Some(&Value::String("value".into())));
std::fs::remove_dir_all(&d).ok();
}
#[test]
fn load_config_merges_multiple_paths() {
let d1 = std::env::temp_dir().join(format!(
"powerliners-lcm1-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let d2 = std::env::temp_dir().join(format!(
"powerliners-lcm2-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&d1).unwrap();
std::fs::create_dir_all(&d2).unwrap();
std::fs::write(d1.join("c.json"), r#"{"a": 1, "b": 2}"#).unwrap();
std::fs::write(d2.join("c.json"), r#"{"b": 3, "c": 4}"#).unwrap();
let d1c = d1.clone();
let d2c = d2.clone();
let finder = move |p: &str| _find_config_files(&[d1c.clone(), d2c.clone()], p);
let result = load_config("c", finder, |p| {
let raw = std::fs::read_to_string(p).map_err(|e| e.to_string())?;
let v: Value = serde_json::from_str(&raw).map_err(|e| e.to_string())?;
v.as_object()
.cloned()
.ok_or_else(|| "not an object".to_string())
})
.unwrap();
assert_eq!(result.get("a"), Some(&Value::from(1)));
assert_eq!(result.get("b"), Some(&Value::from(3)));
assert_eq!(result.get("c"), Some(&Value::from(4)));
std::fs::remove_dir_all(&d1).ok();
std::fs::remove_dir_all(&d2).ok();
}
#[test]
fn load_colorscheme_config_levels_returns_three_levels() {
let (levels, ignore) = Powerline::load_colorscheme_config_levels("shell", "default");
assert_eq!(
levels,
vec![
"colorschemes/default".to_string(),
"colorschemes/shell/__main__".to_string(),
"colorschemes/shell/default".to_string(),
]
);
assert_eq!(ignore, vec![1]);
}
#[test]
fn load_theme_config_levels_appends_ext_name() {
let base = vec![
"themes/__main__".to_string(),
"themes/shell/__main__".to_string(),
];
let (levels, ignore) = Powerline::load_theme_config_levels(&base, "shell", "default");
assert_eq!(levels.len(), 3);
assert_eq!(levels[2], "themes/shell/default");
assert_eq!(ignore, vec![0, 1]);
}
#[test]
fn powerline_shutdown_sets_event() {
use std::sync::atomic::{AtomicBool, Ordering};
let ev = std::sync::Arc::new(AtomicBool::new(false));
Powerline::shutdown(&ev, true);
assert!(ev.load(Ordering::SeqCst));
}
#[test]
fn powerline_shutdown_no_event_set_when_flag_false() {
use std::sync::atomic::{AtomicBool, Ordering};
let ev = std::sync::Arc::new(AtomicBool::new(false));
Powerline::shutdown(&ev, false);
assert!(!ev.load(Ordering::SeqCst));
}
#[test]
fn powerline_setup_clears_shutdown_event_and_calls_do_setup() {
use std::sync::atomic::{AtomicBool, Ordering};
let ev = std::sync::Arc::new(AtomicBool::new(true));
Powerline::setup(&ev);
assert!(!ev.load(Ordering::SeqCst));
}
#[test]
fn powerline_load_main_config_dispatches_to_main_kind() {
let r = Powerline::load_main_config(|cfg, kind| {
let mut m = Map::new();
m.insert("cfg".to_string(), Value::String(cfg.to_string()));
m.insert("kind".to_string(), Value::String(kind.to_string()));
Ok(m)
})
.unwrap();
assert_eq!(r.get("cfg"), Some(&Value::String("config".into())));
assert_eq!(r.get("kind"), Some(&Value::String("main".into())));
}
#[test]
fn powerline_load_colors_config_dispatches_to_colors_kind() {
let r = Powerline::load_colors_config(|cfg, kind| {
let mut m = Map::new();
m.insert("cfg".to_string(), Value::String(cfg.to_string()));
m.insert("kind".to_string(), Value::String(kind.to_string()));
Ok(m)
})
.unwrap();
assert_eq!(r.get("cfg"), Some(&Value::String("colors".into())));
assert_eq!(r.get("kind"), Some(&Value::String("colors".into())));
}
#[test]
fn load_hierarhical_config_merges_levels() {
let levels = vec![
"level_a".to_string(),
"level_b".to_string(),
"level_c".to_string(),
];
let r = _load_hierarhical_config(&levels, &[], |p| {
let mut m = Map::new();
m.insert(p.to_string(), Value::Bool(true));
Ok(m)
})
.unwrap();
assert!(r.contains_key("level_a"));
assert!(r.contains_key("level_b"));
assert!(r.contains_key("level_c"));
}
#[test]
fn load_hierarhical_config_errors_when_no_levels_load() {
let levels = vec!["a".to_string(), "b".to_string()];
let r: Result<Map<String, Value>, _> =
_load_hierarhical_config(&levels, &[], |_| Err("nope".to_string()));
assert!(r.is_err());
}
#[test]
fn load_hierarhical_config_ignores_specified_levels() {
let levels = vec!["a".to_string(), "b".to_string()];
let r = _load_hierarhical_config(&levels, &[], |p| {
if p == "a" {
let mut m = Map::new();
m.insert("x".to_string(), Value::Bool(true));
Ok(m)
} else {
Err("missing".to_string())
}
})
.unwrap();
assert!(r.contains_key("x"));
}
#[test]
fn load_hierarhical_config_ignore_levels_skipped_in_loaded_count() {
let levels = vec!["a".to_string(), "b".to_string()];
let r: Result<Map<String, Value>, _> = _load_hierarhical_config(&levels, &[0], |p| {
if p == "a" {
let mut m = Map::new();
m.insert("x".to_string(), Value::Bool(true));
Ok(m)
} else {
Err("missing".to_string())
}
});
assert!(r.is_err());
}
#[test]
fn powerline_load_config_instance_dispatches_through_finder_and_load_fn() {
let d = std::env::temp_dir().join(format!(
"powerliners-load-config-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&d).unwrap();
std::fs::write(d.join("test.json"), r#"{"k": 1}"#).unwrap();
let d_clone = d.clone();
let r = Powerline::load_config_instance(
"test",
move |p| _find_config_files(std::slice::from_ref(&d_clone), p),
|p| {
let raw = std::fs::read_to_string(p).map_err(|e| e.to_string())?;
let v: Value = serde_json::from_str(&raw).map_err(|e| e.to_string())?;
v.as_object()
.cloned()
.ok_or_else(|| "not an object".to_string())
},
)
.unwrap();
assert_eq!(r.get("k"), Some(&Value::from(1)));
std::fs::remove_dir_all(&d).ok();
}
#[test]
fn powerline_purge_configs_returns_function_id_pair() {
let (fn_id, finder_fn_id) = Powerline::_purge_configs(42);
assert_eq!(fn_id, 42);
assert_eq!(finder_fn_id, 42);
}
#[test]
fn powerline_load_colorscheme_config_walks_three_levels() {
use std::cell::Cell;
let visited = Cell::new(Vec::<String>::new());
let result = Powerline::load_colorscheme_config("shell", "default", |p| {
let mut v = visited.take();
v.push(p.to_string());
visited.set(v);
let mut m = Map::new();
m.insert("level".to_string(), Value::String(p.to_string()));
Ok(m)
});
assert!(result.is_ok());
let v = visited.into_inner();
assert!(v.contains(&"colorschemes/default".to_string()));
assert!(v.contains(&"colorschemes/shell/__main__".to_string()));
assert!(v.contains(&"colorschemes/shell/default".to_string()));
}
#[test]
fn powerline_load_theme_config_appends_ext_name_level() {
use std::cell::Cell;
let base = vec![
"themes/__main__".to_string(),
"themes/shell/__main__".to_string(),
];
let visited = Cell::new(Vec::<String>::new());
let r = Powerline::load_theme_config(&base, "shell", "default", |p| {
let mut v = visited.take();
v.push(p.to_string());
visited.set(v);
let mut m = Map::new();
m.insert("level".to_string(), Value::String(p.to_string()));
Ok(m)
});
assert!(r.is_ok());
let v = visited.into_inner();
assert!(v.contains(&"themes/shell/default".to_string()));
}
#[test]
fn powerline_exception_defaults_prefix_to_powerline() {
let (prefix, msg, used_fallback) = Powerline::powerline_exception("oops", None, true);
assert_eq!(prefix, "powerline");
assert_eq!(msg, "oops");
assert!(!used_fallback);
}
#[test]
fn powerline_exception_explicit_prefix_used() {
let (prefix, _, _) = Powerline::powerline_exception("oops", Some("custom"), true);
assert_eq!(prefix, "custom");
}
#[test]
fn powerline_exception_no_pl_returns_fallback_flag() {
let (_, _, used_fallback) = Powerline::powerline_exception("oops", None, false);
assert!(used_fallback);
}
#[test]
fn powerline_enter_is_no_op() {
let p = Powerline::init("shell", None, false, false);
p.enter();
}
#[test]
fn powerline_exit_sets_shutdown_event() {
use std::sync::atomic::{AtomicBool, Ordering};
let ev = std::sync::Arc::new(AtomicBool::new(false));
Powerline::exit(&ev);
assert!(ev.load(Ordering::SeqCst));
}
#[test]
fn update_renderer_no_pending_kwargs_returns_false() {
let r = Powerline::update_renderer(false, true, || panic!("should not run")).unwrap();
assert!(!r);
}
#[test]
fn update_renderer_success_returns_true() {
let r = Powerline::update_renderer(true, false, || Ok(())).unwrap();
assert!(r);
}
#[test]
fn update_renderer_failure_with_existing_renderer_returns_false() {
let r = Powerline::update_renderer(true, true, || Err("boom".to_string())).unwrap();
assert!(!r);
}
#[test]
fn update_renderer_failure_without_existing_renderer_returns_err() {
let r = Powerline::update_renderer(true, false, || Err("boom".to_string()));
assert!(r.is_err());
assert!(r.unwrap_err().contains("Failed to create renderer"));
}
#[test]
fn render_success_returns_rendered_string() {
let r = Powerline::render(|| Ok(()), || Ok("rendered".to_string()), false);
assert_eq!(r, Ok("rendered".to_string()));
}
#[test]
fn render_failure_returns_failed_message_without_width() {
let r = Powerline::render(|| Ok(()), || Err("error".to_string()), false);
let (msg, width) = r.unwrap_err();
assert!(msg.contains("Failed to render"));
assert!(msg.contains("error"));
assert!(width.is_none());
}
#[test]
fn render_failure_with_output_width_returns_message_and_len() {
let r = Powerline::render(|| Ok(()), || Err("err".to_string()), true);
let (msg, width) = r.unwrap_err();
assert_eq!(width, Some(msg.len()));
}
#[test]
fn render_update_renderer_failure_routes_to_failed() {
let r = Powerline::render(
|| Err("update fail".to_string()),
|| panic!("should not run after update fail"),
false,
);
assert!(r.is_err());
assert!(r.unwrap_err().0.contains("update fail"));
}
#[test]
fn render_above_lines_success_returns_yielded_lines() {
let lines = Powerline::render_above_lines(
|| Ok(()),
|| Ok(vec!["line1".to_string(), "line2".to_string()]),
);
assert_eq!(lines, vec!["line1".to_string(), "line2".to_string()]);
}
#[test]
fn render_above_lines_failure_yields_single_failed_line() {
let lines =
Powerline::render_above_lines(|| Err("boom".to_string()), || panic!("should not run"));
assert_eq!(lines.len(), 1);
assert!(lines[0].contains("Failed to render"));
}
#[test]
fn load_hierarhical_config_instance_delegates_to_module_fn() {
let levels = vec!["a".to_string(), "b".to_string()];
let r = Powerline::_load_hierarhical_config_instance(&levels, &[], |p| {
let mut m = Map::new();
m.insert(p.to_string(), Value::Bool(true));
Ok(m)
})
.unwrap();
assert!(r.contains_key("a"));
assert!(r.contains_key("b"));
}
#[test]
fn create_logger_instance_builds_powerline_logger_for_ext() {
let pl = Powerline::create_logger_instance("vim");
assert_eq!(pl.ext, "vim");
}
#[test]
fn reload_runs_all_three_phases_in_order() {
use std::cell::Cell;
use std::sync::atomic::{AtomicBool, Ordering};
let ev = std::sync::Arc::new(AtomicBool::new(false));
let phases = Cell::new(Vec::<&str>::new());
let r = Powerline::reload(
&ev,
|| {
let mut v = phases.take();
v.push("clear");
phases.set(v);
Ok(())
},
|| {
let mut v = phases.take();
v.push("shutdown");
phases.set(v);
},
|| {
let mut v = phases.take();
v.push("reconstruct");
phases.set(v);
Ok(())
},
);
assert!(r.is_ok());
assert!(ev.load(Ordering::SeqCst));
let v = phases.into_inner();
assert_eq!(v, vec!["clear", "shutdown", "reconstruct"]);
}
#[test]
fn reload_stops_on_clear_modules_error() {
use std::sync::atomic::{AtomicBool, Ordering};
let ev = std::sync::Arc::new(AtomicBool::new(false));
let r = Powerline::reload(
&ev,
|| Err("clear failed".to_string()),
|| panic!("shutdown should not run after clear fail"),
|| panic!("reconstruct should not run after clear fail"),
);
assert!(r.is_err());
assert!(!ev.load(Ordering::SeqCst));
}
#[test]
fn reload_propagates_reconstruct_error() {
use std::sync::atomic::{AtomicBool, Ordering};
let ev = std::sync::Arc::new(AtomicBool::new(false));
let r = Powerline::reload(
&ev,
|| Ok(()),
|| {},
|| Err("reconstruct failed".to_string()),
);
assert!(r.is_err());
assert!(ev.load(Ordering::SeqCst));
}
#[test]
fn reraise_returns_message_unchanged() {
assert_eq!(reraise("boom"), "boom");
}
#[test]
fn gen_module_attr_getter_routes_through_lookup() {
let getter = gen_module_attr_getter(|module, attr| {
if module == "powerline.matchers.vim" && attr == "help" {
Some("vim_help_matcher".to_string())
} else {
None
}
});
assert_eq!(
getter("powerline.matchers.vim", "help"),
Some("vim_help_matcher".to_string())
);
assert_eq!(getter("nonexistent", "attr"), None);
}
}