1use std::any::Any;
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::sync::Mutex;
15
16use super::component::Component;
17use crate::box_component::Box;
18use crate::markdown::Markdown;
19use crate::theme::{theme, theme_revision};
20
21const OSC133_ZONE_START: &str = "\x1b]133;A\x07";
22const OSC133_ZONE_END: &str = "\x1b]133;B\x07";
23const OSC133_ZONE_FINAL: &str = "\x1b]133;C\x07";
24
25pub struct UserMessageComponent {
27 text: String,
28 output_pad: usize,
29 inner: Mutex<Box>,
37 built_revision: AtomicU64,
39}
40
41fn build_box(text: &str, output_pad: usize) -> Box {
42 let bg = theme().colors.surface;
43 let content_box = Box::new(output_pad, 1);
44 content_box.set_bg_fn(Some(std::sync::Arc::new(move |s: &str| bg.bg(s))));
45 let md = Markdown::new(text.to_string(), 0, 0);
46 content_box.add_child(std::sync::Arc::new(md));
47 content_box
48}
49
50impl UserMessageComponent {
51 pub fn new(text: impl Into<String>) -> Self {
56 let text = text.into();
57 let inner = build_box(&text, 1);
58 Self {
59 text,
60 output_pad: 1,
61 inner: Mutex::new(inner),
62 built_revision: AtomicU64::new(theme_revision()),
63 }
64 }
65
66 pub fn with_pad(self, pad: usize) -> Self {
68 let inner = build_box(&self.text, pad);
69 Self {
70 text: self.text,
71 output_pad: pad,
72 inner: Mutex::new(inner),
73 built_revision: AtomicU64::new(theme_revision()),
74 }
75 }
76}
77
78impl Component for UserMessageComponent {
79 fn render(&self, width: usize) -> Vec<String> {
80 let revision = theme_revision();
83 if self.built_revision.load(Ordering::Acquire) != revision {
84 if let Ok(mut inner) = self.inner.lock() {
85 *inner = build_box(&self.text, self.output_pad);
86 }
87 self.built_revision.store(revision, Ordering::Release);
88 }
89
90 let mut lines = match self.inner.lock() {
91 Ok(inner) => inner.render(width),
92 Err(_) => return Vec::new(),
93 };
94 if lines.is_empty() {
95 return lines;
96 }
97 let first = lines.first_mut().unwrap();
100 first.insert_str(0, OSC133_ZONE_START);
101 let last = lines.last_mut().unwrap();
102 last.insert_str(0, OSC133_ZONE_END);
103 last.insert_str(OSC133_ZONE_END.len(), OSC133_ZONE_FINAL);
104 lines
105 }
106
107 fn invalidate(&self) {
108 if let Ok(mut inner) = self.inner.lock() {
111 *inner = build_box(&self.text, self.output_pad);
112 }
113 self.built_revision
114 .store(theme_revision(), Ordering::Release);
115 }
116
117 fn as_any(&self) -> &dyn Any {
118 self
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use crate::ansi::strip_ansi;
126
127 #[test]
128 fn test_renders_non_empty() {
129 let c = UserMessageComponent::new("Hello world");
130 let lines = c.render(40);
131 assert!(!lines.is_empty());
132 let joined = strip_ansi(&lines.join("\n"));
134 assert!(joined.contains("Hello"));
135 }
136
137 #[test]
138 fn test_osc133_markers_present() {
139 let c = UserMessageComponent::new("Hi");
140 let lines = c.render(40);
141 let joined = lines.join("\n");
142 assert!(joined.contains(OSC133_ZONE_START), "missing zone-start A");
143 assert!(joined.contains(OSC133_ZONE_END), "missing zone-end B");
144 assert!(joined.contains(OSC133_ZONE_FINAL), "missing zone-final C");
145 }
146
147 #[test]
148 fn test_cjk_no_panic() {
149 let c = UserMessageComponent::new("你好世界");
150 let lines = c.render(6);
151 assert!(!lines.is_empty());
152 }
153}