blob: a0c821ae5e9ff687fad8a24e00c0ba90acae9677 [file] [log] [blame]
use std::collections::VecDeque;
use unicode_width::UnicodeWidthChar;
use alacritty_terminal::grid::Dimensions;
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,
ty: MessageType,
target: Option<String>,
}
/// Purpose of the message.
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
pub enum MessageType {
/// A message represents an error.
Error,
/// A message represents a warning.
Warning,
}
impl Message {
/// Create a new message.
pub fn new(text: String, ty: MessageType) -> Message {
Message { text, ty, target: None }
}
/// Formatted message text lines.
pub fn text(&self, size_info: &SizeInfo) -> Vec<String> {
let num_cols = size_info.columns();
let total_lines =
(size_info.height() - 2. * size_info.padding_y()) / size_info.cell_height();
let max_lines = (total_lines as usize).saturating_sub(MIN_FREE_LINES);
let button_len = CLOSE_BUTTON_TEXT.chars().count();
// Split line to fit the screen.
let mut lines = Vec::new();
let mut line = String::new();
let mut line_len = 0;
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))
{
let is_whitespace = c.is_whitespace();
// Attempt to wrap on word boundaries.
let mut new_line = String::new();
if let Some(index) = line.rfind(char::is_whitespace).filter(|_| !is_whitespace) {
let split = line.split_off(index + 1);
line.pop();
new_line = split;
}
lines.push(Self::pad_text(line, num_cols));
line = new_line;
line_len = line.chars().count();
// Do not append whitespace at EOL.
if is_whitespace {
continue;
}
}
line.push(c);
// Reserve extra column for fullwidth characters.
let width = c.width().unwrap_or(0);
if width == 2 {
line.push(' ');
}
line_len += width
}
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 type.
#[inline]
pub fn ty(&self) -> MessageType {
self.ty
}
/// 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.chars().count());
text.extend(vec![' '; padding_len]);
text
}
}
/// Storage for message bar.
#[derive(Debug, Default)]
pub struct MessageBuffer {
messages: VecDeque<Message>,
}
impl MessageBuffer {
/// 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::*;
use alacritty_terminal::term::SizeInfo;
#[test]
fn appends_close_button() {
let input = "a";
let mut message_buffer = MessageBuffer::default();
message_buffer.push(Message::new(input.into(), MessageType::Error));
let size = SizeInfo::new(7., 10., 1., 1., 0., 0., false);
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::default();
message_buffer.push(Message::new(input.into(), MessageType::Error));
let size = SizeInfo::new(6., 10., 1., 1., 0., 0., false);
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::default();
message_buffer.push(Message::new(input.into(), MessageType::Error));
let size = SizeInfo::new(6., 10., 1., 1., 0., 0., false);
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::default();
message_buffer.push(Message::new(input.into(), MessageType::Error));
let size = SizeInfo::new(6., 10., 1., 1., 0., 0., false);
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::default();
message_buffer.push(Message::new(input.into(), MessageType::Error));
let size = SizeInfo::new(6., 0., 1., 1., 0., 0., false);
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::default();
message_buffer.push(Message::new(input.into(), MessageType::Error));
let size = SizeInfo::new(22., (MIN_FREE_LINES + 2) as f32, 1., 1., 0., 0., false);
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::default();
message_buffer.push(Message::new(input.into(), MessageType::Error));
let size = SizeInfo::new(2., 10., 1., 1., 0., 0., false);
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::default();
message_buffer.push(Message::new(input.into(), MessageType::Error));
let size = SizeInfo::new(2., (MIN_FREE_LINES + 2) as f32, 1., 1., 0., 0., false);
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::default();
message_buffer.push(Message::new(input.into(), MessageType::Error));
let size = SizeInfo::new(5., 10., 1., 1., 0., 0., false);
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::default();
for i in 0..10 {
let mut msg = Message::new(i.to_string(), MessageType::Error);
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::default();
let one = Message::new(String::from("one"), MessageType::Error);
message_buffer.push(one.clone());
let two = Message::new(String::from("two"), MessageType::Warning);
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::default();
message_buffer.push(Message::new(input.into(), MessageType::Error));
let size = SizeInfo::new(5., 10., 1., 1., 0., 0., false);
let lines = message_buffer.message().unwrap().text(&size);
assert_eq!(lines, vec![
String::from("a [X]"),
String::from("bc "),
String::from("defg ")
]);
}
#[test]
fn wrap_with_unicode() {
let input = "ab\nc 👩d fgh";
let mut message_buffer = MessageBuffer::default();
message_buffer.push(Message::new(input.into(), MessageType::Error));
let size = SizeInfo::new(7., 10., 1., 1., 0., 0., false);
let lines = message_buffer.message().unwrap().text(&size);
assert_eq!(lines, vec![
String::from("ab [X]"),
String::from("c 👩 d "),
String::from("fgh ")
]);
}
#[test]
fn strip_whitespace_at_linebreak() {
let input = "\n0 1 2 3";
let mut message_buffer = MessageBuffer::default();
message_buffer.push(Message::new(input.into(), MessageType::Error));
let size = SizeInfo::new(3., 10., 1., 1., 0., 0., false);
let lines = message_buffer.message().unwrap().text(&size);
assert_eq!(lines, vec![String::from("[X]"), String::from("0 1"), String::from("2 3"),]);
}
#[test]
fn remove_duplicates() {
let mut message_buffer = MessageBuffer::default();
for _ in 0..10 {
let msg = Message::new(String::from("test"), MessageType::Error);
message_buffer.push(msg);
}
message_buffer.push(Message::new(String::from("other"), MessageType::Error));
message_buffer.push(Message::new(String::from("test"), MessageType::Warning));
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);
}
}