474 lines
14 KiB
Rust
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);
|
|
}
|
|
}
|