term_model/
message_bar.rs

1// Copyright 2016 Joe Wilm, The Alacritty Project Contributors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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/// Message for display in the MessageBuffer.
26#[derive(Debug, Eq, PartialEq, Clone)]
27pub struct Message {
28    text: String,
29    color: Rgb,
30    target: Option<String>,
31}
32
33impl Message {
34    /// Create a new message.
35    pub fn new(text: String, color: Rgb) -> Message {
36        Message { text, color, target: None }
37    }
38
39    /// Formatted message text lines.
40    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        // Split line to fit the screen
46        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                // Keep space in first line for button
52                || (lines.is_empty()
53                    && num_cols >= button_len
54                    && line.len() == num_cols.saturating_sub(button_len + CLOSE_BUTTON_PADDING))
55            {
56                // Attempt to wrap on word boundaries
57                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        // Truncate output if it's too long
75        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        // Append close button to first line
85        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    /// Message color.
96    #[inline]
97    pub fn color(&self) -> Rgb {
98        self.color
99    }
100
101    /// Message target.
102    #[inline]
103    pub fn target(&self) -> Option<&String> {
104        self.target.as_ref()
105    }
106
107    /// Update the message target.
108    #[inline]
109    pub fn set_target(&mut self, target: String) {
110        self.target = Some(target);
111    }
112
113    /// Right-pad text to fit a specific number of columns.
114    #[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/// Storage for message bar.
123#[derive(Debug, Default)]
124pub struct MessageBuffer {
125    messages: VecDeque<Message>,
126}
127
128impl MessageBuffer {
129    /// Create new message buffer.
130    pub fn new() -> MessageBuffer {
131        MessageBuffer { messages: VecDeque::new() }
132    }
133
134    /// Check if there are any messages queued.
135    #[inline]
136    pub fn is_empty(&self) -> bool {
137        self.messages.is_empty()
138    }
139
140    /// Current message.
141    #[inline]
142    pub fn message(&self) -> Option<&Message> {
143        self.messages.front()
144    }
145
146    /// Remove the currently visible message.
147    #[inline]
148    pub fn pop(&mut self) {
149        // Remove the message itself
150        let msg = self.messages.pop_front();
151
152        // Remove all duplicates
153        if let Some(msg) = msg {
154            self.messages = self.messages.drain(..).filter(|m| m != &msg).collect();
155        }
156    }
157
158    /// Remove all messages with a specific target.
159    #[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    /// Add a new message to the queue.
169    #[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        // Count number of messages
377        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        // Count number of messages
438        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}