1use std::collections::VecDeque;
16
17use crate::term::color::Rgb;
18use crate::term::SizeInfo;
19
20pub const CLOSE_BUTTON_TEXT: &str = "[X]";
21const CLOSE_BUTTON_PADDING: usize = 1;
22const MIN_FREE_LINES: usize = 3;
23const TRUNCATED_MESSAGE: &str = "[MESSAGE TRUNCATED]";
24
25#[derive(Debug, Eq, PartialEq, Clone)]
27pub struct Message {
28 text: String,
29 color: Rgb,
30 target: Option<String>,
31}
32
33impl Message {
34 pub fn new(text: String, color: Rgb) -> Message {
36 Message { text, color, target: None }
37 }
38
39 pub fn text(&self, size_info: &SizeInfo) -> Vec<String> {
41 let num_cols = size_info.cols().0;
42 let max_lines = size_info.lines().saturating_sub(MIN_FREE_LINES);
43 let button_len = CLOSE_BUTTON_TEXT.len();
44
45 let mut lines = Vec::new();
47 let mut line = String::new();
48 for c in self.text.trim().chars() {
49 if c == '\n'
50 || line.len() == num_cols
51 || (lines.is_empty()
53 && num_cols >= button_len
54 && line.len() == num_cols.saturating_sub(button_len + CLOSE_BUTTON_PADDING))
55 {
56 if let (Some(index), true) = (line.rfind(char::is_whitespace), c != '\n') {
58 let split = line.split_off(index + 1);
59 line.pop();
60 lines.push(Self::pad_text(line, num_cols));
61 line = split
62 } else {
63 lines.push(Self::pad_text(line, num_cols));
64 line = String::new();
65 }
66 }
67
68 if c != '\n' {
69 line.push(c);
70 }
71 }
72 lines.push(Self::pad_text(line, num_cols));
73
74 if lines.len() > max_lines {
76 lines.truncate(max_lines);
77 if TRUNCATED_MESSAGE.len() <= num_cols {
78 if let Some(line) = lines.iter_mut().last() {
79 *line = Self::pad_text(TRUNCATED_MESSAGE.into(), num_cols);
80 }
81 }
82 }
83
84 if button_len <= num_cols {
86 if let Some(line) = lines.get_mut(0) {
87 line.truncate(num_cols - button_len);
88 line.push_str(CLOSE_BUTTON_TEXT);
89 }
90 }
91
92 lines
93 }
94
95 #[inline]
97 pub fn color(&self) -> Rgb {
98 self.color
99 }
100
101 #[inline]
103 pub fn target(&self) -> Option<&String> {
104 self.target.as_ref()
105 }
106
107 #[inline]
109 pub fn set_target(&mut self, target: String) {
110 self.target = Some(target);
111 }
112
113 #[inline]
115 fn pad_text(mut text: String, num_cols: usize) -> String {
116 let padding_len = num_cols.saturating_sub(text.len());
117 text.extend(vec![' '; padding_len]);
118 text
119 }
120}
121
122#[derive(Debug, Default)]
124pub struct MessageBuffer {
125 messages: VecDeque<Message>,
126}
127
128impl MessageBuffer {
129 pub fn new() -> MessageBuffer {
131 MessageBuffer { messages: VecDeque::new() }
132 }
133
134 #[inline]
136 pub fn is_empty(&self) -> bool {
137 self.messages.is_empty()
138 }
139
140 #[inline]
142 pub fn message(&self) -> Option<&Message> {
143 self.messages.front()
144 }
145
146 #[inline]
148 pub fn pop(&mut self) {
149 let msg = self.messages.pop_front();
151
152 if let Some(msg) = msg {
154 self.messages = self.messages.drain(..).filter(|m| m != &msg).collect();
155 }
156 }
157
158 #[inline]
160 pub fn remove_target(&mut self, target: &str) {
161 self.messages = self
162 .messages
163 .drain(..)
164 .filter(|m| m.target().map(String::as_str) != Some(target))
165 .collect();
166 }
167
168 #[inline]
170 pub fn push(&mut self, message: Message) {
171 self.messages.push_back(message);
172 }
173}
174
175#[cfg(test)]
176mod test {
177 use super::{Message, MessageBuffer, MIN_FREE_LINES};
178 use crate::term::{color, SizeInfo};
179
180 #[test]
181 fn appends_close_button() {
182 let input = "a";
183 let mut message_buffer = MessageBuffer::new();
184 message_buffer.push(Message::new(input.into(), color::RED));
185 let size = SizeInfo {
186 width: 7.,
187 height: 10.,
188 cell_width: 1.,
189 cell_height: 1.,
190 padding_x: 0.,
191 padding_y: 0.,
192 dpr: 0.,
193 };
194
195 let lines = message_buffer.message().unwrap().text(&size);
196
197 assert_eq!(lines, vec![String::from("a [X]")]);
198 }
199
200 #[test]
201 fn multiline_close_button_first_line() {
202 let input = "fo\nbar";
203 let mut message_buffer = MessageBuffer::new();
204 message_buffer.push(Message::new(input.into(), color::RED));
205 let size = SizeInfo {
206 width: 6.,
207 height: 10.,
208 cell_width: 1.,
209 cell_height: 1.,
210 padding_x: 0.,
211 padding_y: 0.,
212 dpr: 0.,
213 };
214
215 let lines = message_buffer.message().unwrap().text(&size);
216
217 assert_eq!(lines, vec![String::from("fo [X]"), String::from("bar ")]);
218 }
219
220 #[test]
221 fn splits_on_newline() {
222 let input = "a\nb";
223 let mut message_buffer = MessageBuffer::new();
224 message_buffer.push(Message::new(input.into(), color::RED));
225 let size = SizeInfo {
226 width: 6.,
227 height: 10.,
228 cell_width: 1.,
229 cell_height: 1.,
230 padding_x: 0.,
231 padding_y: 0.,
232 dpr: 0.,
233 };
234
235 let lines = message_buffer.message().unwrap().text(&size);
236
237 assert_eq!(lines.len(), 2);
238 }
239
240 #[test]
241 fn splits_on_length() {
242 let input = "foobar1";
243 let mut message_buffer = MessageBuffer::new();
244 message_buffer.push(Message::new(input.into(), color::RED));
245 let size = SizeInfo {
246 width: 6.,
247 height: 10.,
248 cell_width: 1.,
249 cell_height: 1.,
250 padding_x: 0.,
251 padding_y: 0.,
252 dpr: 0.,
253 };
254
255 let lines = message_buffer.message().unwrap().text(&size);
256
257 assert_eq!(lines.len(), 2);
258 }
259
260 #[test]
261 fn empty_with_shortterm() {
262 let input = "foobar";
263 let mut message_buffer = MessageBuffer::new();
264 message_buffer.push(Message::new(input.into(), color::RED));
265 let size = SizeInfo {
266 width: 6.,
267 height: 0.,
268 cell_width: 1.,
269 cell_height: 1.,
270 padding_x: 0.,
271 padding_y: 0.,
272 dpr: 0.,
273 };
274
275 let lines = message_buffer.message().unwrap().text(&size);
276
277 assert_eq!(lines.len(), 0);
278 }
279
280 #[test]
281 fn truncates_long_messages() {
282 let input = "hahahahahahahahahahaha truncate this because it's too long for the term";
283 let mut message_buffer = MessageBuffer::new();
284 message_buffer.push(Message::new(input.into(), color::RED));
285 let size = SizeInfo {
286 width: 22.,
287 height: (MIN_FREE_LINES + 2) as f32,
288 cell_width: 1.,
289 cell_height: 1.,
290 padding_x: 0.,
291 padding_y: 0.,
292 dpr: 0.,
293 };
294
295 let lines = message_buffer.message().unwrap().text(&size);
296
297 assert_eq!(
298 lines,
299 vec![String::from("hahahahahahahahaha [X]"), String::from("[MESSAGE TRUNCATED] ")]
300 );
301 }
302
303 #[test]
304 fn hide_button_when_too_narrow() {
305 let input = "ha";
306 let mut message_buffer = MessageBuffer::new();
307 message_buffer.push(Message::new(input.into(), color::RED));
308 let size = SizeInfo {
309 width: 2.,
310 height: 10.,
311 cell_width: 1.,
312 cell_height: 1.,
313 padding_x: 0.,
314 padding_y: 0.,
315 dpr: 0.,
316 };
317
318 let lines = message_buffer.message().unwrap().text(&size);
319
320 assert_eq!(lines, vec![String::from("ha")]);
321 }
322
323 #[test]
324 fn hide_truncated_when_too_narrow() {
325 let input = "hahahahahahahahaha";
326 let mut message_buffer = MessageBuffer::new();
327 message_buffer.push(Message::new(input.into(), color::RED));
328 let size = SizeInfo {
329 width: 2.,
330 height: (MIN_FREE_LINES + 2) as f32,
331 cell_width: 1.,
332 cell_height: 1.,
333 padding_x: 0.,
334 padding_y: 0.,
335 dpr: 0.,
336 };
337
338 let lines = message_buffer.message().unwrap().text(&size);
339
340 assert_eq!(lines, vec![String::from("ha"), String::from("ha")]);
341 }
342
343 #[test]
344 fn add_newline_for_button() {
345 let input = "test";
346 let mut message_buffer = MessageBuffer::new();
347 message_buffer.push(Message::new(input.into(), color::RED));
348 let size = SizeInfo {
349 width: 5.,
350 height: 10.,
351 cell_width: 1.,
352 cell_height: 1.,
353 padding_x: 0.,
354 padding_y: 0.,
355 dpr: 0.,
356 };
357
358 let lines = message_buffer.message().unwrap().text(&size);
359
360 assert_eq!(lines, vec![String::from("t [X]"), String::from("est ")]);
361 }
362
363 #[test]
364 fn remove_target() {
365 let mut message_buffer = MessageBuffer::new();
366 for i in 0..10 {
367 let mut msg = Message::new(i.to_string(), color::RED);
368 if i % 2 == 0 && i < 5 {
369 msg.set_target("target".into());
370 }
371 message_buffer.push(msg);
372 }
373
374 message_buffer.remove_target("target");
375
376 let mut num_messages = 0;
378 while message_buffer.message().is_some() {
379 num_messages += 1;
380 message_buffer.pop();
381 }
382
383 assert_eq!(num_messages, 7);
384 }
385
386 #[test]
387 fn pop() {
388 let mut message_buffer = MessageBuffer::new();
389 let one = Message::new(String::from("one"), color::RED);
390 message_buffer.push(one.clone());
391 let two = Message::new(String::from("two"), color::YELLOW);
392 message_buffer.push(two.clone());
393
394 assert_eq!(message_buffer.message(), Some(&one));
395
396 message_buffer.pop();
397
398 assert_eq!(message_buffer.message(), Some(&two));
399 }
400
401 #[test]
402 fn wrap_on_words() {
403 let input = "a\nbc defg";
404 let mut message_buffer = MessageBuffer::new();
405 message_buffer.push(Message::new(input.into(), color::RED));
406 let size = SizeInfo {
407 width: 5.,
408 height: 10.,
409 cell_width: 1.,
410 cell_height: 1.,
411 padding_x: 0.,
412 padding_y: 0.,
413 dpr: 0.,
414 };
415
416 let lines = message_buffer.message().unwrap().text(&size);
417
418 assert_eq!(
419 lines,
420 vec![String::from("a [X]"), String::from("bc "), String::from("defg ")]
421 );
422 }
423
424 #[test]
425 fn remove_duplicates() {
426 let mut message_buffer = MessageBuffer::new();
427 for _ in 0..10 {
428 let msg = Message::new(String::from("test"), color::RED);
429 message_buffer.push(msg);
430 }
431 message_buffer.push(Message::new(String::from("other"), color::RED));
432 message_buffer.push(Message::new(String::from("test"), color::YELLOW));
433 let _ = message_buffer.message();
434
435 message_buffer.pop();
436
437 let mut num_messages = 0;
439 while message_buffer.message().is_some() {
440 num_messages += 1;
441 message_buffer.pop();
442 }
443
444 assert_eq!(num_messages, 2);
445 }
446}