use crate::ported::colorscheme::{ATTR_BOLD, ATTR_ITALIC, ATTR_UNDERLINE};
use std::collections::HashSet;
use std::sync::Mutex;
use std::sync::OnceLock;
pub fn used_styles() -> &'static Mutex<Vec<(String, String)>> {
static U: OnceLock<Mutex<Vec<(String, String)>>> = OnceLock::new();
U.get_or_init(|| Mutex::new(Vec::new()))
}
pub fn seen() -> &'static Mutex<HashSet<String>> {
static S: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
S.get_or_init(|| Mutex::new(HashSet::new()))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ColorSpec {
pub cterm: u16,
pub truecolor: Option<u32>,
}
pub struct PowerlinePromptStyle;
impl Default for PowerlinePromptStyle {
fn default() -> Self {
Self::new()
}
}
impl PowerlinePromptStyle {
pub fn new() -> Self {
Self
}
pub fn style_rules(&self) -> Vec<(String, String)> {
used_styles().lock().unwrap().clone()
}
pub fn invalidation_hash(&self) -> Vec<u64> {
vec![1]
}
}
pub struct IPythonPygmentsRenderer {
pub character_translations: std::collections::HashMap<char, String>,
}
impl Default for IPythonPygmentsRenderer {
fn default() -> Self {
Self::new()
}
}
impl IPythonPygmentsRenderer {
pub const REDUCE_INITIAL: [(); 0] = [];
pub fn new() -> Self {
let mut character_translations = std::collections::HashMap::new();
character_translations.insert(' ', " ".to_string());
Self {
character_translations,
}
}
pub fn get_segment_info<W, S>(
segment_info: &serde_json::Map<String, serde_json::Value>,
mode: &str,
wrap_ipython_info: W,
super_get_segment_info: S,
) -> serde_json::Map<String, serde_json::Value>
where
W: FnOnce(
&serde_json::Map<String, serde_json::Value>,
) -> serde_json::Map<String, serde_json::Value>,
S: FnOnce(
&serde_json::Map<String, serde_json::Value>,
&str,
) -> serde_json::Map<String, serde_json::Value>,
{
let wrapped = wrap_ipython_info(segment_info);
super_get_segment_info(&wrapped, mode)
}
pub fn hl_join<T: Clone>(segments: &[Vec<T>]) -> Vec<T> {
let mut out: Vec<T> = Vec::new();
for s in segments {
out.extend_from_slice(s);
}
out
}
pub fn attrs_to_attr_names(attrs: u32) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
if attrs & ATTR_BOLD != 0 {
out.push("bold".to_string());
}
if attrs & ATTR_ITALIC != 0 {
out.push("italic".to_string());
}
if attrs & ATTR_UNDERLINE != 0 {
out.push("underline".to_string());
}
out
}
pub fn build_token_name(fg: Option<ColorSpec>, bg: Option<ColorSpec>, attrs: u32) -> String {
let guifg = fg.and_then(|f| f.truecolor);
let guibg = bg.and_then(|b| b.truecolor);
let att = Self::attrs_to_attr_names(attrs);
let fg_hex = guifg.map(|v| format!("{:06x}", v)).unwrap_or_default();
let bg_hex = guibg.map(|v| format!("{:06x}", v)).unwrap_or_default();
let mut name = String::from("pl");
for a in &att {
name.push_str("_a");
name.push_str(a);
}
name.push_str("_f");
name.push_str(&fg_hex);
name.push_str("_b");
name.push_str(&bg_hex);
name
}
pub fn build_style_rule(fg: Option<ColorSpec>, bg: Option<ColorSpec>, attrs: u32) -> String {
let guifg = fg.and_then(|f| f.truecolor);
let guibg = bg.and_then(|b| b.truecolor);
let att = Self::attrs_to_attr_names(attrs);
let fg_hex = guifg.map(|v| format!("{:06x}", v)).unwrap_or_default();
let bg_hex = guibg.map(|v| format!("{:06x}", v)).unwrap_or_default();
let mut s = String::new();
for a in &att {
s.push(' ');
s.push_str(a);
}
if !fg_hex.is_empty() {
s.push_str(" fg:#");
s.push_str(&fg_hex);
} else {
s.push_str(" fg:");
}
if !bg_hex.is_empty() {
s.push_str(" bg:#");
s.push_str(&bg_hex);
} else {
s.push_str(" bg:");
}
s
}
pub fn hl(
&self,
escaped_contents: &str,
fg: Option<ColorSpec>,
bg: Option<ColorSpec>,
attrs: Option<u32>,
) -> Vec<(Vec<String>, String)> {
let attrs = attrs.unwrap_or(0);
let name = Self::build_token_name(fg, bg, attrs);
let mut seen_lock = seen().lock().unwrap();
if !seen_lock.contains(&name) {
let rule = Self::build_style_rule(fg, bg, attrs);
used_styles()
.lock()
.unwrap()
.push((format!("pygments.{}", name), rule));
seen_lock.insert(name.clone());
}
drop(seen_lock);
vec![(vec![name], escaped_contents.to_string())]
}
pub fn hlstyle(&self) -> Vec<()> {
Vec::new()
}
pub fn get_client_id(&self) -> usize {
self as *const _ as usize
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
macro_rules! lock_globals {
() => {{
TEST_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|e| e.into_inner())
}};
}
fn reset_globals() {
let mut u = used_styles().lock().unwrap_or_else(|e| e.into_inner());
u.clear();
let mut s = seen().lock().unwrap_or_else(|e| e.into_inner());
s.clear();
}
#[test]
fn used_styles_starts_empty_after_reset() {
let _g = lock_globals!();
reset_globals();
assert!(used_styles().lock().unwrap().is_empty());
assert!(seen().lock().unwrap().is_empty());
}
#[test]
fn ipython_pygments_renderer_init_translates_space() {
let r = IPythonPygmentsRenderer::new();
assert_eq!(r.character_translations.get(&' '), Some(&" ".to_string()));
}
#[test]
fn hl_join_flattens_segments() {
let segs = vec![vec![1, 2], vec![3], vec![4, 5, 6]];
let flat = IPythonPygmentsRenderer::hl_join(&segs);
assert_eq!(flat, vec![1, 2, 3, 4, 5, 6]);
}
#[test]
fn hl_join_empty_returns_empty() {
let segs: Vec<Vec<i32>> = Vec::new();
let flat = IPythonPygmentsRenderer::hl_join(&segs);
assert!(flat.is_empty());
}
#[test]
fn attrs_to_attr_names_zero_is_empty() {
assert!(IPythonPygmentsRenderer::attrs_to_attr_names(0).is_empty());
}
#[test]
fn attrs_to_attr_names_all_three() {
let r =
IPythonPygmentsRenderer::attrs_to_attr_names(ATTR_BOLD | ATTR_ITALIC | ATTR_UNDERLINE);
assert_eq!(r, vec!["bold", "italic", "underline"]);
}
#[test]
fn build_token_name_with_fg_bg_attrs() {
let n = IPythonPygmentsRenderer::build_token_name(
Some(ColorSpec {
cterm: 231,
truecolor: Some(0xffaabb),
}),
Some(ColorSpec {
cterm: 21,
truecolor: Some(0x0000ff),
}),
ATTR_BOLD,
);
assert_eq!(n, "pl_abold_fffaabb_b0000ff");
}
#[test]
fn build_token_name_with_no_truecolor_emits_empty_hex_segments() {
let n = IPythonPygmentsRenderer::build_token_name(None, None, 0);
assert_eq!(n, "pl_f_b");
}
#[test]
fn build_style_rule_includes_attrs_fg_bg() {
let r = IPythonPygmentsRenderer::build_style_rule(
Some(ColorSpec {
cterm: 231,
truecolor: Some(0xffffff),
}),
Some(ColorSpec {
cterm: 21,
truecolor: Some(0x0000ff),
}),
ATTR_BOLD,
);
assert_eq!(r, " bold fg:#ffffff bg:#0000ff");
}
#[test]
fn build_style_rule_no_colors_emits_bare_directives() {
let r = IPythonPygmentsRenderer::build_style_rule(None, None, 0);
assert_eq!(r, " fg: bg:");
}
#[test]
fn hl_emits_token_name_and_contents() {
let _g = lock_globals!();
reset_globals();
let r = IPythonPygmentsRenderer::new();
let out = r.hl(
"hello",
Some(ColorSpec {
cterm: 231,
truecolor: Some(0xffffff),
}),
None,
None,
);
assert_eq!(out.len(), 1);
let (names, contents) = &out[0];
assert_eq!(names.len(), 1);
assert!(names[0].starts_with("pl_f"));
assert_eq!(contents, "hello");
}
#[test]
fn hl_registers_style_rule_on_first_call() {
let _g = lock_globals!();
reset_globals();
let r = IPythonPygmentsRenderer::new();
let _ = r.hl(
"x",
Some(ColorSpec {
cterm: 21,
truecolor: Some(0x010203),
}),
None,
Some(ATTR_BOLD),
);
let used = used_styles().lock().unwrap().clone();
assert_eq!(used.len(), 1);
assert!(used[0].0.starts_with("pygments.pl_abold_f010203"));
assert!(used[0].1.contains("bold"));
assert!(used[0].1.contains("fg:#010203"));
}
#[test]
fn hl_deduplicates_repeated_token_name() {
let _g = lock_globals!();
reset_globals();
let r = IPythonPygmentsRenderer::new();
let spec = Some(ColorSpec {
cterm: 1,
truecolor: Some(0x010101),
});
let _ = r.hl("a", spec, None, None);
let _ = r.hl("b", spec, None, None);
let _ = r.hl("c", spec, None, None);
assert_eq!(used_styles().lock().unwrap().len(), 1);
}
#[test]
fn hl_emits_distinct_token_names_for_different_styles() {
let _g = lock_globals!();
reset_globals();
let r = IPythonPygmentsRenderer::new();
let _ = r.hl(
"x",
Some(ColorSpec {
cterm: 21,
truecolor: Some(0x111111),
}),
None,
None,
);
let _ = r.hl(
"y",
Some(ColorSpec {
cterm: 22,
truecolor: Some(0x222222),
}),
None,
None,
);
assert_eq!(used_styles().lock().unwrap().len(), 2);
}
#[test]
fn hlstyle_returns_empty() {
let r = IPythonPygmentsRenderer::new();
assert!(r.hlstyle().is_empty());
}
#[test]
fn get_client_id_is_stable_for_same_instance() {
let r = IPythonPygmentsRenderer::new();
let a = r.get_client_id();
let b = r.get_client_id();
assert_eq!(a, b);
}
#[test]
fn powerline_prompt_style_style_rules_returns_used_styles_snapshot() {
let _g = lock_globals!();
reset_globals();
used_styles()
.lock()
.unwrap_or_else(|e| e.into_inner())
.push(("pygments.pl_f_b".to_string(), " fg: bg:".to_string()));
let p = PowerlinePromptStyle::new();
let rules = p.style_rules();
assert_eq!(rules.len(), 1);
assert_eq!(rules[0].0, "pygments.pl_f_b");
}
#[test]
fn powerline_prompt_style_invalidation_hash_returns_non_empty() {
let p = PowerlinePromptStyle::new();
let h = p.invalidation_hash();
assert!(!h.is_empty());
}
#[test]
fn get_segment_info_wraps_then_dispatches() {
let mut info = serde_json::Map::new();
info.insert(
"execution_count".to_string(),
serde_json::Value::Number(42.into()),
);
let result = IPythonPygmentsRenderer::get_segment_info(
&info,
"in",
|m| {
let mut wrapped = m.clone();
wrapped.insert("ipython_wrapped".to_string(), serde_json::Value::Bool(true));
wrapped
},
|m, mode| {
let mut out = m.clone();
out.insert(
"mode".to_string(),
serde_json::Value::String(mode.to_string()),
);
out
},
);
assert_eq!(
result.get("ipython_wrapped"),
Some(&serde_json::Value::Bool(true))
);
assert_eq!(
result.get("mode"),
Some(&serde_json::Value::String("in".to_string()))
);
assert_eq!(
result.get("execution_count").and_then(|v| v.as_i64()),
Some(42)
);
}
}