aboutsummaryrefslogtreecommitdiff
path: root/src/message_bar.rs
diff options
context:
space:
mode:
authorChristian Duerr <chrisduerr@users.noreply.github.com>2019-02-07 22:36:45 +0000
committerGitHub <noreply@github.com>2019-02-07 22:36:45 +0000
commit35efb4619c4b7a77b3c30de763856bc4441e236e (patch)
tree99aa70b58f97bd57c2a654e3177eae0ea0b572fa /src/message_bar.rs
parente561ae373393e919cf3dbbf123a98e0aad215dbc (diff)
downloadr-alacritty-35efb4619c4b7a77b3c30de763856bc4441e236e.tar.gz
r-alacritty-35efb4619c4b7a77b3c30de763856bc4441e236e.tar.bz2
r-alacritty-35efb4619c4b7a77b3c30de763856bc4441e236e.zip
Dynamically resize terminal for errors/warnings
The warning and error messages now don't overwrite other terminal content anymore but instead resize the terminal to make sure that text can always be read. Instead of just showing that there is a new error and pointing to the log, errors will now be displayed fully in multiple lines of text, assuming that there is enough space left in the terminal. Explicit mouse click handling has also been added to the message bar, which made it possible to add a simple `close` button in the form of `[X]`. Alacritty's log file location is now stored in the `$ALACRITTY_LOG` environment variable which the shell inherits automatically. Previously there were some issues with the log file only being deleted when certain methods for closing Alacritty were used (like typing `exit`). This has been reworked and now Ctrl+D, exit and signals should all work properly. Before the config is reloaded, all current messages are now dropped. This should help with multiple terminals all getting clogged up at the same time when the config is broken. When one message is removed, all other duplicate messages are automatically removed too.
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);
+ }
+}