alacritty/alacritty_terminal/src/message_bar.rs

474 lines
14 KiB
Rust

// 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(String::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);
}
}