pub mod checks;
pub mod context;
pub mod imp;
pub mod inspect;
pub mod markedjson;
pub mod selfcheck;
pub mod spec;
use crate::ported::lint::checks::register_common_name;
use crate::ported::lint::markedjson::load;
use regex::Regex;
use serde_json::{Map, Value};
use std::sync::OnceLock;
pub fn open_file(path: &std::path::Path) -> std::io::Result<Vec<u8>> {
std::fs::read(path)
}
pub fn function_name_re() -> &'static Regex {
static R: OnceLock<Regex> = OnceLock::new();
R.get_or_init(|| Regex::new(r"^(\w+\.)*[a-zA-Z_]\w*$").unwrap())
}
pub fn register_common_names() {
register_common_name("player", "powerline.segments.common.players", "_player");
}
#[derive(Debug, Clone)]
pub struct LoadJsonResult {
pub hadproblem: bool,
pub config: Option<Value>,
pub error: Option<String>,
}
pub fn load_json_file(path: &std::path::Path) -> LoadJsonResult {
if !path.exists() {
return LoadJsonResult {
hadproblem: true,
config: None,
error: Some(format!("Path not found: {}", path.display())),
};
}
let (config, hadproblem) = load(path);
LoadJsonResult {
hadproblem,
config,
error: None,
}
}
pub fn updated_with_config(d: &mut Map<String, Value>) {
let path = d
.get("path")
.and_then(|v| v.as_str())
.map(std::path::PathBuf::from);
let path = match path {
Some(p) => p,
None => return,
};
let r = load_json_file(&path);
d.insert("hadproblem".to_string(), Value::Bool(r.hadproblem));
if let Some(cfg) = r.config {
d.insert("config".to_string(), cfg);
}
if let Some(err) = r.error {
d.insert("error".to_string(), Value::String(err));
}
}
pub fn dict2(d: &Map<String, Value>) -> Map<String, Value> {
let mut out = Map::new();
for (k, v) in d {
if let Some(inner) = v.as_object() {
out.insert(k.clone(), Value::Object(inner.clone()));
} else {
out.insert(k.clone(), v.clone());
}
}
out
}
pub fn strip_json_suffix(name: &str) -> String {
name.strip_suffix(".json").unwrap_or(name).to_string()
}
pub fn generate_json_config_loader(
lhadproblem: std::sync::Arc<std::sync::Mutex<bool>>,
) -> Box<dyn Fn(&std::path::Path) -> Option<Value>> {
let inner = lhadproblem.clone();
Box::new(move |config_file_path: &std::path::Path| -> Option<Value> {
load_json_config(config_file_path, inner.clone())
})
}
pub fn load_json_config(
config_file_path: &std::path::Path,
lhadproblem: std::sync::Arc<std::sync::Mutex<bool>>,
) -> Option<Value> {
let (r, hadproblem) = load(config_file_path);
if hadproblem {
let mut flag = lhadproblem.lock().unwrap_or_else(|e| e.into_inner());
*flag = true;
}
r
}
#[derive(Debug, Clone)]
pub struct ExtConfigEntry {
pub error: Option<String>,
pub path: std::path::PathBuf,
pub name: Option<String>,
pub ext: Option<String>,
pub kind: Option<String>,
}
pub fn find_all_ext_config_files(
search_paths: &[std::path::PathBuf],
subdir: &str,
) -> Vec<ExtConfigEntry> {
let mut out: Vec<ExtConfigEntry> = Vec::new();
for config_root in search_paths {
let top_config_subpath = config_root.join(subdir);
if !top_config_subpath.is_dir() {
if top_config_subpath.exists() {
out.push(ExtConfigEntry {
error: Some(format!(
"Path {} is not a directory",
top_config_subpath.display()
)),
path: top_config_subpath.clone(),
name: None,
ext: None,
kind: None,
});
}
continue;
}
let entries = match std::fs::read_dir(&top_config_subpath) {
Ok(e) => e,
Err(_) => continue,
};
for entry in entries.flatten() {
let ext_name = match entry.file_name().into_string() {
Ok(s) => s,
Err(_) => continue,
};
let ext_path = entry.path();
if !ext_path.is_dir() {
if ext_name.ends_with(".json") && ext_path.is_file() {
out.push(ExtConfigEntry {
error: None,
path: ext_path.clone(),
name: Some(strip_json_suffix(&ext_name)),
ext: None,
kind: Some(format!("top_{}", subdir)),
});
} else {
out.push(ExtConfigEntry {
error: Some(format!(
"Path {} is not a directory or configuration file",
ext_path.display()
)),
path: ext_path.clone(),
name: None,
ext: None,
kind: None,
});
}
continue;
}
let inner = match std::fs::read_dir(&ext_path) {
Ok(e) => e,
Err(_) => continue,
};
for cfg_entry in inner.flatten() {
let config_file_name = match cfg_entry.file_name().into_string() {
Ok(s) => s,
Err(_) => continue,
};
let config_file_path = cfg_entry.path();
if config_file_name.ends_with(".json") && config_file_path.is_file() {
out.push(ExtConfigEntry {
error: None,
path: config_file_path.clone(),
name: Some(strip_json_suffix(&config_file_name)),
ext: Some(ext_name.clone()),
kind: Some(subdir.to_string()),
});
} else {
out.push(ExtConfigEntry {
error: Some(format!(
"Path {} is not a configuration file",
config_file_path.display()
)),
path: config_file_path.clone(),
name: None,
ext: None,
kind: None,
});
}
}
}
}
out
}
pub fn top_theme_spec() -> spec::Spec {
spec::Spec::new()
.type_check(&[spec::SpecType::Unicode])
.func("check_top_theme")
}
pub fn ext_theme_spec() -> spec::Spec {
spec::Spec::new()
.type_check(&[spec::SpecType::Unicode])
.func("check_config_themes")
}
pub fn log_format_spec() -> spec::Spec {
spec::Spec::new().type_check(&[spec::SpecType::Unicode])
}
pub fn log_level_spec() -> spec::Spec {
spec::Spec::new()
.regex("^[A-Z]+$")
.func("check_logging_level")
}
pub fn term_color_spec() -> spec::Spec {
spec::Spec::new().unsigned().cmp(spec::Cmp::Le, 255.0)
}
pub fn true_color_spec() -> spec::Spec {
spec::Spec::new().regex("^[0-9a-fA-F]{6}$")
}
pub fn colors_spec() -> spec::Spec {
let color_value = spec::Spec::new().either(vec![
spec::Spec::new().tuple(vec![term_color_spec(), true_color_spec()]),
term_color_spec(),
]);
let colors_inner = spec::Spec::new()
.unknown_spec(spec::Spec::new().ident(), color_value)
.context_message("Error while checking colors (key {key})");
let gradient_value = spec::Spec::new().tuple(vec![
spec::Spec::new()
.len(spec::Cmp::Gt, 1)
.list(term_color_spec()),
spec::Spec::new()
.len(spec::Cmp::Gt, 1)
.list(true_color_spec())
.optional(),
]);
let gradients_inner = spec::Spec::new()
.unknown_spec(spec::Spec::new().ident(), gradient_value)
.context_message("Error while checking gradients (key {key})");
let mut s = spec::Spec::new();
s = s.update("colors", colors_inner);
s = s.update("gradients", gradients_inner);
s.context_message("Error while loading colors configuration")
}
pub fn mode_translations_value_spec() -> spec::Spec {
let colors_unknown = spec::Spec::new()
.unknown_spec(color_spec(), color_spec())
.optional();
let groups_unknown = spec::Spec::new()
.unknown_spec(
group_name_spec().func("check_translated_group_name"),
group_spec(),
)
.optional();
let mut s = spec::Spec::new();
s = s.update("colors", colors_unknown);
s = s.update("groups", groups_unknown);
s
}
pub fn top_colorscheme_spec() -> spec::Spec {
let mt_inner = spec::Spec::new()
.unknown_spec(
spec::Spec::new().type_check(&[spec::SpecType::Unicode]),
mode_translations_value_spec(),
)
.optional()
.context_message("Error while loading mode translations (key {key})")
.optional();
let mut s = spec::Spec::new();
s = s.update("name", name_spec());
s = s.update("groups", groups_spec().required());
s = s.update("mode_translations", mt_inner);
s.context_message("Error while loading top-level colorscheme")
}
pub fn vim_mode_spec() -> spec::Spec {
let modes = crate::ported::segments::vim::vim_modes();
let mut values: Vec<&str> = modes.keys().copied().collect();
values.push("nc");
values.push("tab_nc");
values.push("buf_nc");
spec::Spec::new().oneof(&values)
}
pub fn shell_mode_spec() -> spec::Spec {
spec::Spec::new().regex(r"^(?:[\w\-]+|\.safe)$")
}
pub fn divider_spec() -> spec::Spec {
spec::Spec::new().printable().len(spec::Cmp::Le, 3)
}
pub fn ext_spec() -> spec::Spec {
let colorscheme_value = spec::Spec::new()
.type_check(&[spec::SpecType::Unicode])
.func("check_config_colorschemes");
let mut s = spec::Spec::new();
s = s.update("colorscheme", colorscheme_value);
s = s.update("theme", ext_theme_spec());
s = s.update("top_theme", top_theme_spec().optional());
s
}
pub fn gen_components_spec(components: &[&str]) -> spec::Spec {
let item = spec::Spec::new()
.type_check(&[spec::SpecType::Unicode])
.oneof(components);
spec::Spec::new().list(item)
}
pub fn args_spec() -> spec::Spec {
let pl_err = spec::Spec::new()
.error("pl object must be set by powerline")
.optional();
let segment_info_err = spec::Spec::new()
.error("Segment info dictionary must be set by powerline")
.optional();
let mut s = spec::Spec::new();
s = s.update("pl", pl_err);
s = s.update("segment_info", segment_info_err);
s.unknown_spec(spec::Spec::new(), spec::Spec::new())
.optional()
}
pub fn segment_module_spec() -> spec::Spec {
spec::Spec::new()
.type_check(&[spec::SpecType::Unicode])
.func("check_segment_module")
.optional()
}
pub fn exinclude_spec() -> spec::Spec {
spec::Spec::new()
.regex(r"^(\w+\.)*[a-zA-Z_]\w*$")
.func("check_exinclude_function")
}
pub fn highlight_group_spec() -> spec::Spec {
spec::Spec::new().ident()
}
pub fn _highlight_group_spec() -> spec::Spec {
highlight_group_spec().context_message(
"Error while checking function documentation while checking theme (key {key})",
)
}
pub fn vim_colorscheme_spec() -> spec::Spec {
let mt = spec::Spec::new()
.unknown_spec(vim_mode_spec(), mode_translations_value_spec())
.optional()
.context_message("Error while loading mode translations (key {key})");
let mut s = spec::Spec::new();
s = s.update("name", name_spec());
s = s.update("groups", groups_spec());
s = s.update("mode_translations", mt);
s.context_message("Error while loading vim colorscheme")
}
pub fn shell_colorscheme_spec() -> spec::Spec {
let mt = spec::Spec::new()
.unknown_spec(shell_mode_spec(), mode_translations_value_spec())
.optional()
.context_message("Error while loading mode translations (key {key})");
let mut s = spec::Spec::new();
s = s.update("name", name_spec());
s = s.update("groups", groups_spec());
s = s.update("mode_translations", mt);
s.context_message("Error while loading shell colorscheme")
}
pub fn segment_spec_base() -> spec::Spec {
let name_inner = spec::Spec::new().regex(r"^[a-zA-Z_]\w*$").optional();
let function_inner = spec::Spec::new()
.regex(r"^(\w+\.)*[a-zA-Z_]\w*$")
.func("check_segment_function")
.optional();
let exclude_modes_inner = spec::Spec::new().list(vim_mode_spec()).optional();
let include_modes_inner = spec::Spec::new().list(vim_mode_spec()).optional();
let exclude_function_inner = exinclude_spec().optional();
let include_function_inner = exinclude_spec().optional();
let draw_hard_divider_inner = spec::Spec::new()
.type_check(&[spec::SpecType::Bool])
.optional();
let draw_soft_divider_inner = spec::Spec::new()
.type_check(&[spec::SpecType::Bool])
.optional();
let draw_inner_divider_inner = spec::Spec::new()
.type_check(&[spec::SpecType::Bool])
.optional();
let display_inner = spec::Spec::new()
.type_check(&[spec::SpecType::Bool])
.optional();
let module_inner = segment_module_spec();
let priority_inner = spec::Spec::new()
.type_check(&[spec::SpecType::Float, spec::SpecType::Null])
.optional();
let after_inner = spec::Spec::new().printable().optional();
let before_inner = spec::Spec::new().printable().optional();
let width_inner = spec::Spec::new()
.either(vec![
spec::Spec::new().unsigned(),
spec::Spec::new().oneof(&["auto"]),
])
.optional();
let align_inner = spec::Spec::new().oneof(&["l", "r"]).optional();
let args_inner = args_spec().func("check_args");
let contents_inner = spec::Spec::new().printable().optional();
let highlight_group_item = highlight_group_spec().regex(r"^(?:(?!:divider$).)+$");
let highlight_groups_inner = spec::Spec::new()
.list(highlight_group_item)
.func("check_highlight_groups")
.optional();
let divider_hg_inner = highlight_group_spec()
.func("check_highlight_group")
.regex(":divider$")
.optional();
let mut s = spec::Spec::new();
s = s.update("name", name_inner);
s = s.update("function", function_inner);
s = s.update("exclude_modes", exclude_modes_inner);
s = s.update("include_modes", include_modes_inner);
s = s.update("exclude_function", exclude_function_inner);
s = s.update("include_function", include_function_inner);
s = s.update("draw_hard_divider", draw_hard_divider_inner);
s = s.update("draw_soft_divider", draw_soft_divider_inner);
s = s.update("draw_inner_divider", draw_inner_divider_inner);
s = s.update("display", display_inner);
s = s.update("module", module_inner);
s = s.update("priority", priority_inner);
s = s.update("after", after_inner);
s = s.update("before", before_inner);
s = s.update("width", width_inner);
s = s.update("align", align_inner);
s = s.update("args", args_inner);
s = s.update("contents", contents_inner);
s = s.update("highlight_groups", highlight_groups_inner);
s = s.update("divider_highlight_group", divider_hg_inner);
s.func("check_full_segment_data")
}
pub fn subsegment_spec() -> spec::Spec {
let allowed: Vec<&str> = crate::ported::lint::checks::type_keys()
.keys()
.copied()
.filter(|k| *k != "segment_list")
.collect();
let type_inner = spec::Spec::new().oneof(&allowed).optional();
segment_spec_base().update("type", type_inner)
}
pub fn segment_spec() -> spec::Spec {
let allowed: Vec<&str> = crate::ported::lint::checks::type_keys()
.keys()
.copied()
.collect();
let type_inner = spec::Spec::new().oneof(&allowed).optional();
let segments_inner = spec::Spec::new().optional().list(subsegment_spec());
segment_spec_base()
.update("type", type_inner)
.update("segments", segments_inner)
}
pub fn segments_spec() -> spec::Spec {
spec::Spec::new().optional().list(segment_spec())
}
pub fn segdict_spec() -> spec::Spec {
let left_inner =
segments_spec().context_message("Error while loading segments from left side (key {key})");
let right_inner =
segments_spec().context_message("Error while loading segments from right side (key {key})");
let mut s = spec::Spec::new();
s = s.update("left", left_inner);
s = s.update("right", right_inner);
s.func("check_segments_left_or_right")
.context_message("Error while loading segments (key {key})")
}
pub fn divside_spec() -> spec::Spec {
let mut s = spec::Spec::new();
s = s.update("hard", divider_spec());
s = s.update("soft", divider_spec());
s
}
pub fn segment_data_value_spec() -> spec::Spec {
let mut s = spec::Spec::new();
s = s.update("after", spec::Spec::new().printable().optional());
s = s.update("before", spec::Spec::new().printable().optional());
s = s.update(
"display",
spec::Spec::new()
.type_check(&[spec::SpecType::Bool])
.optional(),
);
s = s.update("args", args_spec().func("check_args"));
s = s.update("contents", spec::Spec::new().printable().optional());
s
}
pub fn dividers_spec() -> spec::Spec {
let mut s = spec::Spec::new();
s = s.update("left", divside_spec());
s = s.update("right", divside_spec());
s
}
pub fn spaces_spec() -> spec::Spec {
spec::Spec::new().unsigned().cmp(spec::Cmp::Le, 2.0)
}
pub fn common_theme_spec() -> spec::Spec {
let cursor_space = spec::Spec::new()
.type_check(&[spec::SpecType::Float])
.cmp(spec::Cmp::Le, 100.0)
.cmp(spec::Cmp::Gt, 0.0)
.optional();
let cursor_columns = spec::Spec::new()
.type_check(&[spec::SpecType::Float])
.cmp(spec::Cmp::Gt, 0.0)
.optional();
let mut s = spec::Spec::new();
s = s.update("default_module", segment_module_spec().optional());
s = s.update("cursor_space", cursor_space);
s = s.update("cursor_columns", cursor_columns);
s.context_message("Error while loading theme")
}
pub fn top_theme_structure_spec() -> spec::Spec {
let segment_data = spec::Spec::new()
.unknown_spec(
spec::Spec::new().func("check_segment_data_key"),
segment_data_value_spec(),
)
.optional()
.context_message("Error while loading segment data (key {key})");
common_theme_spec()
.update("dividers", dividers_spec())
.update("spaces", spaces_spec())
.update(
"use_non_breaking_spaces",
spec::Spec::new()
.type_check(&[spec::SpecType::Bool])
.optional(),
)
.update("segment_data", segment_data)
}
pub fn main_theme_spec() -> spec::Spec {
let segment_data = spec::Spec::new()
.unknown_spec(
spec::Spec::new().func("check_segment_data_key"),
segment_data_value_spec(),
)
.optional()
.context_message("Error while loading segment data (key {key})");
common_theme_spec()
.update("dividers", dividers_spec().optional())
.update("spaces", spaces_spec().optional())
.update("segment_data", segment_data)
}
pub fn log_file_spec() -> spec::Spec {
let unicode_path = spec::Spec::new()
.type_check(&[spec::SpecType::Unicode])
.func("check_log_file_dir");
let tuple_entry = spec::Spec::new().tuple(vec![
spec::Spec::new()
.regex(r"^(\w+\.)*[a-zA-Z_]\w*$")
.func("check_logging_handler"),
spec::Spec::new().tuple(vec![
spec::Spec::new()
.type_check(&[spec::SpecType::List])
.optional(),
spec::Spec::new()
.type_check(&[spec::SpecType::Dict])
.optional(),
]),
log_level_spec().func("check_log_file_level").optional(),
log_format_spec().optional(),
]);
let list_item = spec::Spec::new().either(vec![
spec::Spec::new().type_check(&[spec::SpecType::Unicode, spec::SpecType::Null]),
tuple_entry,
]);
let list_variant = spec::Spec::new().list(list_item);
spec::Spec::new()
.either(vec![unicode_path, list_variant])
.optional()
}
pub fn common_spec() -> spec::Spec {
let default_top_theme = top_theme_spec().optional();
let term_truecolor = spec::Spec::new()
.type_check(&[spec::SpecType::Bool])
.optional();
let term_escape_style = spec::Spec::new()
.type_check(&[spec::SpecType::Unicode])
.oneof(&["auto", "xterm", "fbterm"])
.optional();
let paths_inner = spec::Spec::new()
.list(spec::Spec::new().func("check_path_exists"))
.optional();
let log_file = log_file_spec();
let log_level = log_level_spec().optional();
let log_format = log_format_spec().optional();
let interval = spec::Spec::new()
.either(vec![
spec::Spec::new().cmp(spec::Cmp::Gt, 0.0),
spec::Spec::new().type_check(&[spec::SpecType::Null]),
])
.optional();
let reload_config = spec::Spec::new()
.type_check(&[spec::SpecType::Bool])
.optional();
let watcher = spec::Spec::new()
.type_check(&[spec::SpecType::Unicode])
.oneof(&["auto", "inotify", "stat"])
.optional();
let mut s = spec::Spec::new();
s = s.update("default_top_theme", default_top_theme);
s = s.update("term_truecolor", term_truecolor);
s = s.update("term_escape_style", term_escape_style);
s = s.update("paths", paths_inner);
s = s.update("log_file", log_file);
s = s.update("log_level", log_level);
s = s.update("log_format", log_format);
s = s.update("interval", interval);
s = s.update("reload_config", reload_config);
s = s.update("watcher", watcher);
s.context_message("Error while loading common configuration (key {key})")
}
pub fn ext_block_spec() -> spec::Spec {
let vim_local_themes = {
let mut t = spec::Spec::new();
t = t.update("__tabline__", ext_theme_spec());
t.unknown_spec(
spec::Spec::new()
.regex(r"^(\w+\.)*[a-zA-Z_]\w*$")
.func("check_matcher_func_vim"),
ext_theme_spec(),
)
};
let vim_inner = ext_spec()
.update(
"components",
gen_components_spec(&["statusline", "tabline"]).optional(),
)
.update("local_themes", vim_local_themes)
.optional();
let ipython_local_themes = {
let mut t = spec::Spec::new();
t = t.update("in2", ext_theme_spec());
t = t.update("out", ext_theme_spec());
t = t.update("rewrite", ext_theme_spec());
t
};
let ipython_inner = ext_spec()
.update("local_themes", ipython_local_themes)
.optional();
let shell_local_themes = {
let mut t = spec::Spec::new();
t = t.update("continuation", ext_theme_spec());
t = t.update("select", ext_theme_spec());
t
};
let shell_inner = ext_spec()
.update(
"components",
gen_components_spec(&["tmux", "prompt"]).optional(),
)
.update("local_themes", shell_local_themes)
.optional();
let wm_local_themes = spec::Spec::new()
.unknown_spec(
spec::Spec::new().regex(r"^[0-9A-Za-z-]+$"),
ext_theme_spec(),
)
.optional();
let wm_inner = ext_spec()
.update("local_themes", wm_local_themes)
.update(
"update_interval",
spec::Spec::new().cmp(spec::Cmp::Gt, 0.0).optional(),
)
.optional();
let mut s = spec::Spec::new();
s = s.update("vim", vim_inner);
s = s.update("ipython", ipython_inner);
s = s.update("shell", shell_inner);
s = s.update("wm", wm_inner);
s.unknown_spec(spec::Spec::new().func("check_ext"), ext_spec())
.context_message("Error while loading extensions configuration (key {key})")
}
pub fn main_spec() -> spec::Spec {
let mut s = spec::Spec::new();
s = s.update("common", common_spec());
s = s.update("ext", ext_block_spec());
s.context_message("Error while loading main configuration")
}
pub fn theme_spec() -> spec::Spec {
let segment_data = spec::Spec::new()
.unknown_spec(
spec::Spec::new().func("check_segment_data_key"),
segment_data_value_spec(),
)
.optional()
.context_message("Error while loading segment data (key {key})");
let segments_inner =
segdict_spec().update("above", spec::Spec::new().list(segdict_spec()).optional());
common_theme_spec()
.update("dividers", dividers_spec().optional())
.update("spaces", spaces_spec().optional())
.update("segment_data", segment_data)
.update("segments", segments_inner)
}
pub fn color_spec() -> spec::Spec {
spec::Spec::new()
.type_check(&[spec::SpecType::Unicode])
.func("check_color")
}
pub fn name_spec() -> spec::Spec {
spec::Spec::new()
.type_check(&[spec::SpecType::Unicode])
.len(spec::Cmp::Gt, 0)
.optional()
}
pub fn group_name_spec() -> spec::Spec {
spec::Spec::new().ident()
}
pub fn attrs_spec() -> spec::Spec {
let item = spec::Spec::new()
.type_check(&[spec::SpecType::Unicode])
.oneof(&["bold", "italic", "underline"]);
spec::Spec::new().list(item)
}
pub fn group_spec() -> spec::Spec {
let mut inner = spec::Spec::new();
inner = inner.update("fg", color_spec());
inner = inner.update("bg", color_spec());
inner = inner.update("attrs", attrs_spec());
let alias = group_name_spec().func("check_group");
spec::Spec::new().either(vec![inner, alias])
}
pub fn groups_spec() -> spec::Spec {
spec::Spec::new()
.unknown_spec(group_name_spec(), group_spec())
.context_message("Error while loading groups (key {key})")
}
pub fn colorscheme_spec() -> spec::Spec {
let mut s = spec::Spec::new();
s = s.update("name", name_spec());
s = s.update("groups", groups_spec().required());
s.context_message("Error while loading colorscheme")
}
pub fn check(paths: Option<&[String]>, debug: bool, require_ext: Option<&str>) -> bool {
let _ = (debug, require_ext);
let mut hadproblem = false;
register_common_names();
let search_paths: Vec<std::path::PathBuf> = match paths {
Some(ps) if !ps.is_empty() => ps.iter().map(std::path::PathBuf::from).collect(),
_ => crate::ported::get_config_paths(),
};
for subdir in ["colorschemes", "themes"] {
for entry in find_all_ext_config_files(&search_paths, subdir) {
if let Some(err) = &entry.error {
eprintln!("powerline-lint: {}", err);
hadproblem = true;
continue;
}
if let Some(name) = entry.name.as_deref() {
if name != "__main__" && (name.starts_with("__") || name.ends_with("__")) {
eprintln!(
"powerline-lint: File name is not supposed to start or end with \u{201c}__\u{201d}: {}",
entry.path.display()
);
hadproblem = true;
}
}
let config_v = match crate::ported::lib::config::load_json_config(&entry.path) {
Ok(v) => v,
Err(e) => {
eprintln!(
"powerline-lint: parse error in {}: {}",
entry.path.display(),
e
);
hadproblem = true;
continue;
}
};
let config_obj = match config_v.as_object() {
Some(o) => o,
None => {
eprintln!(
"powerline-lint: {} root must be a JSON object",
entry.path.display()
);
hadproblem = true;
continue;
}
};
let is_main = entry.name.as_deref() == Some("__main__");
match subdir {
"colorschemes" if !is_main => {
let groups = config_obj.get("groups");
if groups.is_none() {
eprintln!(
"powerline-lint: colorscheme {} missing required key 'groups'",
entry.path.display()
);
hadproblem = true;
}
if let Some(groups_obj) = groups.and_then(|v| v.as_object()) {
for (gname, gv) in groups_obj {
match gv {
Value::String(_) => {}
Value::Object(gobj) => {
for required_key in ["fg", "bg"] {
if !gobj.contains_key(required_key) {
eprintln!(
"powerline-lint: colorscheme {} group {:?} missing required key {:?}",
entry.path.display(),
gname,
required_key,
);
hadproblem = true;
}
}
if let Some(a) = gobj.get("attrs") {
if !a.is_array() {
eprintln!(
"powerline-lint: colorscheme {} group {:?} 'attrs' must be a list",
entry.path.display(),
gname,
);
hadproblem = true;
}
}
}
_ => {
eprintln!(
"powerline-lint: colorscheme {} group {:?} must be an object or alias string",
entry.path.display(),
gname,
);
hadproblem = true;
}
}
}
}
if let Some(mt) = config_obj.get("mode_translations") {
if !mt.is_object() {
eprintln!(
"powerline-lint: colorscheme {} 'mode_translations' must be an object",
entry.path.display()
);
hadproblem = true;
}
}
}
"themes" if !is_main => {
let segments = config_obj.get("segments");
if segments.is_none() {
eprintln!(
"powerline-lint: theme {} missing required key 'segments'",
entry.path.display()
);
hadproblem = true;
}
if let Some(seg_obj) = segments.and_then(|v| v.as_object()) {
for side in ["left", "right"] {
if let Some(side_v) = seg_obj.get(side) {
if !side_v.is_array() {
eprintln!(
"powerline-lint: theme {} segments.{} must be an array",
entry.path.display(),
side
);
hadproblem = true;
}
}
}
if let Some(above) = seg_obj.get("above") {
if !above.is_array() {
eprintln!(
"powerline-lint: theme {} segments.above must be an array",
entry.path.display()
);
hadproblem = true;
}
}
}
for (key, expected) in [
("spaces", "integer"),
("outer_padding", "integer"),
("cursor_space", "number"),
("cursor_columns", "integer"),
] {
if let Some(v) = config_obj.get(key) {
let ok = matches!(expected, "integer") && v.is_i64()
|| matches!(expected, "number") && v.is_number();
if !ok {
eprintln!(
"powerline-lint: theme {} key {:?} must be a {}",
entry.path.display(),
key,
expected
);
hadproblem = true;
}
}
}
}
_ => {}
}
}
}
let _ = paths; hadproblem
}
#[cfg(any())]
mod _check_body_trace {
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn tmp_dir() -> std::path::PathBuf {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let mut p = std::env::temp_dir();
p.push(format!(
"powerliners-lint-{}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos(),
COUNTER.fetch_add(1, Ordering::SeqCst)
));
std::fs::create_dir_all(&p).unwrap();
p
}
#[test]
fn function_name_re_matches_simple_name() {
let r = function_name_re();
assert!(r.is_match("foo"));
assert!(r.is_match("_private"));
assert!(r.is_match("func2"));
}
#[test]
fn function_name_re_matches_dotted_path() {
let r = function_name_re();
assert!(r.is_match("powerline.segments.common.sys.uptime"));
assert!(r.is_match("mymodule.fn"));
}
#[test]
fn function_name_re_rejects_starting_digit() {
let r = function_name_re();
assert!(!r.is_match("1foo"));
assert!(!r.is_match("powerline.1foo"));
}
#[test]
fn function_name_re_rejects_special_chars() {
let r = function_name_re();
assert!(!r.is_match("foo bar"));
assert!(!r.is_match("foo-bar"));
}
#[test]
fn open_file_reads_bytes() {
let d = tmp_dir();
let p = d.join("test.json");
let mut h = std::fs::File::create(&p).unwrap();
h.write_all(b"{\"k\": 1}").unwrap();
let bytes = open_file(&p).unwrap();
assert_eq!(bytes, b"{\"k\": 1}".to_vec());
std::fs::remove_dir_all(&d).ok();
}
#[test]
fn open_file_missing_returns_error() {
let r = open_file(std::path::Path::new("/nonexistent/path/x.json"));
assert!(r.is_err());
}
#[test]
fn load_json_file_parses_valid_config() {
let d = tmp_dir();
let p = d.join("good.json");
let mut h = std::fs::File::create(&p).unwrap();
h.write_all(b"{\"key\": \"value\"}").unwrap();
let r = load_json_file(&p);
assert!(!r.hadproblem);
assert!(r.config.is_some());
assert!(r.error.is_none());
std::fs::remove_dir_all(&d).ok();
}
#[test]
fn load_json_file_missing_returns_error() {
let r = load_json_file(std::path::Path::new("/never/exists/x.json"));
assert!(r.hadproblem);
assert!(r.config.is_none());
assert!(r.error.is_some());
}
#[test]
fn load_json_file_invalid_json_sets_hadproblem() {
let d = tmp_dir();
let p = d.join("bad.json");
let mut h = std::fs::File::create(&p).unwrap();
h.write_all(b"not valid json").unwrap();
let r = load_json_file(&p);
assert!(r.hadproblem);
std::fs::remove_dir_all(&d).ok();
}
#[test]
fn updated_with_config_merges_results_into_dict() {
let d_path = tmp_dir();
let p = d_path.join("c.json");
let mut h = std::fs::File::create(&p).unwrap();
h.write_all(b"{\"a\": 1}").unwrap();
let mut d = Map::new();
d.insert(
"path".to_string(),
Value::String(p.to_string_lossy().to_string()),
);
updated_with_config(&mut d);
assert_eq!(d.get("hadproblem"), Some(&Value::Bool(false)));
assert!(d.get("config").is_some());
std::fs::remove_dir_all(&d_path).ok();
}
#[test]
fn updated_with_config_no_path_no_op() {
let mut d = Map::new();
d.insert("k".to_string(), Value::from(1));
updated_with_config(&mut d);
assert!(d.get("hadproblem").is_none());
assert!(d.get("config").is_none());
}
#[test]
fn dict2_shallow_copies_inner_dicts() {
let mut inner = Map::new();
inner.insert("inner_k".to_string(), Value::from(42));
let mut d = Map::new();
d.insert("outer".to_string(), Value::Object(inner));
let r = dict2(&d);
assert!(r.get("outer").is_some());
assert_eq!(
r["outer"].as_object().unwrap().get("inner_k"),
Some(&Value::from(42))
);
}
#[test]
fn dict2_passes_non_dict_values_through() {
let mut d = Map::new();
d.insert("scalar".to_string(), Value::from(7));
let r = dict2(&d);
assert_eq!(r["scalar"], Value::from(7));
}
#[test]
fn strip_json_suffix_removes_suffix() {
assert_eq!(strip_json_suffix("powerline.json"), "powerline");
assert_eq!(strip_json_suffix("default.json"), "default");
}
#[test]
fn strip_json_suffix_passes_through_when_absent() {
assert_eq!(strip_json_suffix("powerline"), "powerline");
assert_eq!(strip_json_suffix(""), "");
}
#[test]
fn load_json_result_struct_fields_accessible() {
let r = LoadJsonResult {
hadproblem: false,
config: Some(Value::Object(Map::new())),
error: None,
};
assert!(!r.hadproblem);
assert!(r.config.is_some());
assert!(r.error.is_none());
}
#[test]
fn register_common_names_inserts_player_alias() {
register_common_names();
let map = crate::ported::lint::checks::common_names()
.lock()
.unwrap_or_else(|e| e.into_inner());
assert!(map.contains_key("player"));
let entries = map.get("player").unwrap();
let pair = entries
.iter()
.find(|(m, n)| m == "powerline.segments.common.players" && n == "_player");
assert!(pair.is_some());
}
#[test]
fn generate_json_config_loader_loads_valid_config_and_flag_stays_false() {
let d = tmp_dir();
let p = d.join("good.json");
let mut h = std::fs::File::create(&p).unwrap();
h.write_all(b"{\"k\": 1}").unwrap();
let flag = std::sync::Arc::new(std::sync::Mutex::new(false));
let loader = generate_json_config_loader(flag.clone());
let r = loader(&p);
assert!(r.is_some());
assert!(!*flag.lock().unwrap());
std::fs::remove_dir_all(&d).ok();
}
#[test]
fn generate_json_config_loader_missing_file_does_not_set_flag() {
let flag = std::sync::Arc::new(std::sync::Mutex::new(false));
let loader = generate_json_config_loader(flag.clone());
let r = loader(std::path::Path::new("/never/exists/x.json"));
assert!(r.is_none());
}
#[test]
fn find_all_ext_config_files_yields_ext_scoped_entry() {
let root = tmp_dir();
let themes_dir = root.join("themes");
let vim_dir = themes_dir.join("vim");
std::fs::create_dir_all(&vim_dir).unwrap();
std::fs::write(vim_dir.join("default.json"), "{}").unwrap();
let entries = find_all_ext_config_files(std::slice::from_ref(&root), "themes");
let ext_entry = entries
.iter()
.find(|e| e.error.is_none() && e.ext.is_some());
assert!(ext_entry.is_some());
let e = ext_entry.unwrap();
assert_eq!(e.name.as_deref(), Some("default"));
assert_eq!(e.ext.as_deref(), Some("vim"));
assert_eq!(e.kind.as_deref(), Some("themes"));
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn find_all_ext_config_files_yields_top_level_entry() {
let root = tmp_dir();
let themes_dir = root.join("themes");
std::fs::create_dir_all(&themes_dir).unwrap();
std::fs::write(themes_dir.join("base.json"), "{}").unwrap();
let entries = find_all_ext_config_files(std::slice::from_ref(&root), "themes");
let top_entry = entries
.iter()
.find(|e| e.error.is_none() && e.kind.as_deref() == Some("top_themes"));
assert!(top_entry.is_some());
let e = top_entry.unwrap();
assert_eq!(e.name.as_deref(), Some("base"));
assert!(e.ext.is_none());
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn find_all_ext_config_files_yields_error_for_non_directory() {
let root = tmp_dir();
std::fs::write(root.join("themes"), "not-a-dir").unwrap();
let entries = find_all_ext_config_files(std::slice::from_ref(&root), "themes");
let err = entries
.iter()
.find(|e| e.error.as_deref().map(|s| s.contains("not a directory")) == Some(true));
assert!(err.is_some());
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn find_all_ext_config_files_skips_nonexistent_root_silently() {
let entries =
find_all_ext_config_files(&[std::path::PathBuf::from("/never/exists/abc")], "themes");
assert!(entries.is_empty());
}
#[test]
fn find_all_ext_config_files_yields_error_for_non_json_file_in_subdir() {
let root = tmp_dir();
let themes_dir = root.join("themes");
std::fs::create_dir_all(&themes_dir).unwrap();
std::fs::write(themes_dir.join("README.txt"), "not json").unwrap();
let entries = find_all_ext_config_files(std::slice::from_ref(&root), "themes");
let err = entries.iter().find(|e| {
e.error
.as_deref()
.map(|s| s.contains("not a directory or configuration file"))
== Some(true)
});
assert!(err.is_some());
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn colorscheme_spec_registers_required_groups_key() {
let s = colorscheme_spec();
assert!(s.get("groups").is_some(), "groups key missing");
assert!(s.get("name").is_some(), "name key missing");
}
#[test]
fn group_spec_constructs_without_panic() {
let _ = group_spec();
}
#[test]
fn groups_spec_attrs_spec_color_spec_constructible() {
let _ = groups_spec();
let _ = attrs_spec();
let _ = color_spec();
}
#[test]
fn check_against_clean_fixture_returns_false() {
let fixture = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/data/e2e/scenario_hostname");
if !fixture.is_dir() {
return;
}
let paths = vec![fixture.to_string_lossy().to_string()];
let hadproblem = check(Some(&paths), false, None);
assert!(!hadproblem, "lint flagged a clean fixture");
}
#[test]
fn check_rejects_colorscheme_missing_groups() {
let root = tmp_dir();
std::fs::create_dir_all(root.join("colorschemes/tmux")).unwrap();
std::fs::write(
root.join("colorschemes/tmux/bad.json"),
br#"{"name": "Bad"}"#,
)
.unwrap();
let paths = vec![root.to_string_lossy().to_string()];
let hadproblem = check(Some(&paths), false, None);
assert!(hadproblem, "missing 'groups' key should be flagged");
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn check_rejects_double_underscore_filenames() {
let root = tmp_dir();
std::fs::create_dir_all(root.join("themes/tmux")).unwrap();
std::fs::write(
root.join("themes/tmux/__bogus.json"),
br#"{"segments": {"left": [], "right": []}}"#,
)
.unwrap();
let paths = vec![root.to_string_lossy().to_string()];
let hadproblem = check(Some(&paths), false, None);
assert!(hadproblem, "double-underscore filename should be flagged");
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn check_rejects_theme_segments_left_not_array() {
let root = tmp_dir();
std::fs::create_dir_all(root.join("themes/tmux")).unwrap();
std::fs::write(
root.join("themes/tmux/typo.json"),
br#"{"segments": {"left": "should-be-array", "right": []}}"#,
)
.unwrap();
let paths = vec![root.to_string_lossy().to_string()];
let hadproblem = check(Some(&paths), false, None);
assert!(hadproblem, "non-array segments.left should be flagged");
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn check_rejects_parse_error() {
let root = tmp_dir();
std::fs::create_dir_all(root.join("themes/tmux")).unwrap();
std::fs::write(
root.join("themes/tmux/broken.json"),
br#"{ this is not json"#,
)
.unwrap();
let paths = vec![root.to_string_lossy().to_string()];
let hadproblem = check(Some(&paths), false, None);
assert!(hadproblem, "JSON parse failure should be flagged");
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn find_all_ext_config_files_walks_multiple_search_paths() {
let r1 = tmp_dir();
let r2 = tmp_dir();
std::fs::create_dir_all(r1.join("themes").join("vim")).unwrap();
std::fs::create_dir_all(r2.join("themes").join("shell")).unwrap();
std::fs::write(r1.join("themes").join("vim").join("a.json"), "{}").unwrap();
std::fs::write(r2.join("themes").join("shell").join("b.json"), "{}").unwrap();
let entries = find_all_ext_config_files(&[r1.clone(), r2.clone()], "themes");
let exts: std::collections::HashSet<String> =
entries.iter().filter_map(|e| e.ext.clone()).collect();
assert!(exts.contains("vim"));
assert!(exts.contains("shell"));
std::fs::remove_dir_all(&r1).ok();
std::fs::remove_dir_all(&r2).ok();
}
#[test]
fn ext_spec_carries_required_keys() {
let s = ext_spec();
assert!(s.get("colorscheme").is_some(), "colorscheme key missing");
assert!(s.get("theme").is_some(), "theme key missing");
assert!(s.get("top_theme").is_some(), "top_theme key missing");
let tt = s.get("top_theme").unwrap();
assert!(tt.isoptional, "top_theme must be optional per py:56");
}
#[test]
fn divider_spec_has_printable_and_len_constraint() {
let s = divider_spec();
assert!(s.printable_flag, "divider must be printable");
assert!(
s.len_constraints.contains(&(spec::Cmp::Le, 3)),
"divider len <= 3 missing"
);
}
#[test]
fn term_color_spec_is_unsigned_le_255() {
let s = term_color_spec();
assert!(s.unsigned_flag, "term color must be unsigned");
let (op, v) = s.cmp_constraint.unwrap();
assert_eq!(op, spec::Cmp::Le);
assert_eq!(v, 255.0);
}
#[test]
fn true_color_spec_has_hex_regex() {
let s = true_color_spec();
assert_eq!(s.regex.unwrap(), "^[0-9a-fA-F]{6}$");
}
#[test]
fn colors_spec_has_colors_and_gradients_keys() {
let s = colors_spec();
assert!(s.get("colors").is_some(), "colors key missing");
assert!(s.get("gradients").is_some(), "gradients key missing");
assert!(s.cmsg.contains("colors configuration"));
}
#[test]
fn vim_mode_spec_contains_known_modes() {
let s = vim_mode_spec();
let one = s.oneof.unwrap();
assert!(one.iter().any(|m| m == "n"));
assert!(one.iter().any(|m| m == "i"));
assert!(one.iter().any(|m| m == "nc"));
assert!(one.iter().any(|m| m == "tab_nc"));
assert!(one.iter().any(|m| m == "buf_nc"));
}
#[test]
fn shell_mode_spec_has_regex() {
let s = shell_mode_spec();
assert_eq!(s.regex.unwrap(), r"^(?:[\w\-]+|\.safe)$");
}
#[test]
fn top_colorscheme_spec_has_mt_optional() {
let s = top_colorscheme_spec();
assert!(s.get("mode_translations").unwrap().isoptional);
assert!(s.get("groups").is_some(), "groups required by spec");
assert!(s.get("name").is_some(), "name present");
}
#[test]
fn args_spec_has_pl_and_segment_info_optional() {
let s = args_spec();
assert!(s.isoptional);
assert!(s.get("pl").is_some());
assert!(s.get("segment_info").is_some());
}
#[test]
fn gen_components_spec_lists_oneof_components() {
let s = gen_components_spec(&["statusline", "tabline"]);
assert!(s.allowed_types.contains(&spec::SpecType::List));
assert_eq!(s.specs.len(), 1);
let inner = &s.specs[0];
let one = inner.oneof.as_ref().unwrap();
assert!(one.iter().any(|m| m == "statusline"));
assert!(one.iter().any(|m| m == "tabline"));
}
#[test]
fn segment_spec_base_has_all_segment_keys() {
let s = segment_spec_base();
for k in &[
"name",
"function",
"exclude_modes",
"include_modes",
"exclude_function",
"include_function",
"draw_hard_divider",
"draw_soft_divider",
"draw_inner_divider",
"display",
"module",
"priority",
"after",
"before",
"width",
"align",
"args",
"contents",
"highlight_groups",
"divider_highlight_group",
] {
assert!(s.get(k).is_some(), "missing key {k} in segment_spec_base");
}
}
#[test]
fn segment_spec_has_segments_and_type_keys() {
let s = segment_spec();
assert!(s.get("type").is_some(), "type key missing");
assert!(s.get("segments").is_some(), "segments key missing");
}
#[test]
fn subsegment_spec_excludes_segment_list_from_type() {
let s = subsegment_spec();
let allowed = s.get("type").unwrap().oneof.as_ref().unwrap();
assert!(!allowed.iter().any(|t| t == "segment_list"));
assert!(allowed.iter().any(|t| t == "string" || t == "function"));
}
#[test]
fn segdict_spec_has_left_and_right() {
let s = segdict_spec();
assert!(s.get("left").is_some(), "left missing");
assert!(s.get("right").is_some(), "right missing");
}
#[test]
fn spaces_spec_has_unsigned_le_2() {
let s = spaces_spec();
assert!(s.unsigned_flag);
assert_eq!(s.cmp_constraint.unwrap(), (spec::Cmp::Le, 2.0));
}
#[test]
fn common_theme_spec_has_cursor_keys() {
let s = common_theme_spec();
assert!(s.get("default_module").is_some());
assert!(s.get("cursor_space").is_some());
assert!(s.get("cursor_columns").is_some());
assert!(s.cmsg.contains("theme"));
}
#[test]
fn theme_spec_has_segments_key() {
let s = theme_spec();
assert!(s.get("dividers").is_some());
assert!(s.get("spaces").is_some());
assert!(s.get("segment_data").is_some());
assert!(s.get("segments").is_some());
}
#[test]
fn top_theme_structure_spec_includes_non_breaking_spaces() {
let s = top_theme_structure_spec();
assert!(s.get("use_non_breaking_spaces").is_some());
assert!(s.get("dividers").is_some());
assert!(s.get("spaces").is_some());
assert!(s.get("segment_data").is_some());
}
#[test]
fn main_spec_has_common_and_ext_blocks() {
let s = main_spec();
assert!(s.get("common").is_some(), "common block missing");
assert!(s.get("ext").is_some(), "ext block missing");
assert!(s.cmsg.contains("main configuration"));
}
#[test]
fn common_spec_has_log_and_watcher_keys() {
let s = common_spec();
for k in &[
"default_top_theme",
"term_truecolor",
"term_escape_style",
"paths",
"log_file",
"log_level",
"log_format",
"interval",
"reload_config",
"watcher",
] {
assert!(s.get(k).is_some(), "common missing {k}");
}
}
#[test]
fn ext_block_spec_has_known_extensions() {
let s = ext_block_spec();
for k in &["vim", "ipython", "shell", "wm"] {
assert!(s.get(k).is_some(), "ext missing {k}");
}
}
#[test]
fn log_file_spec_is_optional_with_either() {
let s = log_file_spec();
assert!(s.isoptional, "log_file should be optional");
assert!(!s.specs.is_empty(), "either branches missing");
}
}