use std::any::Any;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
use super::component::Component;
use crate::box_component::Box;
use crate::markdown::Markdown;
use crate::theme::{theme, theme_revision};
const OSC133_ZONE_START: &str = "\x1b]133;A\x07";
const OSC133_ZONE_END: &str = "\x1b]133;B\x07";
const OSC133_ZONE_FINAL: &str = "\x1b]133;C\x07";
pub struct UserMessageComponent {
text: String,
output_pad: usize,
inner: Mutex<Box>,
built_revision: AtomicU64,
}
fn build_box(text: &str, output_pad: usize) -> Box {
let bg = theme().colors.surface;
let content_box = Box::new(output_pad, 1);
content_box.set_bg_fn(Some(std::sync::Arc::new(move |s: &str| bg.bg(s))));
let md = Markdown::new(text.to_string(), 0, 0);
content_box.add_child(std::sync::Arc::new(md));
content_box
}
impl UserMessageComponent {
pub fn new(text: impl Into<String>) -> Self {
let text = text.into();
let inner = build_box(&text, 1);
Self {
text,
output_pad: 1,
inner: Mutex::new(inner),
built_revision: AtomicU64::new(theme_revision()),
}
}
pub fn with_pad(self, pad: usize) -> Self {
let inner = build_box(&self.text, pad);
Self {
text: self.text,
output_pad: pad,
inner: Mutex::new(inner),
built_revision: AtomicU64::new(theme_revision()),
}
}
}
impl Component for UserMessageComponent {
fn render(&self, width: usize) -> Vec<String> {
let revision = theme_revision();
if self.built_revision.load(Ordering::Acquire) != revision {
if let Ok(mut inner) = self.inner.lock() {
*inner = build_box(&self.text, self.output_pad);
}
self.built_revision.store(revision, Ordering::Release);
}
let mut lines = match self.inner.lock() {
Ok(inner) => inner.render(width),
Err(_) => return Vec::new(),
};
if lines.is_empty() {
return lines;
}
let first = lines.first_mut().unwrap();
first.insert_str(0, OSC133_ZONE_START);
let last = lines.last_mut().unwrap();
last.insert_str(0, OSC133_ZONE_END);
last.insert_str(OSC133_ZONE_END.len(), OSC133_ZONE_FINAL);
lines
}
fn invalidate(&self) {
if let Ok(mut inner) = self.inner.lock() {
*inner = build_box(&self.text, self.output_pad);
}
self.built_revision
.store(theme_revision(), Ordering::Release);
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ansi::strip_ansi;
#[test]
fn test_renders_non_empty() {
let c = UserMessageComponent::new("Hello world");
let lines = c.render(40);
assert!(!lines.is_empty());
let joined = strip_ansi(&lines.join("\n"));
assert!(joined.contains("Hello"));
}
#[test]
fn test_osc133_markers_present() {
let c = UserMessageComponent::new("Hi");
let lines = c.render(40);
let joined = lines.join("\n");
assert!(joined.contains(OSC133_ZONE_START), "missing zone-start A");
assert!(joined.contains(OSC133_ZONE_END), "missing zone-end B");
assert!(joined.contains(OSC133_ZONE_FINAL), "missing zone-final C");
}
#[test]
fn test_cjk_no_panic() {
let c = UserMessageComponent::new("你好世界");
let lines = c.render(6);
assert!(!lines.is_empty());
}
}