use crate::ported::colorscheme::Colorscheme;
use serde_json::{Map, Value};
#[allow(clippy::too_many_arguments)]
pub fn list_segment_key_values(
segment: &Map<String, Value>,
theme_configs: &[&Map<String, Value>],
segment_data: Option<&Map<String, Value>>,
key: &str,
function_name: Option<&str>,
name: Option<&str>,
module: Option<&str>,
default: Option<Value>,
) -> Vec<Value> {
let mut out: Vec<Value> = Vec::new();
if let Some(v) = segment.get(key) {
out.push(v.clone());
}
let mut found_module_key = false;
for theme_config in theme_configs {
let seg_data = match theme_config.get("segment_data").and_then(|v| v.as_object()) {
Some(s) => s,
None => continue,
};
if let (Some(fname), None) = (function_name, name) {
if let Some(module) = module {
let mod_key = format!("{}.{}", module, fname);
if let Some(v) = seg_data
.get(&mod_key)
.and_then(|x| x.as_object())
.and_then(|o| o.get(key))
{
out.push(v.clone());
found_module_key = true;
}
}
if !found_module_key {
if let Some(v) = seg_data
.get(fname)
.and_then(|x| x.as_object())
.and_then(|o| o.get(key))
{
out.push(v.clone());
}
}
}
if let Some(n) = name {
if let Some(v) = seg_data
.get(n)
.and_then(|x| x.as_object())
.and_then(|o| o.get(key))
{
out.push(v.clone());
}
}
}
if let Some(sd) = segment_data {
if let Some(v) = sd.get(key) {
out.push(v.clone());
}
}
if let Some(d) = default {
out.push(d);
}
out
}
#[allow(clippy::too_many_arguments)]
pub fn get_segment_key(
merge: bool,
segment: &Map<String, Value>,
theme_configs: &[&Map<String, Value>],
segment_data: Option<&Map<String, Value>>,
key: &str,
function_name: Option<&str>,
name: Option<&str>,
module: Option<&str>,
default: Option<Value>,
) -> Option<Value> {
let candidates = list_segment_key_values(
segment,
theme_configs,
segment_data,
key,
function_name,
name,
module,
default,
);
if merge {
let mut ret: Option<Value> = None;
for value in candidates {
if ret.is_none() {
ret = Some(value);
} else if matches!(ret, Some(Value::Object(_))) && matches!(value, Value::Object(_)) {
let old_ret = ret.take().unwrap();
let mut new_ret = value.as_object().unwrap().clone();
for (k, v) in old_ret.as_object().unwrap() {
new_ret.insert(k.clone(), v.clone());
}
ret = Some(Value::Object(new_ret));
} else {
return ret;
}
}
ret
} else {
candidates.into_iter().next()
}
}
#[allow(clippy::type_complexity)]
pub fn get_string(
data: &Map<String, Value>,
segment: &Map<String, Value>,
) -> (
Option<Value>,
Option<Value>,
Option<String>,
Option<String>,
Option<String>,
) {
let name = segment
.get("name")
.and_then(|v| v.as_str())
.map(String::from);
let contents = segment.get("contents").cloned();
let _ = data;
(contents, None, None, None, name)
}
pub fn set_segment_highlighting(
_pl: &(),
colorscheme: &Colorscheme,
segment: &mut Map<String, Value>,
mode: Option<&str>,
) -> bool {
if let Some(Value::Array(lc)) = segment.get("literal_contents") {
if lc.len() == 2 && !lc[1].as_str().unwrap_or("").is_empty() {
return true;
}
}
let highlight_group_prefix = segment
.get("highlight_group_prefix")
.and_then(|v| v.as_str())
.map(String::from);
let hl_groups = |hlgs: Vec<String>| -> Vec<String> {
match &highlight_group_prefix {
None => hlgs,
Some(prefix) => {
let mut out: Vec<String> =
hlgs.iter().map(|h| format!("{}:{}", prefix, h)).collect();
out.extend(hlgs);
out
}
}
};
let hlgs_raw: Vec<String> = segment
.get("highlight_groups")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|s| s.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let gradient_level = segment.get("gradient_level").and_then(|v| v.as_f64());
match colorscheme.get_highlighting(&hl_groups(hlgs_raw), mode, gradient_level) {
Ok(hl) => {
segment.insert("highlight".to_string(), Value::Object(hl));
}
Err(_) => {
return false;
}
}
if let Some(dhg) = segment
.get("divider_highlight_group")
.and_then(|v| v.as_str())
.map(String::from)
{
if dhg.is_empty() {
segment.insert("divider_highlight".to_string(), Value::Null);
} else {
match colorscheme.get_highlighting(&hl_groups(vec![dhg]), mode, None) {
Ok(hl) => {
segment.insert("divider_highlight".to_string(), Value::Object(hl));
}
Err(_) => {
return false;
}
}
}
} else {
segment.insert("divider_highlight".to_string(), Value::Null);
}
true
}
pub fn always_true(
_pl: &(),
_segment_info: Option<&Map<String, Value>>,
_mode: Option<&str>,
) -> bool {
true
}
pub fn get_fallback_segment() -> Map<String, Value> {
let mut m = Map::new();
m.insert("name".into(), Value::String("fallback".into())); m.insert("type".into(), Value::String("string".into())); m.insert(
"highlight_groups".into(),
Value::Array(vec![Value::String("background".into())]), );
m.insert("divider_highlight_group".into(), Value::Null); m.insert("before".into(), Value::Null); m.insert("after".into(), Value::Null); m.insert("contents".into(), Value::String("".into())); m.insert(
"literal_contents".into(),
Value::Array(vec![Value::from(0), Value::String("".into())]), );
m.insert("priority".into(), Value::Null); m.insert("draw_soft_divider".into(), Value::Bool(true)); m.insert("draw_hard_divider".into(), Value::Bool(true)); m.insert("draw_inner_divider".into(), Value::Bool(true)); m.insert("width".into(), Value::Null); m.insert("align".into(), Value::Null); m.insert("expand".into(), Value::Null); m.insert("truncate".into(), Value::Null); m.insert("startup".into(), Value::Null); m.insert("shutdown".into(), Value::Null); m.insert("_rendered_raw".into(), Value::String("".into())); m.insert("_rendered_hl".into(), Value::String("".into())); m.insert("_len".into(), Value::Null); m.insert("_contents_len".into(), Value::Null); m
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SegmentGetterResult {
pub contents: Option<String>,
pub function_name: Option<String>,
pub module: Option<String>,
pub function_name_dup: Option<String>,
pub name: Option<String>,
}
pub fn get_function(
segment: &Map<String, Value>,
default_module: &str,
import_module_attr: impl FnOnce(&str, &str) -> Option<()>,
) -> Result<SegmentGetterResult, String> {
let raw_name = segment
.get("function")
.and_then(|v| v.as_str())
.ok_or_else(|| "segment has no 'function' key".to_string())?;
let (module, function_name) = match raw_name.rfind('.') {
Some(idx) => (raw_name[..idx].to_string(), raw_name[idx + 1..].to_string()),
None => (default_module.to_string(), raw_name.to_string()),
};
let imported = import_module_attr(&module, &function_name);
if imported.is_none() {
return Err("Failed to obtain segment function".to_string());
}
let name = segment
.get("name")
.and_then(|v| v.as_str())
.map(String::from);
Ok(SegmentGetterResult {
contents: None,
function_name: Some(function_name.clone()),
module: Some(module),
function_name_dup: Some(function_name),
name,
})
}
pub fn segment_getter_name(segment_type: &str) -> Option<&'static str> {
match segment_type {
"function" => Some("get_function"),
"string" => Some("get_string"),
"segment_list" => Some("get_function"),
_ => None,
}
}
pub type SpaceExpandFn = Box<dyn Fn(&(), usize, &Map<String, Value>) -> String>;
pub type StartupFn = Box<dyn Fn(&(), &std::sync::atomic::AtomicBool)>;
pub enum AttrFunc {
Space(SpaceExpandFn),
Plain(StartupFn),
None,
}
impl AttrFunc {
pub fn is_none(&self) -> bool {
matches!(self, AttrFunc::None)
}
}
pub fn get_attr_func<F>(func_lookup: F, is_space_func: bool) -> AttrFunc
where
F: FnOnce() -> Option<()>,
{
if func_lookup().is_none() {
return AttrFunc::None;
}
if is_space_func {
AttrFunc::Space(Box::new(
|_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))
},
))
} else {
AttrFunc::Plain(Box::new(
|_pl: &(), _shutdown_event: &std::sync::atomic::AtomicBool| {
},
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn always_true_is_always_true() {
assert!(always_true(&(), None, None));
assert!(always_true(&(), None, Some("normal")));
}
#[test]
fn get_fallback_segment_has_expected_shape() {
let s = get_fallback_segment();
assert_eq!(s.get("name").and_then(|v| v.as_str()), Some("fallback"));
assert_eq!(s.get("type").and_then(|v| v.as_str()), Some("string"));
assert_eq!(
s.get("highlight_groups")
.and_then(|v| v.as_array())
.map(|a| a.len()),
Some(1)
);
assert_eq!(s.get("contents").and_then(|v| v.as_str()), Some(""));
let lc = s
.get("literal_contents")
.and_then(|v| v.as_array())
.unwrap();
assert_eq!(lc[0].as_u64(), Some(0));
assert_eq!(lc[1].as_str(), Some(""));
}
#[test]
fn list_segment_key_values_finds_segment_key_first() {
let mut seg = Map::new();
seg.insert("contents".into(), json!("hello"));
let theme_configs: &[&Map<String, Value>] = &[];
let vals = list_segment_key_values(
&seg,
theme_configs,
None,
"contents",
None,
None,
None,
Some(json!("DEFAULT")),
);
assert_eq!(vals[0], json!("hello"));
assert_eq!(vals[vals.len() - 1], json!("DEFAULT"));
}
#[test]
fn get_segment_key_merge_collapses_dicts_old_wins() {
let mut seg = Map::new();
seg.insert("args".into(), json!({"a": 1, "b": 2}));
let theme_config = json!({
"segment_data": {
"func_name": {"args": {"b": 99, "c": 3}}
}
})
.as_object()
.unwrap()
.clone();
let theme_configs: Vec<&Map<String, Value>> = vec![&theme_config];
let merged = get_segment_key(
true,
&seg,
&theme_configs,
None,
"args",
Some("func_name"),
None,
None,
Some(json!({})),
);
let merged = merged.unwrap();
let merged_obj = merged.as_object().unwrap();
assert_eq!(merged_obj.get("a"), Some(&json!(1)));
assert_eq!(merged_obj.get("b"), Some(&json!(2)));
assert_eq!(merged_obj.get("c"), Some(&json!(3)));
}
#[test]
fn get_segment_key_no_merge_returns_first() {
let mut seg = Map::new();
seg.insert("priority".into(), json!(10));
let theme_configs: &[&Map<String, Value>] = &[];
let v = get_segment_key(
false,
&seg,
theme_configs,
None,
"priority",
None,
None,
None,
Some(json!(0)),
);
assert_eq!(v, Some(json!(10)));
}
#[test]
fn set_segment_highlighting_basic() {
use crate::ported::colorscheme::Colorscheme;
let colorscheme_config = json!({
"groups": {"info": {"fg": "white", "bg": "blue", "attrs": []}}
})
.as_object()
.unwrap()
.clone();
let colors_config = json!({
"colors": {"white": [231, "ffffff"], "blue": [21, "0000ff"]},
"gradients": {}
})
.as_object()
.unwrap()
.clone();
let cs = Colorscheme::new(&colorscheme_config, &colors_config);
let mut segment = Map::new();
segment.insert("highlight_groups".into(), json!(["info"]));
segment.insert("literal_contents".into(), json!([0, ""]));
assert!(set_segment_highlighting(&(), &cs, &mut segment, None));
assert!(segment.contains_key("highlight"));
let hl = segment
.get("highlight")
.and_then(|v| v.as_object())
.unwrap();
assert!(hl.contains_key("fg"));
assert!(hl.contains_key("bg"));
assert!(hl.contains_key("attrs"));
}
#[test]
fn get_function_dotted_name_splits_via_rpartition() {
let mut seg = Map::new();
seg.insert(
"function".to_string(),
json!("powerline.segments.shell.uptime"),
);
seg.insert("name".to_string(), json!("custom"));
let r = get_function(&seg, "powerline.segments", |_, _| Some(())).unwrap();
assert_eq!(r.module.as_deref(), Some("powerline.segments.shell"));
assert_eq!(r.function_name.as_deref(), Some("uptime"));
assert_eq!(r.function_name_dup.as_deref(), Some("uptime"));
assert!(r.contents.is_none());
assert_eq!(r.name.as_deref(), Some("custom"));
}
#[test]
fn get_function_undotted_uses_default_module() {
let mut seg = Map::new();
seg.insert("function".to_string(), json!("uptime"));
let r = get_function(&seg, "powerline.segments.shell", |_, _| Some(())).unwrap();
assert_eq!(r.module.as_deref(), Some("powerline.segments.shell"));
assert_eq!(r.function_name.as_deref(), Some("uptime"));
}
#[test]
fn get_function_missing_function_key_returns_err() {
let seg = Map::new();
let r = get_function(&seg, "powerline.segments.shell", |_, _| Some(()));
assert!(r.is_err());
}
#[test]
fn get_function_failed_import_returns_err() {
let mut seg = Map::new();
seg.insert("function".to_string(), json!("missing_fn"));
let r = get_function(&seg, "powerline.segments.shell", |_, _| None);
let err = r.unwrap_err();
assert!(err.contains("Failed to obtain segment function"));
}
#[test]
fn get_function_passes_resolved_args_to_importer() {
let mut seg = Map::new();
seg.insert("function".to_string(), json!("my.mod.fn_name"));
use std::cell::Cell;
let captured_module: Cell<String> = Cell::new(String::new());
let captured_fn: Cell<String> = Cell::new(String::new());
let _ = get_function(&seg, "fallback", |m, n| {
captured_module.set(m.to_string());
captured_fn.set(n.to_string());
Some(())
});
assert_eq!(captured_module.into_inner(), "my.mod");
assert_eq!(captured_fn.into_inner(), "fn_name");
}
#[test]
fn segment_getter_name_dispatches_by_type() {
assert_eq!(segment_getter_name("function"), Some("get_function"));
assert_eq!(segment_getter_name("segment_list"), Some("get_function"));
assert_eq!(segment_getter_name("string"), Some("get_string"));
assert_eq!(segment_getter_name("bogus"), None);
}
#[test]
fn get_attr_func_no_attribute_returns_none() {
let r = get_attr_func(|| None, false);
assert!(r.is_none());
}
#[test]
fn get_attr_func_is_space_func_returns_expand_closure() {
let r = get_attr_func(|| Some(()), true);
match r {
AttrFunc::Space(f) => {
let mut seg = Map::new();
seg.insert("contents".to_string(), json!("hi"));
let out = f(&(), 3, &seg);
assert_eq!(out, "hi ");
}
_ => panic!("expected Space variant"),
}
}
#[test]
fn get_attr_func_not_space_func_returns_plain_closure() {
let r = get_attr_func(|| Some(()), false);
match r {
AttrFunc::Plain(_) => {} _ => panic!("expected Plain variant"),
}
}
#[test]
fn attr_func_is_none_helper() {
assert!(AttrFunc::None.is_none());
assert!(!AttrFunc::Space(Box::new(|_, _, _| String::new())).is_none());
}
}