diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bc0097..ce81761 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `ReceiveChar` key binding action to insert the key's text character - Live reload font size from config - New CLI flag `--hold` for keeping Alacritty opened after its child process exits +- Escape sequence to save and restore window title from stack ### Changed diff --git a/alacritty/src/cli.rs b/alacritty/src/cli.rs index 99387a6..b0e3cc9 100644 --- a/alacritty/src/cli.rs +++ b/alacritty/src/cli.rs @@ -266,9 +266,12 @@ impl Options { config.window.dimensions = self.dimensions.unwrap_or(config.window.dimensions); config.window.position = self.position.or(config.window.position); - config.window.title = self.title.or(config.window.title); config.window.embed = self.embed.and_then(|embed| embed.parse().ok()); + if let Some(title) = self.title { + config.window.title = title.clone(); + } + if let Some(class) = self.class { let parts: Vec<_> = class.split(',').collect(); config.window.class.instance = parts[0].into(); @@ -277,7 +280,7 @@ impl Options { } } - config.set_dynamic_title(config.dynamic_title() && config.window.title.is_none()); + config.set_dynamic_title(config.dynamic_title() && config.window.title == DEFAULT_NAME); config.debug.print_events = self.print_events || config.debug.print_events; config.debug.log_level = max(config.debug.log_level, self.log_level); @@ -298,7 +301,8 @@ mod test { #[test] fn dynamic_title_ignoring_options_by_default() { - let config = Config::default(); + let mut config = Config::default(); + config.window.title = "Alacritty".to_string(); let old_dynamic_title = config.dynamic_title(); let config = Options::default().into_config(config); @@ -321,7 +325,7 @@ mod test { fn dynamic_title_overridden_by_config() { let mut config = Config::default(); - config.window.title = Some("foo".to_owned()); + config.window.title = "foo".to_owned(); let config = Options::default().into_config(config); assert!(!config.dynamic_title()); diff --git a/alacritty/src/window.rs b/alacritty/src/window.rs index c783dc6..0394296 100644 --- a/alacritty/src/window.rs +++ b/alacritty/src/window.rs @@ -31,7 +31,7 @@ use image::ImageFormat; #[cfg(not(any(target_os = "macos", windows)))] use x11_dl::xlib::{Display as XDisplay, PropModeReplace, XErrorEvent, Xlib}; -use alacritty_terminal::config::{Decorations, StartupMode, WindowConfig, DEFAULT_NAME}; +use alacritty_terminal::config::{Decorations, StartupMode, WindowConfig}; use alacritty_terminal::event::Event; use alacritty_terminal::gl; use alacritty_terminal::term::{SizeInfo, Term}; @@ -146,9 +146,7 @@ impl Window { config: &Config, logical: Option, ) -> Result { - let title = config.window.title.as_ref().map_or(DEFAULT_NAME, |t| t); - - let window_builder = Window::get_platform_window(title, &config.window); + let window_builder = Window::get_platform_window(&config.window.title, &config.window); let windowed_context = create_gl_window(window_builder.clone(), &event_loop, false, logical) .or_else(|_| create_gl_window(window_builder, &event_loop, true, logical))?; diff --git a/alacritty_terminal/src/ansi.rs b/alacritty_terminal/src/ansi.rs index b34c1cd..493b02a 100644 --- a/alacritty_terminal/src/ansi.rs +++ b/alacritty_terminal/src/ansi.rs @@ -328,6 +328,12 @@ pub trait Handler { /// Run the dectest routine fn dectest(&mut self) {} + + /// Push a title onto the stack + fn push_title(&mut self) {} + + /// Pop the last title from the stack + fn pop_title(&mut self) {} } /// Describes shape of cursor @@ -412,7 +418,13 @@ impl Mode { /// Create mode from a primitive /// /// TODO lots of unhandled values.. - pub fn from_primitive(private: bool, num: i64) -> Option { + pub fn from_primitive(intermediate: Option<&u8>, num: i64) -> Option { + let private = match intermediate { + Some(b'?') => true, + None => false, + _ => return None, + }; + if private { Some(match num { 1 => Mode::CursorKeys, @@ -991,22 +1003,18 @@ where handler.clear_line(mode); }, ('S', None) => handler.scroll_up(Line(arg_or_default!(idx: 0, default: 1) as usize)), + ('t', None) => match arg_or_default!(idx: 0, default: 1) as usize { + 22 => handler.push_title(), + 23 => handler.pop_title(), + _ => unhandled!(), + }, ('T', None) => handler.scroll_down(Line(arg_or_default!(idx: 0, default: 1) as usize)), ('L', None) => { handler.insert_blank_lines(Line(arg_or_default!(idx: 0, default: 1) as usize)) }, ('l', intermediate) => { - let is_private_mode = match intermediate { - Some(b'?') => true, - None => false, - _ => { - unhandled!(); - return; - }, - }; for arg in args { - let mode = Mode::from_primitive(is_private_mode, *arg); - match mode { + match Mode::from_primitive(intermediate, *arg) { Some(mode) => handler.unset_mode(mode), None => { unhandled!(); @@ -1027,17 +1035,8 @@ where handler.goto_line(Line(arg_or_default!(idx: 0, default: 1) as usize - 1)) }, ('h', intermediate) => { - let is_private_mode = match intermediate { - Some(b'?') => true, - None => false, - _ => { - unhandled!(); - return; - }, - }; for arg in args { - let mode = Mode::from_primitive(is_private_mode, *arg); - match mode { + match Mode::from_primitive(intermediate, *arg) { Some(mode) => handler.set_mode(mode), None => { unhandled!(); diff --git a/alacritty_terminal/src/config/window.rs b/alacritty_terminal/src/config/window.rs index f470f93..49bd70b 100644 --- a/alacritty_terminal/src/config/window.rs +++ b/alacritty_terminal/src/config/window.rs @@ -36,8 +36,8 @@ pub struct WindowConfig { startup_mode: StartupMode, /// Window title - #[serde(deserialize_with = "failure_default")] - pub title: Option, + #[serde(default = "default_title")] + pub title: String, /// Window class #[serde(deserialize_with = "from_string_or_deserialize")] @@ -56,6 +56,10 @@ pub struct WindowConfig { pub start_maximized: Option, } +pub fn default_title() -> String { + DEFAULT_NAME.to_string() +} + impl WindowConfig { pub fn startup_mode(&self) -> StartupMode { match self.start_maximized { diff --git a/alacritty_terminal/src/term/mod.rs b/alacritty_terminal/src/term/mod.rs index 4e51d73..382b10a 100644 --- a/alacritty_terminal/src/term/mod.rs +++ b/alacritty_terminal/src/term/mod.rs @@ -27,7 +27,7 @@ use crate::ansi::{ self, Attr, CharsetIndex, Color, CursorStyle, Handler, NamedColor, StandardCharset, TermInfo, }; use crate::clipboard::{Clipboard, ClipboardType}; -use crate::config::{Config, VisualBellAnimation}; +use crate::config::{Config, VisualBellAnimation, DEFAULT_NAME}; use crate::cursor::CursorKey; use crate::event::{Event, EventListener}; use crate::grid::{ @@ -47,6 +47,9 @@ pub mod color; /// Used to match equal brackets, when performing a bracket-pair selection. const BRACKET_PAIRS: [(char, char); 4] = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')]; +/// Max size of the window title stack +const TITLE_STACK_MAX_DEPTH: usize = 4096; + /// A type that can expand a given point to a region /// /// Usually this is implemented for some 2-D array type since @@ -760,6 +763,13 @@ pub struct Term { /// Terminal focus pub is_focused: bool, + + /// Current title of the window + title: String, + + /// Stack of saved window titles. When a title is popped from this stack, the `title` for the + /// term is set, and the Glutin window's title attribute is changed through the event listener. + title_stack: Vec, } /// Terminal size info @@ -887,6 +897,8 @@ impl Term { clipboard, event_proxy, is_focused: true, + title: config.window.title.clone(), + title_stack: Vec::new(), } } @@ -1322,6 +1334,9 @@ impl ansi::Handler for Term { #[cfg(not(windows))] fn set_title(&mut self, title: &str) { if self.dynamic_title { + trace!("Setting window title to '{}'", title); + + self.title = title.into(); self.event_proxy.send_event(Event::Title(title.to_owned())); } } @@ -1336,13 +1351,16 @@ impl ansi::Handler for Term { // // The starts_with check is necessary because other shells e.g. bash set a // different title and don't need Alacritty prepended. + trace!("Setting window title to '{}'", title); + let title = if !tty::is_conpty() && title.starts_with(' ') { format!("Alacritty {}", title.trim()) } else { title.to_owned() }; - self.event_proxy.send_event(Event::Title(title)); + self.title = title.clone(); + self.event_proxy.send_event(Event::Title(title.to_owned())); } } @@ -1912,6 +1930,8 @@ impl ansi::Handler for Term { self.grid.reset(&Cell::default()); self.alt_grid.reset(&Cell::default()); self.scroll_region = Line(0)..self.grid.num_lines(); + self.title = DEFAULT_NAME.to_string(); + self.title_stack.clear(); } #[inline] @@ -2089,6 +2109,31 @@ impl ansi::Handler for Term { trace!("Setting cursor style {:?}", style); self.cursor_style = style; } + + #[inline] + fn push_title(&mut self) { + trace!("Pushing '{}' onto title stack", self.title); + + if self.title_stack.len() >= TITLE_STACK_MAX_DEPTH { + let removed = self.title_stack.remove(0); + trace!( + "Removing '{}' from bottom of title stack that exceeds its maximum depth", + removed + ); + } + + self.title_stack.push(self.title.clone()); + } + + #[inline] + fn pop_title(&mut self) { + trace!("Attempting to pop title from stack..."); + + if let Some(popped) = self.title_stack.pop() { + trace!("Title '{}' popped from stack", popped); + self.set_title(&popped); + } + } } struct TabStops { @@ -2301,6 +2346,57 @@ mod tests { scrolled_grid.scroll_display(Scroll::Top); assert_eq!(term.grid, scrolled_grid); } + + #[test] + fn window_title() { + let size = SizeInfo { + width: 21.0, + height: 51.0, + cell_width: 3.0, + cell_height: 3.0, + padding_x: 0.0, + padding_y: 0.0, + dpr: 1.0, + }; + let mut term = Term::new(&MockConfig::default(), &size, Clipboard::new_nop(), Mock); + + // Title can be set + { + term.title = "Test".to_string(); + assert_eq!(term.title, "Test"); + } + + // Title can be pushed onto stack + { + term.push_title(); + term.title = "Next".to_string(); + assert_eq!(term.title, "Next"); + assert_eq!(term.title_stack.get(0).unwrap(), "Test"); + } + + // Title can be popped from stack and set as the window title + { + term.pop_title(); + assert_eq!(term.title, "Test"); + assert!(term.title_stack.is_empty()); + } + + // Title stack doesn't grow infinitely + { + for _ in 0..4097 { + term.push_title(); + } + assert_eq!(term.title_stack.len(), 4096); + } + + // Title and title stack reset when terminal state is reset + { + term.push_title(); + term.reset_state(); + assert_eq!(term.title, "Alacritty"); + assert!(term.title_stack.is_empty()); + } + } } #[cfg(all(test, feature = "bench"))] diff --git a/alacritty_terminal/src/tty/windows/conpty.rs b/alacritty_terminal/src/tty/windows/conpty.rs index fe49b4d..185acfc 100644 --- a/alacritty_terminal/src/tty/windows/conpty.rs +++ b/alacritty_terminal/src/tty/windows/conpty.rs @@ -143,7 +143,7 @@ pub fn new<'a, C>( let mut startup_info_ex: STARTUPINFOEXW = Default::default(); - let title = config.window.title.as_ref().map(String::as_str).unwrap_or("Alacritty"); + let title = config.window.title.clone(); let title = U16CString::from_str(title).unwrap(); startup_info_ex.StartupInfo.lpTitle = title.as_ptr() as LPWSTR;