use serde_json::{Map, Value};
pub fn requires_segment_info<F>(func: F) -> F {
func
}
pub fn requires_filesystem_watcher<F>(func: F) -> F {
func
}
pub fn new_empty_segment_line() -> Map<String, Value> {
let mut m = Map::new();
m.insert("left".to_string(), Value::Array(Vec::new()));
m.insert("right".to_string(), Value::Array(Vec::new()));
m
}
pub fn add_spaces_left(_pl: &(), amount: usize, segment: &Map<String, Value>) -> String {
let contents = segment
.get("contents")
.and_then(|v| v.as_str())
.unwrap_or("");
format!("{}{}", " ".repeat(amount), contents)
}
pub fn add_spaces_right(_pl: &(), amount: usize, segment: &Map<String, Value>) -> String {
let contents = segment
.get("contents")
.and_then(|v| v.as_str())
.unwrap_or("");
format!("{}{}", contents, " ".repeat(amount))
}
pub fn add_spaces_center(_pl: &(), amount: usize, segment: &Map<String, Value>) -> String {
let (half, remainder) = (amount / 2, amount % 2);
let contents = segment
.get("contents")
.and_then(|v| v.as_str())
.unwrap_or("");
format!(
"{}{}{}",
" ".repeat(half + remainder),
contents,
" ".repeat(half)
)
}
#[allow(clippy::type_complexity)]
pub fn expand_functions(align: char) -> Option<fn(&(), usize, &Map<String, Value>) -> String> {
match align {
'l' => Some(add_spaces_right),
'r' => Some(add_spaces_left),
'c' => Some(add_spaces_center),
_ => None,
}
}
pub struct Theme {
pub colorscheme: Value,
pub dividers: Map<String, Value>,
pub cursor_space_multiplier: Option<f64>,
pub cursor_columns: Option<i64>,
pub spaces: i64,
pub outer_padding: i64,
pub segments: Vec<Map<String, Value>>,
pub empty_segment: Value,
pub shutdown_called: std::sync::Mutex<Vec<String>>,
}
impl Theme {
pub fn new() -> Self {
Self {
colorscheme: Value::Null,
dividers: Map::new(),
cursor_space_multiplier: None,
cursor_columns: None,
spaces: 0,
outer_padding: 1,
segments: Vec::new(),
empty_segment: serde_json::json!({
"contents": Value::Null,
"highlight": {"fg": false, "bg": false, "attrs": 0},
}),
shutdown_called: std::sync::Mutex::new(Vec::new()),
}
}
pub fn apply_cursor_space(&mut self, cursor_space: Option<f64>) {
self.cursor_space_multiplier = cursor_space.map(|cs| 1.0 - (cs / 100.0));
}
pub fn shutdown(&self) {
let mut log = self
.shutdown_called
.lock()
.unwrap_or_else(|e| e.into_inner());
for line in &self.segments {
for (_side, segments_value) in line {
let segments = match segments_value.as_array() {
Some(s) => s,
None => continue,
};
for segment in segments {
if let Some(name) = segment.get("name").and_then(|v| v.as_str()) {
if segment.get("shutdown").is_some_and(|v| !v.is_null()) {
log.push(name.to_string());
}
}
}
}
}
}
pub fn get_divider(&self, side: &str, divider_type: &str) -> Option<String> {
self.dividers
.get(side)
.and_then(|v| v.as_object())
.and_then(|m| m.get(divider_type))
.and_then(|v| v.as_str())
.map(String::from)
}
pub fn get_spaces(&self) -> i64 {
self.spaces
}
pub fn get_line_number(&self) -> usize {
self.segments.len()
}
pub fn get_segments(
&self,
_side: Option<&str>,
_line: usize,
_segment_info: Option<&Value>,
_mode: Option<&str>,
) -> Vec<Value> {
Vec::new()
}
}
impl Default for Theme {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn seg(contents: &str) -> Map<String, Value> {
let mut m = Map::new();
m.insert("contents".to_string(), Value::String(contents.into()));
m
}
#[test]
fn new_empty_segment_line_has_left_and_right() {
let line = new_empty_segment_line();
assert!(line.get("left").is_some());
assert!(line.get("right").is_some());
assert!(line["left"].as_array().unwrap().is_empty());
assert!(line["right"].as_array().unwrap().is_empty());
}
#[test]
fn add_spaces_left_pads_on_left() {
let s = seg("hi");
assert_eq!(add_spaces_left(&(), 3, &s), " hi");
}
#[test]
fn add_spaces_right_pads_on_right() {
let s = seg("hi");
assert_eq!(add_spaces_right(&(), 3, &s), "hi ");
}
#[test]
fn add_spaces_center_pads_evenly_extra_left_when_odd() {
let s = seg("hi");
assert_eq!(add_spaces_center(&(), 4, &s), " hi ");
assert_eq!(add_spaces_center(&(), 5, &s), " hi ");
assert_eq!(add_spaces_center(&(), 0, &s), "hi");
}
#[test]
fn expand_functions_returns_correct_fn_for_each_align() {
let s = seg("x");
let f = expand_functions('l').unwrap();
assert_eq!(f(&(), 2, &s), "x ");
let f = expand_functions('r').unwrap();
assert_eq!(f(&(), 2, &s), " x");
let f = expand_functions('c').unwrap();
assert_eq!(f(&(), 2, &s), " x ");
assert!(expand_functions('z').is_none());
}
#[test]
fn requires_decorators_are_identity() {
let f = |x: i32| x + 1;
let _g = requires_segment_info(f);
let _h = requires_filesystem_watcher(f);
assert_eq!(f(1), 2);
}
#[test]
fn alignment_idiom_matches_documented_inverse_mapping() {
let s = json!({"contents": "x"}).as_object().unwrap().clone();
let right_aligned = expand_functions('r').unwrap();
let result = right_aligned(&(), 3, &s);
assert!(result.ends_with('x'));
assert!(result.starts_with(' '));
}
#[test]
fn theme_new_initialises_empty_segment_with_python_shape() {
let t = Theme::new();
let es = &t.empty_segment;
assert_eq!(es["contents"], Value::Null);
assert_eq!(es["highlight"]["fg"], false);
assert_eq!(es["highlight"]["bg"], false);
assert_eq!(es["highlight"]["attrs"], 0);
}
#[test]
fn theme_new_defaults_outer_padding_to_1() {
let t = Theme::new();
assert_eq!(t.outer_padding, 1);
}
#[test]
fn theme_apply_cursor_space_computes_multiplier() {
let mut t = Theme::new();
t.apply_cursor_space(Some(25.0));
assert!((t.cursor_space_multiplier.unwrap() - 0.75).abs() < 1e-9);
}
#[test]
fn theme_apply_cursor_space_none_leaves_multiplier_unset() {
let mut t = Theme::new();
t.apply_cursor_space(None);
assert!(t.cursor_space_multiplier.is_none());
}
#[test]
fn theme_get_divider_resolves_nested_lookup() {
let mut t = Theme::new();
let mut left_dividers = Map::new();
left_dividers.insert("hard".to_string(), Value::String("\u{e0b0}".into()));
left_dividers.insert("soft".to_string(), Value::String("\u{e0b1}".into()));
t.dividers
.insert("left".to_string(), Value::Object(left_dividers));
assert_eq!(t.get_divider("left", "hard").as_deref(), Some("\u{e0b0}"));
assert_eq!(t.get_divider("left", "soft").as_deref(), Some("\u{e0b1}"));
}
#[test]
fn theme_get_divider_returns_none_for_missing_side_or_type() {
let mut t = Theme::new();
let mut left_dividers = Map::new();
left_dividers.insert("hard".to_string(), Value::String(">".into()));
t.dividers
.insert("left".to_string(), Value::Object(left_dividers));
assert!(t.get_divider("left", "soft").is_none());
assert!(t.get_divider("right", "hard").is_none());
}
#[test]
fn theme_get_spaces_returns_field_value() {
let mut t = Theme::new();
t.spaces = 2;
assert_eq!(t.get_spaces(), 2);
}
#[test]
fn theme_get_line_number_returns_segments_len() {
let mut t = Theme::new();
assert_eq!(t.get_line_number(), 0);
t.segments.push(new_empty_segment_line());
assert_eq!(t.get_line_number(), 1);
t.segments.push(new_empty_segment_line());
assert_eq!(t.get_line_number(), 2);
}
#[test]
fn theme_shutdown_records_segments_with_shutdown_callable() {
let mut t = Theme::new();
let mut line = new_empty_segment_line();
let segment = json!({
"name": "uptime",
"shutdown": "ptr_placeholder",
});
line.insert("left".to_string(), Value::Array(vec![segment]));
t.segments.push(line);
t.shutdown();
let log = t.shutdown_called.lock().unwrap();
assert_eq!(*log, vec!["uptime".to_string()]);
}
#[test]
fn theme_shutdown_skips_segments_with_null_shutdown() {
let mut t = Theme::new();
let mut line = new_empty_segment_line();
let segment_no_shutdown = json!({
"name": "no_shutdown",
"shutdown": Value::Null,
});
let segment_with_shutdown = json!({
"name": "has_shutdown",
"shutdown": "ptr_placeholder",
});
line.insert(
"left".to_string(),
Value::Array(vec![segment_no_shutdown, segment_with_shutdown]),
);
t.segments.push(line);
t.shutdown();
let log = t.shutdown_called.lock().unwrap();
assert_eq!(*log, vec!["has_shutdown".to_string()]);
}
#[test]
fn theme_shutdown_walks_both_sides() {
let mut t = Theme::new();
let mut line = Map::new();
line.insert(
"left".to_string(),
Value::Array(vec![json!({"name": "left_seg", "shutdown": "ptr"})]),
);
line.insert(
"right".to_string(),
Value::Array(vec![json!({"name": "right_seg", "shutdown": "ptr"})]),
);
t.segments.push(line);
t.shutdown();
let log = t.shutdown_called.lock().unwrap();
assert_eq!(log.len(), 2);
assert!(log.contains(&"left_seg".to_string()));
assert!(log.contains(&"right_seg".to_string()));
}
}