summaryrefslogtreecommitdiff
path: root/src/message_bar.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/message_bar.rs')
-rw-r--r--src/message_bar.rs517
1 files changed, 517 insertions, 0 deletions
diff --git a/src/message_bar.rs b/src/message_bar.rs
new file mode 100644
index 00000000..bbd705aa
--- /dev/null
+++ b/src/message_bar.rs
@@ -0,0 +1,517 @@
+// Copyright 2016 Joe Wilm, The Alacritty Project Contributors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use crossbeam_channel::{Receiver, Sender};
+
+use crate::term::color::Rgb;
+use crate::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,
+ topic: Option<String>,
+}
+
+impl Message {
+ /// Create a new message
+ pub fn new(text: String, color: Rgb) -> Message {
+ Message {
+ text,
+ color,
+ topic: 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 topic
+ #[inline]
+ pub fn topic(&self) -> Option<&String> {
+ self.topic.as_ref()
+ }
+
+ /// Update the message topic
+ #[inline]
+ pub fn set_topic(&mut self, topic: String) {
+ self.topic = Some(topic);
+ }
+
+ /// 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)]
+pub struct MessageBuffer {
+ current: Option<Message>,
+ messages: Receiver<Message>,
+ tx: Sender<Message>,
+}
+
+impl MessageBuffer {
+ /// Create new message buffer
+ pub fn new() -> MessageBuffer {
+ let (tx, messages) = crossbeam_channel::unbounded();
+ MessageBuffer {
+ current: None,
+ messages,
+ tx,
+ }
+ }
+
+ /// Check if there are any messages queued
+ #[inline]
+ pub fn is_empty(&self) -> bool {
+ self.current.is_none()
+ }
+
+ /// Current message
+ #[inline]
+ pub fn message(&mut self) -> Option<Message> {
+ if let Some(current) = &self.current {
+ Some(current.clone())
+ } else {
+ self.current = self.messages.try_recv().ok();
+ self.current.clone()
+ }
+ }
+
+ /// Channel for adding new messages
+ #[inline]
+ pub fn tx(&self) -> Sender<Message> {
+ self.tx.clone()
+ }
+
+ /// Remove the currently visible message
+ #[inline]
+ pub fn pop(&mut self) {
+ // Remove all duplicates
+ for msg in self
+ .messages
+ .try_iter()
+ .take(self.messages.len())
+ .filter(|m| Some(m) != self.current.as_ref())
+ {
+ let _ = self.tx.send(msg);
+ }
+
+ // Remove the message itself
+ self.current = self.messages.try_recv().ok();
+ }
+
+ /// Remove all messages with a specific topic
+ #[inline]
+ pub fn remove_topic(&mut self, topic: &str) {
+ // Filter messages currently pending
+ for msg in self
+ .messages
+ .try_iter()
+ .take(self.messages.len())
+ .filter(|m| m.topic().map(|s| s.as_str()) != Some(topic))
+ {
+ let _ = self.tx.send(msg);
+ }
+
+ // Remove the currently active message
+ self.current = self.messages.try_recv().ok();
+ }
+}
+
+impl Default for MessageBuffer {
+ fn default() -> MessageBuffer {
+ MessageBuffer::new()
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::{Message, MessageBuffer, MIN_FREE_LINES};
+ use crate::term::{color, SizeInfo};
+
+ #[test]
+ fn appends_close_button() {
+ let input = "a";
+ let mut message_buffer = MessageBuffer::new();
+ message_buffer
+ .tx()
+ .send(Message::new(input.into(), color::RED))
+ .unwrap();
+ 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
+ .tx()
+ .send(Message::new(input.into(), color::RED))
+ .unwrap();
+ 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
+ .tx()
+ .send(Message::new(input.into(), color::RED))
+ .unwrap();
+ 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
+ .tx()
+ .send(Message::new(input.into(), color::RED))
+ .unwrap();
+ 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
+ .tx()
+ .send(Message::new(input.into(), color::RED))
+ .unwrap();
+ 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
+ .tx()
+ .send(Message::new(input.into(), color::RED))
+ .unwrap();
+ 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
+ .tx()
+ .send(Message::new(input.into(), color::RED))
+ .unwrap();
+ 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
+ .tx()
+ .send(Message::new(input.into(), color::RED))
+ .unwrap();
+ 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
+ .tx()
+ .send(Message::new(input.into(), color::RED))
+ .unwrap();
+ 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_topic() {
+ 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_topic("topic".into());
+ }
+ message_buffer.tx().send(msg).unwrap();
+ }
+
+ message_buffer.remove_topic("topic");
+
+ // 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.tx().send(one.clone()).unwrap();
+ let two = Message::new(String::from("two"), color::YELLOW);
+ message_buffer.tx().send(two.clone()).unwrap();
+
+ 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
+ .tx()
+ .send(Message::new(input.into(), color::RED))
+ .unwrap();
+ 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.tx().send(msg).unwrap();
+ }
+ message_buffer.tx().send(Message::new(String::from("other"), color::RED)).unwrap();
+ message_buffer.tx().send(Message::new(String::from("test"), color::YELLOW)).unwrap();
+ 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);
+ }
+}