diff options
Diffstat (limited to 'alacritty/src/message_bar.rs')
-rw-r--r-- | alacritty/src/message_bar.rs | 433 |
1 files changed, 433 insertions, 0 deletions
diff --git a/alacritty/src/message_bar.rs b/alacritty/src/message_bar.rs new file mode 100644 index 00000000..175f7f81 --- /dev/null +++ b/alacritty/src/message_bar.rs @@ -0,0 +1,433 @@ +use std::collections::VecDeque; + +use alacritty_terminal::term::color::Rgb; +use alacritty_terminal::term::SizeInfo; + +pub const CLOSE_BUTTON_TEXT: &str = "[X]"; +const CLOSE_BUTTON_PADDING: usize = 1; +const MIN_FREE_LINES: usize = 3; +const TRUNCATED_MESSAGE: &str = "[MESSAGE TRUNCATED]"; + +/// Message for display in the MessageBuffer. +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct Message { + text: String, + color: Rgb, + target: Option<String>, +} + +impl Message { + /// Create a new message. + pub fn new(text: String, color: Rgb) -> Message { + Message { text, color, target: None } + } + + /// Formatted message text lines. + pub fn text(&self, size_info: &SizeInfo) -> Vec<String> { + let num_cols = size_info.cols().0; + let max_lines = size_info.lines().saturating_sub(MIN_FREE_LINES); + let button_len = CLOSE_BUTTON_TEXT.len(); + + // Split line to fit the screen. + let mut lines = Vec::new(); + let mut line = String::new(); + for c in self.text.trim().chars() { + if c == '\n' + || line.len() == num_cols + // Keep space in first line for button. + || (lines.is_empty() + && num_cols >= button_len + && line.len() == num_cols.saturating_sub(button_len + CLOSE_BUTTON_PADDING)) + { + // Attempt to wrap on word boundaries. + if let (Some(index), true) = (line.rfind(char::is_whitespace), c != '\n') { + let split = line.split_off(index + 1); + line.pop(); + lines.push(Self::pad_text(line, num_cols)); + line = split + } else { + lines.push(Self::pad_text(line, num_cols)); + line = String::new(); + } + } + + if c != '\n' { + line.push(c); + } + } + lines.push(Self::pad_text(line, num_cols)); + + // Truncate output if it's too long. + if lines.len() > max_lines { + lines.truncate(max_lines); + if TRUNCATED_MESSAGE.len() <= num_cols { + if let Some(line) = lines.iter_mut().last() { + *line = Self::pad_text(TRUNCATED_MESSAGE.into(), num_cols); + } + } + } + + // Append close button to first line. + if button_len <= num_cols { + if let Some(line) = lines.get_mut(0) { + line.truncate(num_cols - button_len); + line.push_str(CLOSE_BUTTON_TEXT); + } + } + + lines + } + + /// Message color. + #[inline] + pub fn color(&self) -> Rgb { + self.color + } + + /// Message target. + #[inline] + pub fn target(&self) -> Option<&String> { + self.target.as_ref() + } + + /// Update the message target. + #[inline] + pub fn set_target(&mut self, target: String) { + self.target = Some(target); + } + + /// Right-pad text to fit a specific number of columns. + #[inline] + fn pad_text(mut text: String, num_cols: usize) -> String { + let padding_len = num_cols.saturating_sub(text.len()); + text.extend(vec![' '; padding_len]); + text + } +} + +/// Storage for message bar. +#[derive(Debug, Default)] +pub struct MessageBuffer { + messages: VecDeque<Message>, +} + +impl MessageBuffer { + /// Create new message buffer. + pub fn new() -> MessageBuffer { + MessageBuffer { messages: VecDeque::new() } + } + + /// Check if there are any messages queued. + #[inline] + pub fn is_empty(&self) -> bool { + self.messages.is_empty() + } + + /// Current message. + #[inline] + pub fn message(&self) -> Option<&Message> { + self.messages.front() + } + + /// Remove the currently visible message. + #[inline] + pub fn pop(&mut self) { + // Remove the message itself. + let msg = self.messages.pop_front(); + + // Remove all duplicates. + if let Some(msg) = msg { + self.messages = self.messages.drain(..).filter(|m| m != &msg).collect(); + } + } + + /// Remove all messages with a specific target. + #[inline] + pub fn remove_target(&mut self, target: &str) { + self.messages = self + .messages + .drain(..) + .filter(|m| m.target().map(String::as_str) != Some(target)) + .collect(); + } + + /// Add a new message to the queue. + #[inline] + pub fn push(&mut self, message: Message) { + self.messages.push_back(message); + } +} + +#[cfg(test)] +mod tests { + use super::{Message, MessageBuffer, MIN_FREE_LINES}; + use alacritty_terminal::term::{color, SizeInfo}; + + #[test] + fn appends_close_button() { + let input = "a"; + let mut message_buffer = MessageBuffer::new(); + message_buffer.push(Message::new(input.into(), color::RED)); + let size = SizeInfo { + width: 7., + height: 10., + cell_width: 1., + cell_height: 1., + padding_x: 0., + padding_y: 0., + dpr: 0., + }; + + let lines = message_buffer.message().unwrap().text(&size); + + assert_eq!(lines, vec![String::from("a [X]")]); + } + + #[test] + fn multiline_close_button_first_line() { + let input = "fo\nbar"; + let mut message_buffer = MessageBuffer::new(); + message_buffer.push(Message::new(input.into(), color::RED)); + let size = SizeInfo { + width: 6., + height: 10., + cell_width: 1., + cell_height: 1., + padding_x: 0., + padding_y: 0., + dpr: 0., + }; + + let lines = message_buffer.message().unwrap().text(&size); + + assert_eq!(lines, vec![String::from("fo [X]"), String::from("bar ")]); + } + + #[test] + fn splits_on_newline() { + let input = "a\nb"; + let mut message_buffer = MessageBuffer::new(); + message_buffer.push(Message::new(input.into(), color::RED)); + let size = SizeInfo { + width: 6., + height: 10., + cell_width: 1., + cell_height: 1., + padding_x: 0., + padding_y: 0., + dpr: 0., + }; + + let lines = message_buffer.message().unwrap().text(&size); + + assert_eq!(lines.len(), 2); + } + + #[test] + fn splits_on_length() { + let input = "foobar1"; + let mut message_buffer = MessageBuffer::new(); + message_buffer.push(Message::new(input.into(), color::RED)); + let size = SizeInfo { + width: 6., + height: 10., + cell_width: 1., + cell_height: 1., + padding_x: 0., + padding_y: 0., + dpr: 0., + }; + + let lines = message_buffer.message().unwrap().text(&size); + + assert_eq!(lines.len(), 2); + } + + #[test] + fn empty_with_shortterm() { + let input = "foobar"; + let mut message_buffer = MessageBuffer::new(); + message_buffer.push(Message::new(input.into(), color::RED)); + let size = SizeInfo { + width: 6., + height: 0., + cell_width: 1., + cell_height: 1., + padding_x: 0., + padding_y: 0., + dpr: 0., + }; + + let lines = message_buffer.message().unwrap().text(&size); + + assert_eq!(lines.len(), 0); + } + + #[test] + fn truncates_long_messages() { + let input = "hahahahahahahahahahaha truncate this because it's too long for the term"; + let mut message_buffer = MessageBuffer::new(); + message_buffer.push(Message::new(input.into(), color::RED)); + let size = SizeInfo { + width: 22., + height: (MIN_FREE_LINES + 2) as f32, + cell_width: 1., + cell_height: 1., + padding_x: 0., + padding_y: 0., + dpr: 0., + }; + + let lines = message_buffer.message().unwrap().text(&size); + + assert_eq!(lines, vec![ + String::from("hahahahahahahahaha [X]"), + String::from("[MESSAGE TRUNCATED] ") + ]); + } + + #[test] + fn hide_button_when_too_narrow() { + let input = "ha"; + let mut message_buffer = MessageBuffer::new(); + message_buffer.push(Message::new(input.into(), color::RED)); + let size = SizeInfo { + width: 2., + height: 10., + cell_width: 1., + cell_height: 1., + padding_x: 0., + padding_y: 0., + dpr: 0., + }; + + let lines = message_buffer.message().unwrap().text(&size); + + assert_eq!(lines, vec![String::from("ha")]); + } + + #[test] + fn hide_truncated_when_too_narrow() { + let input = "hahahahahahahahaha"; + let mut message_buffer = MessageBuffer::new(); + message_buffer.push(Message::new(input.into(), color::RED)); + let size = SizeInfo { + width: 2., + height: (MIN_FREE_LINES + 2) as f32, + cell_width: 1., + cell_height: 1., + padding_x: 0., + padding_y: 0., + dpr: 0., + }; + + let lines = message_buffer.message().unwrap().text(&size); + + assert_eq!(lines, vec![String::from("ha"), String::from("ha")]); + } + + #[test] + fn add_newline_for_button() { + let input = "test"; + let mut message_buffer = MessageBuffer::new(); + message_buffer.push(Message::new(input.into(), color::RED)); + let size = SizeInfo { + width: 5., + height: 10., + cell_width: 1., + cell_height: 1., + padding_x: 0., + padding_y: 0., + dpr: 0., + }; + + let lines = message_buffer.message().unwrap().text(&size); + + assert_eq!(lines, vec![String::from("t [X]"), String::from("est ")]); + } + + #[test] + fn remove_target() { + let mut message_buffer = MessageBuffer::new(); + for i in 0..10 { + let mut msg = Message::new(i.to_string(), color::RED); + if i % 2 == 0 && i < 5 { + msg.set_target("target".into()); + } + message_buffer.push(msg); + } + + message_buffer.remove_target("target"); + + // Count number of messages. + let mut num_messages = 0; + while message_buffer.message().is_some() { + num_messages += 1; + message_buffer.pop(); + } + + assert_eq!(num_messages, 7); + } + + #[test] + fn pop() { + let mut message_buffer = MessageBuffer::new(); + let one = Message::new(String::from("one"), color::RED); + message_buffer.push(one.clone()); + let two = Message::new(String::from("two"), color::YELLOW); + message_buffer.push(two.clone()); + + assert_eq!(message_buffer.message(), Some(&one)); + + message_buffer.pop(); + + assert_eq!(message_buffer.message(), Some(&two)); + } + + #[test] + fn wrap_on_words() { + let input = "a\nbc defg"; + let mut message_buffer = MessageBuffer::new(); + message_buffer.push(Message::new(input.into(), color::RED)); + let size = SizeInfo { + width: 5., + height: 10., + cell_width: 1., + cell_height: 1., + padding_x: 0., + padding_y: 0., + dpr: 0., + }; + + let lines = message_buffer.message().unwrap().text(&size); + + assert_eq!(lines, vec![ + String::from("a [X]"), + String::from("bc "), + String::from("defg ") + ]); + } + + #[test] + fn remove_duplicates() { + let mut message_buffer = MessageBuffer::new(); + for _ in 0..10 { + let msg = Message::new(String::from("test"), color::RED); + message_buffer.push(msg); + } + message_buffer.push(Message::new(String::from("other"), color::RED)); + message_buffer.push(Message::new(String::from("test"), color::YELLOW)); + let _ = message_buffer.message(); + + message_buffer.pop(); + + // Count number of messages. + let mut num_messages = 0; + while message_buffer.message().is_some() { + num_messages += 1; + message_buffer.pop(); + } + + assert_eq!(num_messages, 2); + } +} |