use regex::Regex;
use serde_json::{Map, Value};
use std::collections::HashMap;
use std::sync::OnceLock;
pub const NBSP: &str = "\u{a0}";
pub fn np_control_character_translations() -> &'static HashMap<char, String> {
static M: OnceLock<HashMap<char, String>> = OnceLock::new();
M.get_or_init(|| {
let mut m = HashMap::new();
for i in 0u32..0x20 {
let ch = char::from_u32(i).unwrap();
let repl_byte = (i + 0x40) as u8;
let repl = format!("^{}", repl_byte as char);
m.insert(ch, repl);
}
m
})
}
pub fn np_invalid_character_translations() -> &'static HashMap<u32, String> {
static M: OnceLock<HashMap<u32, String>> = OnceLock::new();
M.get_or_init(|| {
let mut m = HashMap::new();
for i in 0xDC80u32..0xDD00 {
m.insert(i, format!("<{:02x}>", i - 0xDC00));
}
m
})
}
pub fn np_invalid_character_re() -> &'static Regex {
static R: OnceLock<Regex> = OnceLock::new();
R.get_or_init(|| Regex::new(r"^$").unwrap())
}
pub fn np_character_translations() -> HashMap<char, String> {
let mut m = np_control_character_translations().clone();
for (cp, repl) in np_invalid_character_translations() {
if let Some(c) = char::from_u32(*cp) {
m.insert(c, repl.clone());
}
}
m
}
pub fn translate_np(s: &str) -> String {
let table = np_character_translations();
let mut out = String::with_capacity(s.len());
for c in s.chars() {
if let Some(repl) = table.get(&c) {
out.push_str(repl);
} else {
out.push(c);
}
}
out
}
#[derive(Debug, Clone)]
pub enum RenderReturn {
Plain(String),
Tuple {
highlighted: String,
raw: Option<String>,
width: Option<usize>,
},
}
pub fn construct_returned_value(
rendered_highlighted: String,
rendered_raw: Option<String>,
width: usize,
output_raw: bool,
output_width: bool,
) -> RenderReturn {
if !output_raw && !output_width {
return RenderReturn::Plain(rendered_highlighted);
}
RenderReturn::Tuple {
highlighted: rendered_highlighted,
raw: if output_raw { rendered_raw } else { None },
width: if output_width { Some(width) } else { None },
}
}
pub fn width_data(ambiwidth: u8) -> HashMap<char, u8> {
let mut m = HashMap::new();
m.insert('N', 1);
m.insert('a', 1);
m.insert('A', ambiwidth);
m.insert('H', 1);
m.insert('W', 2);
m.insert('F', 2);
m
}
pub fn strwidth(s: &str) -> usize {
s.chars().count()
}
pub fn compute_divider_widths<F>(mut get_divider: F) -> Map<String, Value>
where
F: FnMut(&str, &str) -> String,
{
let mut out = Map::new();
for side in ["left", "right"] {
let mut side_map = Map::new();
side_map.insert(
"hard".to_string(),
Value::from(strwidth(&get_divider(side, "hard"))),
);
side_map.insert(
"soft".to_string(),
Value::from(strwidth(&get_divider(side, "soft"))),
);
out.insert(side.to_string(), Value::Object(side_map));
}
out
}
pub struct Renderer {
pub theme_config: Map<String, Value>,
pub local_themes: Map<String, Value>,
pub character_translations: HashMap<char, String>,
pub width_data: HashMap<char, u8>,
pub theme: Value,
pub shutdown_called: std::sync::Mutex<Vec<String>>,
}
impl Renderer {
pub fn new(
theme_config: Map<String, Value>,
local_themes: Map<String, Value>,
ambiwidth: u8,
) -> Self {
let use_nbsp = theme_config
.get("use_non_breaking_spaces")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let mut character_translations: HashMap<char, String> = HashMap::new();
if use_nbsp {
character_translations.insert(' ', NBSP.to_string());
}
Self {
theme_config,
local_themes,
character_translations,
width_data: width_data(ambiwidth),
theme: Value::Null,
shutdown_called: std::sync::Mutex::new(Vec::new()),
}
}
pub fn get_theme(&self, _matcher_info: Option<&Value>) -> Value {
self.theme.clone()
}
pub fn shutdown(&self) {
let mut log = self
.shutdown_called
.lock()
.unwrap_or_else(|e| e.into_inner());
log.push("theme".to_string());
}
pub fn escape(&self, string: &str) -> String {
let mut out = String::with_capacity(string.len());
for c in string.chars() {
match self.character_translations.get(&c) {
Some(replacement) => out.push_str(replacement),
None => out.push(c),
}
}
out
}
pub fn hlstyle(_fg: Option<u16>, _bg: Option<u16>, _attrs: Option<u32>) -> String {
unimplemented!("Renderer.hlstyle is abstract; concrete renderers must override (py:598)")
}
pub fn hl(contents: Option<&str>, hlstyle_output: &str) -> String {
format!("{}{}", hlstyle_output, contents.unwrap_or(""))
}
pub fn _prepare_segments(segments: &mut [Value], calculate_contents_len: bool) {
for segment in segments.iter_mut() {
if let Some(obj) = segment.as_object_mut() {
if let Some(contents) = obj.get("contents").and_then(|v| v.as_str()) {
let translated = translate_np(contents);
obj.insert("contents".to_string(), Value::String(translated));
}
}
}
if calculate_contents_len {
for segment in segments.iter_mut() {
if let Some(obj) = segment.as_object_mut() {
let literal = obj
.get("literal_contents")
.and_then(|v| v.as_array())
.cloned();
let contents_len = if let Some(lit) = literal {
let has_literal = lit
.get(1)
.and_then(|v| v.as_str())
.map(|s| !s.is_empty())
.unwrap_or(false);
if has_literal {
lit.first().and_then(|v| v.as_u64()).unwrap_or(0) as usize
} else {
obj.get("contents")
.and_then(|v| v.as_str())
.map(strwidth)
.unwrap_or(0)
}
} else {
obj.get("contents")
.and_then(|v| v.as_str())
.map(strwidth)
.unwrap_or(0)
};
obj.insert(
"_contents_len".to_string(),
Value::from(contents_len as u64),
);
}
}
}
}
pub fn segment_info() -> Map<String, Value> {
let mut info = Map::new();
let env_map: Map<String, Value> = std::env::vars()
.map(|(k, v)| (k, Value::String(v)))
.collect();
info.insert("environ".to_string(), Value::Object(env_map.clone()));
info.insert(
"home".to_string(),
env_map.get("HOME").cloned().unwrap_or(Value::Null),
);
info
}
pub fn get_segment_info(
&self,
segment_info: Option<Map<String, Value>>,
mode: Option<&str>,
) -> Map<String, Value> {
let mut r = Self::segment_info();
r.insert(
"mode".to_string(),
mode.map(|s| Value::String(s.into())).unwrap_or(Value::Null),
);
if let Some(extra) = segment_info {
for (k, v) in extra {
r.insert(k, v);
}
}
let pwd = r
.get("environ")
.and_then(|v| v.as_object())
.and_then(|env| env.get("PWD"))
.and_then(|v| v.as_str())
.map(String::from);
if let Some(p) = pwd {
r.insert("getcwd".to_string(), Value::String(p));
}
r
}
#[allow(clippy::too_many_arguments)]
pub fn render_above_lines<HS, H, CF>(
&self,
mode: Option<&str>,
width: Option<usize>,
output_raw: bool,
output_width: bool,
segment_info: Option<Map<String, Value>>,
matcher_info: Option<&Value>,
hl_args: Option<Map<String, Value>>,
theme: &crate::ported::theme::Theme,
colorscheme: &crate::ported::colorscheme::Colorscheme,
contents_func: &CF,
hlstyle_fn: &HS,
hl_fn: &H,
) -> Vec<RenderReturn>
where
HS: Fn(&Value, &Value, &Value, &Map<String, Value>) -> String,
H: Fn(Option<&str>, &Value, &Value, &Value, &Map<String, Value>) -> String,
CF: Fn(&str, &(), &Map<String, Value>, &Map<String, Value>) -> Option<Value>,
{
let mut out: Vec<RenderReturn> = Vec::new();
let _ = self.get_theme(matcher_info);
let line_n = theme.get_line_number();
if line_n == 0 {
return out;
}
for line in (1..line_n).rev() {
out.push(self.render(
mode,
width,
None,
line,
output_raw,
output_width,
segment_info.clone(),
matcher_info,
hl_args.clone(),
theme,
colorscheme,
contents_func,
hlstyle_fn,
hl_fn,
));
}
out
}
#[allow(clippy::too_many_arguments)]
pub fn render<HS, H, CF>(
&self,
mode: Option<&str>,
width: Option<usize>,
side: Option<&str>,
line: usize,
output_raw: bool,
output_width: bool,
segment_info: Option<Map<String, Value>>,
matcher_info: Option<&Value>,
hl_args: Option<Map<String, Value>>,
theme: &crate::ported::theme::Theme,
colorscheme: &crate::ported::colorscheme::Colorscheme,
contents_func: &CF,
hlstyle_fn: &HS,
hl_fn: &H,
) -> RenderReturn
where
HS: Fn(&Value, &Value, &Value, &Map<String, Value>) -> String,
H: Fn(Option<&str>, &Value, &Value, &Value, &Map<String, Value>) -> String,
CF: Fn(&str, &(), &Map<String, Value>, &Map<String, Value>) -> Option<Value>,
{
let _ = self.get_theme(matcher_info);
self.do_render(
mode,
width,
side,
line,
output_raw,
output_width,
self.get_segment_info(segment_info, mode),
theme,
colorscheme,
contents_func,
hl_args,
hlstyle_fn,
hl_fn,
)
}
#[allow(clippy::too_many_arguments)]
pub fn do_render<HS, H, CF>(
&self,
mode: Option<&str>,
width: Option<usize>,
side: Option<&str>,
line: usize,
output_raw: bool,
output_width: bool,
segment_info: Map<String, Value>,
theme: &crate::ported::theme::Theme,
colorscheme: &crate::ported::colorscheme::Colorscheme,
contents_func: &CF,
hl_args: Option<Map<String, Value>>,
hlstyle_fn: &HS,
hl_fn: &H,
) -> RenderReturn
where
HS: Fn(&Value, &Value, &Value, &Map<String, Value>) -> String,
H: Fn(Option<&str>, &Value, &Value, &Value, &Map<String, Value>) -> String,
CF: Fn(&str, &(), &Map<String, Value>, &Map<String, Value>) -> Option<Value>,
{
let segment_info_value = Value::Object(segment_info.clone());
let mut segments: Vec<Value> = theme.get_segments(
side,
line,
Some(&segment_info_value),
mode,
colorscheme,
contents_func,
);
let mut current_width: usize = 0;
Self::_prepare_segments(&mut segments, output_width || width.is_some());
let hl_args: Map<String, Value> = hl_args.unwrap_or_default();
if width.is_none() {
if output_width {
let dw = compute_divider_widths(|s, k| theme.get_divider(s, k).unwrap_or_default());
current_width = self._render_length(theme, &mut segments, &dw);
}
let rendered =
self._render_segments(theme, &mut segments, &hl_args, true, hlstyle_fn, hl_fn);
let joined: String = rendered
.iter()
.filter_map(|s| s.get("_rendered_hl").and_then(|v| v.as_str()))
.collect::<Vec<_>>()
.concat();
let trailing = hlstyle_fn(&Value::Null, &Value::Null, &Value::Null, &hl_args);
let raw = if output_raw {
Some(
segments
.iter()
.filter_map(|s| s.get("_rendered_raw").and_then(|v| v.as_str()))
.collect::<Vec<_>>()
.concat(),
)
} else {
None
};
return construct_returned_value(
format!("{}{}", joined, trailing),
raw,
current_width,
output_raw,
output_width,
);
}
let width = width.expect("checked above");
let divider_widths =
compute_divider_widths(|s, k| theme.get_divider(s, k).unwrap_or_default());
let mut segments_priority: Vec<usize> = segments
.iter()
.enumerate()
.filter(|(_, s)| s.get("priority").map(|v| !v.is_null()).unwrap_or(false))
.map(|(i, _)| i)
.collect();
segments_priority.sort_by(|a, b| {
let pa = segments[*a]
.get("priority")
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
let pb = segments[*b]
.get("priority")
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
pb.partial_cmp(&pa).unwrap_or(std::cmp::Ordering::Equal)
});
let no_priority_segments: Vec<usize> = segments
.iter()
.enumerate()
.filter(|(_, s)| s.get("priority").map(|v| v.is_null()).unwrap_or(true))
.map(|(i, _)| i)
.collect();
current_width = self._render_length(theme, &mut segments, ÷r_widths);
if current_width > width {
for &idx in segments_priority.iter().chain(no_priority_segments.iter()) {
let has_truncate = segments[idx]
.get("truncate")
.map(|v| !v.is_null())
.unwrap_or(false);
if has_truncate {
}
}
let mut drop_order: Vec<Value> = segments_priority
.iter()
.map(|i| segments[*i].clone())
.collect();
if current_width > width && segments.len() > 100 {
let mut diff = current_width as i64 - width as i64;
let mut consumed = 0usize;
for victim in &drop_order {
let seg_len = victim.get("_len").and_then(|v| v.as_i64()).unwrap_or(0);
if let Some(pos) = segments.iter().position(|s| s == victim) {
segments.remove(pos);
}
consumed += 1;
diff -= seg_len;
if diff <= 0 {
break;
}
}
drop_order.drain(..consumed);
current_width = self._render_length(theme, &mut segments, ÷r_widths);
}
if current_width > width {
for victim in &drop_order {
if let Some(pos) = segments.iter().position(|s| s == victim) {
segments.remove(pos);
}
current_width = self._render_length(theme, &mut segments, ÷r_widths);
if current_width <= width {
break;
}
}
}
}
let segments_spacers: Vec<usize> = segments
.iter()
.enumerate()
.filter(|(_, s)| s.get("expand").map(|v| !v.is_null()).unwrap_or(false))
.map(|(i, _)| i)
.collect();
if !segments_spacers.is_empty() {
let remaining_total = width.saturating_sub(current_width) as i64;
let n = segments_spacers.len() as i64;
let distribute_len = remaining_total / n;
let mut distribute_len_remainder = remaining_total % n;
for idx in &segments_spacers {
let extra = if distribute_len_remainder > 0 { 1 } else { 0 };
let pad_n = (distribute_len + extra).max(0) as usize;
if let Some(obj) = segments[*idx].as_object_mut() {
let align_char = obj
.get("expand")
.and_then(|v| v.as_str())
.and_then(|s| s.chars().next())
.unwrap_or('l');
let expanded = match crate::ported::theme::expand_functions(align_char) {
Some(f) => f(&(), pad_n, obj),
None => {
let cur = obj
.get("contents")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
format!("{}{}", cur, " ".repeat(pad_n))
}
};
obj.insert("contents".to_string(), Value::String(expanded));
}
distribute_len_remainder -= 1;
}
current_width = width;
} else if output_width {
current_width = self._render_length(theme, &mut segments, ÷r_widths);
}
let rendered =
self._render_segments(theme, &mut segments, &hl_args, true, hlstyle_fn, hl_fn);
let mut rendered_highlighted: String = rendered
.iter()
.filter_map(|s| s.get("_rendered_hl").and_then(|v| v.as_str()))
.collect::<Vec<_>>()
.concat();
if !rendered_highlighted.is_empty() {
rendered_highlighted.push_str(&hlstyle_fn(
&Value::Null,
&Value::Null,
&Value::Null,
&hl_args,
));
}
let raw = if output_raw {
Some(
segments
.iter()
.filter_map(|s| s.get("_rendered_raw").and_then(|v| v.as_str()))
.collect::<Vec<_>>()
.concat(),
)
} else {
None
};
construct_returned_value(
rendered_highlighted,
raw,
current_width,
output_raw,
output_width,
)
}
pub fn _render_length(
&self,
theme: &crate::ported::theme::Theme,
segments: &mut [Value],
divider_widths: &Map<String, Value>,
) -> usize {
let _segments_len = segments.len();
let mut ret: usize = 0;
let divider_spaces = theme.get_spaces() as usize;
let mut prev_segment_bg: Value = theme
.empty_segment
.get("highlight")
.and_then(|v| v.get("bg"))
.cloned()
.unwrap_or(Value::Null);
let first_segment_idx = segments.iter().position(|s| {
let __l = s
.get("literal_contents")
.and_then(|v| v.as_array())
.and_then(|a| a.get(1))
.and_then(|v| v.as_str())
.map(|x| !x.is_empty())
.unwrap_or(false);
!__l
});
let last_segment_idx = segments.iter().rposition(|s| {
let __l = s
.get("literal_contents")
.and_then(|v| v.as_array())
.and_then(|a| a.get(1))
.and_then(|v| v.as_str())
.map(|x| !x.is_empty())
.unwrap_or(false);
!__l
});
for index in 0..segments.len() {
let side = segments[index]
.get("side")
.and_then(|v| v.as_str())
.unwrap_or("left")
.to_string();
let mut segment_len: usize = segments[index]
.get("_contents_len")
.and_then(|v| v.as_u64())
.unwrap_or(0) as usize;
let lit_empty = !segments[index]
.get("literal_contents")
.and_then(|v| v.as_array())
.and_then(|a| a.get(1))
.and_then(|v| v.as_str())
.map(|x| !x.is_empty())
.unwrap_or(false);
if lit_empty {
let compare_bg: Value = if side == "left" {
if Some(index) != last_segment_idx {
let nxt = segments[index + 1..]
.iter()
.position(|s| {
let __l = s
.get("literal_contents")
.and_then(|v| v.as_array())
.and_then(|a| a.get(1))
.and_then(|v| v.as_str())
.map(|x| !x.is_empty())
.unwrap_or(false);
!__l
})
.map(|p| p + index + 1);
match nxt {
Some(j) => segments[j]
.get("highlight")
.and_then(|v| v.get("bg"))
.cloned()
.unwrap_or(Value::Null),
None => theme
.empty_segment
.get("highlight")
.and_then(|v| v.get("bg"))
.cloned()
.unwrap_or(Value::Null),
}
} else {
theme
.empty_segment
.get("highlight")
.and_then(|v| v.get("bg"))
.cloned()
.unwrap_or(Value::Null)
}
} else {
prev_segment_bg.clone()
};
let seg_bg: Value = segments[index]
.get("highlight")
.and_then(|v| v.get("bg"))
.cloned()
.unwrap_or(Value::Null);
let divider_type = if compare_bg == seg_bg { "soft" } else { "hard" };
let is_first = Some(index) == first_segment_idx;
let is_last = Some(index) == last_segment_idx;
let outer_padding = if (side == "left" && is_first) || (side == "right" && is_last)
{
theme.outer_padding as usize
} else {
0
};
let draw_divider = segments[index]
.get(format!("draw_{}_divider", divider_type))
.and_then(|v| v.as_bool())
.unwrap_or(true);
segment_len += outer_padding;
if draw_divider {
let dw = divider_widths
.get(&side)
.and_then(|v| v.get(divider_type))
.and_then(|v| v.as_u64())
.unwrap_or(0) as usize;
segment_len += dw + divider_spaces;
}
prev_segment_bg = seg_bg;
}
if let Some(obj) = segments[index].as_object_mut() {
obj.insert("_len".to_string(), Value::from(segment_len as u64));
}
ret += segment_len;
}
ret
}
pub fn _render_segments<HS, H>(
&self,
theme: &crate::ported::theme::Theme,
segments: &mut [Value],
hl_args: &Map<String, Value>,
render_highlighted: bool,
hlstyle_fn: &HS,
hl_fn: &H,
) -> Vec<Value>
where
HS: Fn(&Value, &Value, &Value, &Map<String, Value>) -> String,
H: Fn(Option<&str>, &Value, &Value, &Value, &Map<String, Value>) -> String,
{
let _segments_len = segments.len();
let divider_spaces = theme.get_spaces() as usize;
let mut prev_segment_hl: Map<String, Value> = theme
.empty_segment
.get("highlight")
.and_then(|v| v.as_object())
.cloned()
.unwrap_or_default();
let first_segment_idx = segments.iter().position(|s| {
let __l = s
.get("literal_contents")
.and_then(|v| v.as_array())
.and_then(|a| a.get(1))
.and_then(|v| v.as_str())
.map(|x| !x.is_empty())
.unwrap_or(false);
!__l
});
let last_segment_idx = segments.iter().rposition(|s| {
let __l = s
.get("literal_contents")
.and_then(|v| v.as_array())
.and_then(|a| a.get(1))
.and_then(|v| v.as_str())
.map(|x| !x.is_empty())
.unwrap_or(false);
!__l
});
for index in 0..segments.len() {
let side = segments[index]
.get("side")
.and_then(|v| v.as_str())
.unwrap_or("left")
.to_string();
let lit_empty = !segments[index]
.get("literal_contents")
.and_then(|v| v.as_array())
.and_then(|a| a.get(1))
.and_then(|v| v.as_str())
.map(|x| !x.is_empty())
.unwrap_or(false);
if lit_empty {
let compare_hl: Map<String, Value> = if side == "left" {
if Some(index) != last_segment_idx {
let nxt = segments[index + 1..]
.iter()
.position(|s| {
let __l = s
.get("literal_contents")
.and_then(|v| v.as_array())
.and_then(|a| a.get(1))
.and_then(|v| v.as_str())
.map(|x| !x.is_empty())
.unwrap_or(false);
!__l
})
.map(|p| p + index + 1);
match nxt {
Some(j) => segments[j]
.get("highlight")
.and_then(|v| v.as_object())
.cloned()
.unwrap_or_default(),
None => theme
.empty_segment
.get("highlight")
.and_then(|v| v.as_object())
.cloned()
.unwrap_or_default(),
}
} else {
theme
.empty_segment
.get("highlight")
.and_then(|v| v.as_object())
.cloned()
.unwrap_or_default()
}
} else {
prev_segment_hl.clone()
};
let is_first = Some(index) == first_segment_idx;
let is_last = Some(index) == last_segment_idx;
let outer_padding = if (side == "left" && is_first) || (side == "right" && is_last)
{
" ".repeat(theme.outer_padding as usize)
} else {
String::new()
};
let seg_bg = segments[index]
.get("highlight")
.and_then(|v| v.get("bg"))
.cloned()
.unwrap_or(Value::Null);
let cmp_bg = compare_hl.get("bg").cloned().unwrap_or(Value::Null);
let divider_type = if seg_bg == cmp_bg { "soft" } else { "hard" };
let mut divider_highlighted = String::new();
let mut contents_raw = segments[index]
.get("contents")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let mut contents_highlighted = String::new();
let draw_divider = segments[index]
.get(format!("draw_{}_divider", divider_type))
.and_then(|v| v.as_bool())
.unwrap_or(true);
let mut segment_hl_args: Map<String, Value> = segments[index]
.get("highlight")
.and_then(|v| v.as_object())
.cloned()
.unwrap_or_default();
for (k, v) in hl_args {
segment_hl_args.insert(k.clone(), v.clone());
}
let seg_fg = segments[index]
.get("highlight")
.and_then(|v| v.get("fg"))
.cloned()
.unwrap_or(Value::Null);
let seg_attrs = segments[index]
.get("highlight")
.and_then(|v| v.get("attrs"))
.cloned()
.unwrap_or(Value::Null);
if draw_divider {
let divider_raw =
self.escape(&theme.get_divider(&side, divider_type).unwrap_or_default());
if side == "left" {
contents_raw = format!(
"{}{}{}",
outer_padding,
contents_raw,
" ".repeat(divider_spaces)
);
} else {
contents_raw = format!(
"{}{}{}",
" ".repeat(divider_spaces),
contents_raw,
outer_padding
);
}
let (divider_fg, divider_bg) = if divider_type == "soft" {
let dhg_present = segments[index]
.get("divider_highlight_group")
.map(|v| !v.is_null())
.unwrap_or(false);
let key = if dhg_present {
"divider_highlight"
} else {
"highlight"
};
let fg = segments[index]
.get(key)
.and_then(|v| v.get("fg"))
.cloned()
.unwrap_or(Value::Null);
let bg = segments[index]
.get(key)
.and_then(|v| v.get("bg"))
.cloned()
.unwrap_or(Value::Null);
(fg, bg)
} else {
let fg = seg_bg.clone();
let bg = compare_hl.get("bg").cloned().unwrap_or(Value::Null);
(fg, bg)
};
let attrs_false = Value::Bool(false);
if side == "left" {
if render_highlighted {
let escaped = self.escape(&contents_raw);
contents_highlighted = hl_fn(
Some(&escaped),
&seg_fg,
&seg_bg,
&seg_attrs,
&segment_hl_args,
);
divider_highlighted = hl_fn(
Some(÷r_raw),
÷r_fg,
÷r_bg,
&attrs_false,
hl_args,
);
}
if let Some(obj) = segments[index].as_object_mut() {
obj.insert(
"_rendered_raw".to_string(),
Value::String(format!("{}{}", contents_raw, divider_raw)),
);
obj.insert(
"_rendered_hl".to_string(),
Value::String(format!(
"{}{}",
contents_highlighted, divider_highlighted
)),
);
}
} else {
if render_highlighted {
divider_highlighted = hl_fn(
Some(÷r_raw),
÷r_fg,
÷r_bg,
&attrs_false,
hl_args,
);
let escaped = self.escape(&contents_raw);
contents_highlighted = hl_fn(
Some(&escaped),
&seg_fg,
&seg_bg,
&seg_attrs,
&segment_hl_args,
);
}
if let Some(obj) = segments[index].as_object_mut() {
obj.insert(
"_rendered_raw".to_string(),
Value::String(format!("{}{}", divider_raw, contents_raw)),
);
obj.insert(
"_rendered_hl".to_string(),
Value::String(format!(
"{}{}",
divider_highlighted, contents_highlighted
)),
);
}
}
} else {
if side == "left" {
contents_raw = format!("{}{}", outer_padding, contents_raw);
} else {
contents_raw = format!("{}{}", contents_raw, outer_padding);
}
let escaped = self.escape(&contents_raw);
contents_highlighted = hl_fn(
Some(&escaped),
&seg_fg,
&seg_bg,
&seg_attrs,
&segment_hl_args,
);
if let Some(obj) = segments[index].as_object_mut() {
obj.insert("_rendered_raw".to_string(), Value::String(contents_raw));
obj.insert(
"_rendered_hl".to_string(),
Value::String(contents_highlighted),
);
}
}
prev_segment_hl = segments[index]
.get("highlight")
.and_then(|v| v.as_object())
.cloned()
.unwrap_or_default();
} else {
let n = segments[index]
.get("literal_contents")
.and_then(|v| v.as_array())
.and_then(|a| a.first())
.and_then(|v| v.as_u64())
.unwrap_or(0) as usize;
let lit = segments[index]
.get("literal_contents")
.and_then(|v| v.as_array())
.and_then(|a| a.get(1))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if let Some(obj) = segments[index].as_object_mut() {
obj.insert("_rendered_raw".to_string(), Value::String(" ".repeat(n)));
obj.insert("_rendered_hl".to_string(), Value::String(lit));
}
}
}
let _ = hlstyle_fn; segments.to_vec()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nbsp_constant_matches_upstream() {
assert_eq!(NBSP, "\u{a0}");
}
#[test]
fn np_control_character_translations_has_32_entries() {
let m = np_control_character_translations();
assert_eq!(m.len(), 32);
}
#[test]
fn np_control_character_translations_maps_null_to_caret_at() {
let m = np_control_character_translations();
assert_eq!(m.get(&'\u{00}'), Some(&"^@".to_string()));
}
#[test]
fn np_control_character_translations_maps_a_to_caret_a() {
let m = np_control_character_translations();
assert_eq!(m.get(&'\u{01}'), Some(&"^A".to_string()));
}
#[test]
fn np_control_character_translations_maps_tab_to_caret_i() {
let m = np_control_character_translations();
assert_eq!(m.get(&'\t'), Some(&"^I".to_string()));
}
#[test]
fn np_control_character_translations_maps_newline_to_caret_j() {
let m = np_control_character_translations();
assert_eq!(m.get(&'\n'), Some(&"^J".to_string()));
}
#[test]
fn np_invalid_character_translations_has_128_entries() {
let m = np_invalid_character_translations();
assert_eq!(m.len(), 128);
}
#[test]
fn np_invalid_character_translations_first_entry_is_80() {
let m = np_invalid_character_translations();
assert_eq!(m.get(&0xDC80), Some(&"<80>".to_string()));
}
#[test]
fn np_invalid_character_translations_last_entry_is_ff() {
let m = np_invalid_character_translations();
assert_eq!(m.get(&0xDCFF), Some(&"<ff>".to_string()));
}
#[test]
fn np_character_translations_contains_both_control_and_invalid() {
let m = np_character_translations();
assert!(m.len() >= 32);
assert!(m.contains_key(&'\u{00}'));
}
#[test]
fn translate_np_replaces_control_chars() {
let r = translate_np("abc\x01def");
assert_eq!(r, "abc^Adef");
}
#[test]
fn translate_np_passes_printable_chars_through() {
let r = translate_np("hello world");
assert_eq!(r, "hello world");
}
#[test]
fn translate_np_handles_multiple_control_chars() {
let r = translate_np("\x00\x01\x02");
assert_eq!(r, "^@^A^B");
}
#[test]
fn construct_returned_value_plain_when_no_flags() {
let r = construct_returned_value("hi".to_string(), None, 5, false, false);
match r {
RenderReturn::Plain(s) => assert_eq!(s, "hi"),
_ => panic!("expected Plain"),
}
}
#[test]
fn construct_returned_value_tuple_with_width_only() {
let r = construct_returned_value("hi".to_string(), None, 5, false, true);
match r {
RenderReturn::Tuple {
highlighted,
raw,
width,
} => {
assert_eq!(highlighted, "hi");
assert!(raw.is_none());
assert_eq!(width, Some(5));
}
_ => panic!("expected Tuple"),
}
}
#[test]
fn construct_returned_value_tuple_with_raw_only() {
let r =
construct_returned_value("hi".to_string(), Some("hi-raw".to_string()), 5, true, false);
match r {
RenderReturn::Tuple {
highlighted,
raw,
width,
} => {
assert_eq!(highlighted, "hi");
assert_eq!(raw, Some("hi-raw".to_string()));
assert!(width.is_none());
}
_ => panic!("expected Tuple"),
}
}
#[test]
fn construct_returned_value_tuple_with_both() {
let r =
construct_returned_value("hi".to_string(), Some("hi-raw".to_string()), 5, true, true);
match r {
RenderReturn::Tuple {
highlighted,
raw,
width,
} => {
assert_eq!(highlighted, "hi");
assert_eq!(raw, Some("hi-raw".to_string()));
assert_eq!(width, Some(5));
}
_ => panic!("expected Tuple"),
}
}
#[test]
fn width_data_default_ambiwidth_is_1() {
let w = width_data(1);
assert_eq!(w.get(&'N'), Some(&1));
assert_eq!(w.get(&'A'), Some(&1));
assert_eq!(w.get(&'W'), Some(&2));
assert_eq!(w.get(&'F'), Some(&2));
}
#[test]
fn width_data_ambiwidth_overrides_ambiguous() {
let w = width_data(2);
assert_eq!(w.get(&'A'), Some(&2));
}
#[test]
fn strwidth_counts_chars() {
assert_eq!(strwidth("hello"), 5);
assert_eq!(strwidth(""), 0);
assert_eq!(strwidth("café"), 4);
}
#[test]
fn compute_divider_widths_emits_both_sides_and_kinds() {
let r = compute_divider_widths(|side, kind| match (side, kind) {
("left", "hard") => " ".to_string(),
("left", "soft") => " ".to_string(),
("right", "hard") => " ".to_string(),
("right", "soft") => " ".to_string(),
_ => "".to_string(),
});
assert!(r.contains_key("left"));
assert!(r.contains_key("right"));
let left = r["left"].as_object().unwrap();
assert_eq!(left["hard"], 1);
assert_eq!(left["soft"], 1);
}
#[test]
fn renderer_init_use_nbsp_default_adds_space_translation() {
let cfg = Map::new();
let r = Renderer::new(cfg, Map::new(), 1);
assert_eq!(r.character_translations.get(&' '), Some(&NBSP.to_string()));
}
#[test]
fn renderer_init_use_nbsp_false_omits_space_translation() {
let mut cfg = Map::new();
cfg.insert("use_non_breaking_spaces".to_string(), Value::Bool(false));
let r = Renderer::new(cfg, Map::new(), 1);
assert!(!r.character_translations.contains_key(&' '));
}
#[test]
fn renderer_init_uses_ambiwidth_for_width_data() {
let cfg = Map::new();
let r = Renderer::new(cfg, Map::new(), 2);
assert_eq!(r.width_data.get(&'A'), Some(&2));
}
#[test]
fn segment_info_includes_environ_and_home() {
let info = Renderer::segment_info();
assert!(info.contains_key("environ"));
assert!(info.contains_key("home"));
}
#[test]
fn get_segment_info_merges_segment_info_over_base() {
let cfg = Map::new();
let r = Renderer::new(cfg, Map::new(), 1);
let mut extra = Map::new();
extra.insert("client_id".to_string(), Value::from(42));
let info = r.get_segment_info(Some(extra), Some("normal"));
assert_eq!(info["mode"], "normal");
assert_eq!(info["client_id"], 42);
}
#[test]
fn get_segment_info_sets_mode_null_when_none() {
let cfg = Map::new();
let r = Renderer::new(cfg, Map::new(), 1);
let info = r.get_segment_info(None, None);
assert_eq!(info["mode"], Value::Null);
}
#[test]
fn get_segment_info_overrides_getcwd_when_pwd_set() {
let cfg = Map::new();
let r = Renderer::new(cfg, Map::new(), 1);
let mut extra = Map::new();
let mut env = Map::new();
env.insert("PWD".to_string(), Value::String("/my/cwd".into()));
extra.insert("environ".to_string(), Value::Object(env));
let info = r.get_segment_info(Some(extra), None);
assert_eq!(info.get("getcwd"), Some(&Value::String("/my/cwd".into())));
}
#[test]
fn get_theme_returns_self_theme() {
let mut r = Renderer::new(Map::new(), Map::new(), 1);
r.theme = serde_json::json!({"name": "default"});
let t = r.get_theme(None);
assert_eq!(t["name"], "default");
}
#[test]
fn get_theme_ignores_matcher_info() {
let mut r = Renderer::new(Map::new(), Map::new(), 1);
r.theme = serde_json::json!({"name": "default"});
let info = serde_json::json!({"foo": "bar"});
let t = r.get_theme(Some(&info));
assert_eq!(t["name"], "default");
}
#[test]
fn shutdown_records_theme() {
let r = Renderer::new(Map::new(), Map::new(), 1);
r.shutdown();
let log = r.shutdown_called.lock().unwrap();
assert_eq!(*log, vec!["theme".to_string()]);
}
#[test]
fn escape_translates_chars_via_character_translations() {
let mut r = Renderer::new(Map::new(), Map::new(), 1);
r.character_translations.clear();
r.character_translations.insert('%', "%%".to_string());
assert_eq!(r.escape("100% done"), "100%% done");
}
#[test]
fn escape_passes_untranslated_chars_through() {
let r = Renderer::new(Map::new(), Map::new(), 1);
let s = "abc";
assert_eq!(r.escape(s), "abc");
}
#[test]
fn escape_default_translates_space_to_nbsp() {
let r = Renderer::new(Map::new(), Map::new(), 1);
let result = r.escape("hi there");
assert!(result.contains('\u{a0}'));
assert!(!result.contains(' '));
}
#[test]
fn escape_use_non_breaking_spaces_false_keeps_spaces() {
let mut theme_config = Map::new();
theme_config.insert("use_non_breaking_spaces".to_string(), Value::Bool(false));
let r = Renderer::new(theme_config, Map::new(), 1);
assert_eq!(r.escape("hi there"), "hi there");
}
#[test]
fn hl_concatenates_hlstyle_output_and_contents() {
let result = Renderer::hl(Some("text"), "\x1b[1m");
assert_eq!(result, "\x1b[1mtext");
}
#[test]
fn hl_none_contents_becomes_empty_string() {
let result = Renderer::hl(None, "\x1b[1m");
assert_eq!(result, "\x1b[1m");
}
#[test]
fn _prepare_segments_translates_non_printable_contents() {
let mut segments: Vec<Value> = vec![serde_json::json!({
"contents": "hello\x01world",
})];
Renderer::_prepare_segments(&mut segments, false);
let c = segments[0]["contents"].as_str().unwrap();
assert!(!c.contains('\x01'));
}
#[test]
fn _prepare_segments_calculates_contents_len_from_strwidth_when_no_literal() {
let mut segments: Vec<Value> = vec![serde_json::json!({
"contents": "hello",
"literal_contents": [0, ""],
})];
Renderer::_prepare_segments(&mut segments, true);
assert_eq!(segments[0]["_contents_len"], 5);
}
#[test]
fn _prepare_segments_uses_literal_contents_len_when_literal_non_empty() {
let mut segments: Vec<Value> = vec![serde_json::json!({
"contents": "ignored",
"literal_contents": [42, "raw text"],
})];
Renderer::_prepare_segments(&mut segments, true);
assert_eq!(segments[0]["_contents_len"], 42);
}
#[test]
fn _prepare_segments_skips_contents_len_when_not_requested() {
let mut segments: Vec<Value> = vec![serde_json::json!({
"contents": "hello",
"literal_contents": [0, ""],
})];
Renderer::_prepare_segments(&mut segments, false);
assert!(segments[0].get("_contents_len").is_none());
}
}