mirror of
https://github.com/alacritty/alacritty.git
synced 2025-02-24 16:06:43 -05:00
Add support for title stack escape sequences
This commit adds the concept of a "title stack" to the terminal. Some programs (e.g. vim) send control sequences `CSI 22 ; 0` (push title) and `CSI 23 ; 0` (pop title). The title stack is just a history of previous titles. Applications can push the current title onto the stack, and pop it back off (setting the window title in the process). Fixes #2840.
This commit is contained in:
parent
3475e44987
commit
401c2aab96
7 changed files with 136 additions and 34 deletions
|
@ -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
|
- `ReceiveChar` key binding action to insert the key's text character
|
||||||
- Live reload font size from config
|
- Live reload font size from config
|
||||||
- New CLI flag `--hold` for keeping Alacritty opened after its child process exits
|
- New CLI flag `--hold` for keeping Alacritty opened after its child process exits
|
||||||
|
- Escape sequence to save and restore window title from stack
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|
|
@ -266,9 +266,12 @@ impl Options {
|
||||||
|
|
||||||
config.window.dimensions = self.dimensions.unwrap_or(config.window.dimensions);
|
config.window.dimensions = self.dimensions.unwrap_or(config.window.dimensions);
|
||||||
config.window.position = self.position.or(config.window.position);
|
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());
|
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 {
|
if let Some(class) = self.class {
|
||||||
let parts: Vec<_> = class.split(',').collect();
|
let parts: Vec<_> = class.split(',').collect();
|
||||||
config.window.class.instance = parts[0].into();
|
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.print_events = self.print_events || config.debug.print_events;
|
||||||
config.debug.log_level = max(config.debug.log_level, self.log_level);
|
config.debug.log_level = max(config.debug.log_level, self.log_level);
|
||||||
|
@ -298,7 +301,8 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dynamic_title_ignoring_options_by_default() {
|
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 old_dynamic_title = config.dynamic_title();
|
||||||
|
|
||||||
let config = Options::default().into_config(config);
|
let config = Options::default().into_config(config);
|
||||||
|
@ -321,7 +325,7 @@ mod test {
|
||||||
fn dynamic_title_overridden_by_config() {
|
fn dynamic_title_overridden_by_config() {
|
||||||
let mut config = Config::default();
|
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);
|
let config = Options::default().into_config(config);
|
||||||
|
|
||||||
assert!(!config.dynamic_title());
|
assert!(!config.dynamic_title());
|
||||||
|
|
|
@ -31,7 +31,7 @@ use image::ImageFormat;
|
||||||
#[cfg(not(any(target_os = "macos", windows)))]
|
#[cfg(not(any(target_os = "macos", windows)))]
|
||||||
use x11_dl::xlib::{Display as XDisplay, PropModeReplace, XErrorEvent, Xlib};
|
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::event::Event;
|
||||||
use alacritty_terminal::gl;
|
use alacritty_terminal::gl;
|
||||||
use alacritty_terminal::term::{SizeInfo, Term};
|
use alacritty_terminal::term::{SizeInfo, Term};
|
||||||
|
@ -146,9 +146,7 @@ impl Window {
|
||||||
config: &Config,
|
config: &Config,
|
||||||
logical: Option<LogicalSize>,
|
logical: Option<LogicalSize>,
|
||||||
) -> Result<Window> {
|
) -> Result<Window> {
|
||||||
let title = config.window.title.as_ref().map_or(DEFAULT_NAME, |t| t);
|
let window_builder = Window::get_platform_window(&config.window.title, &config.window);
|
||||||
|
|
||||||
let window_builder = Window::get_platform_window(title, &config.window);
|
|
||||||
let windowed_context =
|
let windowed_context =
|
||||||
create_gl_window(window_builder.clone(), &event_loop, false, logical)
|
create_gl_window(window_builder.clone(), &event_loop, false, logical)
|
||||||
.or_else(|_| create_gl_window(window_builder, &event_loop, true, logical))?;
|
.or_else(|_| create_gl_window(window_builder, &event_loop, true, logical))?;
|
||||||
|
|
|
@ -328,6 +328,12 @@ pub trait Handler {
|
||||||
|
|
||||||
/// Run the dectest routine
|
/// Run the dectest routine
|
||||||
fn dectest(&mut self) {}
|
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
|
/// Describes shape of cursor
|
||||||
|
@ -412,7 +418,13 @@ impl Mode {
|
||||||
/// Create mode from a primitive
|
/// Create mode from a primitive
|
||||||
///
|
///
|
||||||
/// TODO lots of unhandled values..
|
/// TODO lots of unhandled values..
|
||||||
pub fn from_primitive(private: bool, num: i64) -> Option<Mode> {
|
pub fn from_primitive(intermediate: Option<&u8>, num: i64) -> Option<Mode> {
|
||||||
|
let private = match intermediate {
|
||||||
|
Some(b'?') => true,
|
||||||
|
None => false,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
if private {
|
if private {
|
||||||
Some(match num {
|
Some(match num {
|
||||||
1 => Mode::CursorKeys,
|
1 => Mode::CursorKeys,
|
||||||
|
@ -991,22 +1003,18 @@ where
|
||||||
handler.clear_line(mode);
|
handler.clear_line(mode);
|
||||||
},
|
},
|
||||||
('S', None) => handler.scroll_up(Line(arg_or_default!(idx: 0, default: 1) as usize)),
|
('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)),
|
('T', None) => handler.scroll_down(Line(arg_or_default!(idx: 0, default: 1) as usize)),
|
||||||
('L', None) => {
|
('L', None) => {
|
||||||
handler.insert_blank_lines(Line(arg_or_default!(idx: 0, default: 1) as usize))
|
handler.insert_blank_lines(Line(arg_or_default!(idx: 0, default: 1) as usize))
|
||||||
},
|
},
|
||||||
('l', intermediate) => {
|
('l', intermediate) => {
|
||||||
let is_private_mode = match intermediate {
|
|
||||||
Some(b'?') => true,
|
|
||||||
None => false,
|
|
||||||
_ => {
|
|
||||||
unhandled!();
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
for arg in args {
|
for arg in args {
|
||||||
let mode = Mode::from_primitive(is_private_mode, *arg);
|
match Mode::from_primitive(intermediate, *arg) {
|
||||||
match mode {
|
|
||||||
Some(mode) => handler.unset_mode(mode),
|
Some(mode) => handler.unset_mode(mode),
|
||||||
None => {
|
None => {
|
||||||
unhandled!();
|
unhandled!();
|
||||||
|
@ -1027,17 +1035,8 @@ where
|
||||||
handler.goto_line(Line(arg_or_default!(idx: 0, default: 1) as usize - 1))
|
handler.goto_line(Line(arg_or_default!(idx: 0, default: 1) as usize - 1))
|
||||||
},
|
},
|
||||||
('h', intermediate) => {
|
('h', intermediate) => {
|
||||||
let is_private_mode = match intermediate {
|
|
||||||
Some(b'?') => true,
|
|
||||||
None => false,
|
|
||||||
_ => {
|
|
||||||
unhandled!();
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
for arg in args {
|
for arg in args {
|
||||||
let mode = Mode::from_primitive(is_private_mode, *arg);
|
match Mode::from_primitive(intermediate, *arg) {
|
||||||
match mode {
|
|
||||||
Some(mode) => handler.set_mode(mode),
|
Some(mode) => handler.set_mode(mode),
|
||||||
None => {
|
None => {
|
||||||
unhandled!();
|
unhandled!();
|
||||||
|
|
|
@ -36,8 +36,8 @@ pub struct WindowConfig {
|
||||||
startup_mode: StartupMode,
|
startup_mode: StartupMode,
|
||||||
|
|
||||||
/// Window title
|
/// Window title
|
||||||
#[serde(deserialize_with = "failure_default")]
|
#[serde(default = "default_title")]
|
||||||
pub title: Option<String>,
|
pub title: String,
|
||||||
|
|
||||||
/// Window class
|
/// Window class
|
||||||
#[serde(deserialize_with = "from_string_or_deserialize")]
|
#[serde(deserialize_with = "from_string_or_deserialize")]
|
||||||
|
@ -56,6 +56,10 @@ pub struct WindowConfig {
|
||||||
pub start_maximized: Option<bool>,
|
pub start_maximized: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn default_title() -> String {
|
||||||
|
DEFAULT_NAME.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
impl WindowConfig {
|
impl WindowConfig {
|
||||||
pub fn startup_mode(&self) -> StartupMode {
|
pub fn startup_mode(&self) -> StartupMode {
|
||||||
match self.start_maximized {
|
match self.start_maximized {
|
||||||
|
|
|
@ -27,7 +27,7 @@ use crate::ansi::{
|
||||||
self, Attr, CharsetIndex, Color, CursorStyle, Handler, NamedColor, StandardCharset, TermInfo,
|
self, Attr, CharsetIndex, Color, CursorStyle, Handler, NamedColor, StandardCharset, TermInfo,
|
||||||
};
|
};
|
||||||
use crate::clipboard::{Clipboard, ClipboardType};
|
use crate::clipboard::{Clipboard, ClipboardType};
|
||||||
use crate::config::{Config, VisualBellAnimation};
|
use crate::config::{Config, VisualBellAnimation, DEFAULT_NAME};
|
||||||
use crate::cursor::CursorKey;
|
use crate::cursor::CursorKey;
|
||||||
use crate::event::{Event, EventListener};
|
use crate::event::{Event, EventListener};
|
||||||
use crate::grid::{
|
use crate::grid::{
|
||||||
|
@ -47,6 +47,9 @@ pub mod color;
|
||||||
/// Used to match equal brackets, when performing a bracket-pair selection.
|
/// Used to match equal brackets, when performing a bracket-pair selection.
|
||||||
const BRACKET_PAIRS: [(char, char); 4] = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')];
|
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
|
/// A type that can expand a given point to a region
|
||||||
///
|
///
|
||||||
/// Usually this is implemented for some 2-D array type since
|
/// Usually this is implemented for some 2-D array type since
|
||||||
|
@ -760,6 +763,13 @@ pub struct Term<T> {
|
||||||
|
|
||||||
/// Terminal focus
|
/// Terminal focus
|
||||||
pub is_focused: bool,
|
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<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Terminal size info
|
/// Terminal size info
|
||||||
|
@ -887,6 +897,8 @@ impl<T> Term<T> {
|
||||||
clipboard,
|
clipboard,
|
||||||
event_proxy,
|
event_proxy,
|
||||||
is_focused: true,
|
is_focused: true,
|
||||||
|
title: config.window.title.clone(),
|
||||||
|
title_stack: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1322,6 +1334,9 @@ impl<T: EventListener> ansi::Handler for Term<T> {
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
fn set_title(&mut self, title: &str) {
|
fn set_title(&mut self, title: &str) {
|
||||||
if self.dynamic_title {
|
if self.dynamic_title {
|
||||||
|
trace!("Setting window title to '{}'", title);
|
||||||
|
|
||||||
|
self.title = title.into();
|
||||||
self.event_proxy.send_event(Event::Title(title.to_owned()));
|
self.event_proxy.send_event(Event::Title(title.to_owned()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1336,13 +1351,16 @@ impl<T: EventListener> ansi::Handler for Term<T> {
|
||||||
//
|
//
|
||||||
// The starts_with check is necessary because other shells e.g. bash set a
|
// The starts_with check is necessary because other shells e.g. bash set a
|
||||||
// different title and don't need Alacritty prepended.
|
// different title and don't need Alacritty prepended.
|
||||||
|
trace!("Setting window title to '{}'", title);
|
||||||
|
|
||||||
let title = if !tty::is_conpty() && title.starts_with(' ') {
|
let title = if !tty::is_conpty() && title.starts_with(' ') {
|
||||||
format!("Alacritty {}", title.trim())
|
format!("Alacritty {}", title.trim())
|
||||||
} else {
|
} else {
|
||||||
title.to_owned()
|
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<T: EventListener> ansi::Handler for Term<T> {
|
||||||
self.grid.reset(&Cell::default());
|
self.grid.reset(&Cell::default());
|
||||||
self.alt_grid.reset(&Cell::default());
|
self.alt_grid.reset(&Cell::default());
|
||||||
self.scroll_region = Line(0)..self.grid.num_lines();
|
self.scroll_region = Line(0)..self.grid.num_lines();
|
||||||
|
self.title = DEFAULT_NAME.to_string();
|
||||||
|
self.title_stack.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
@ -2089,6 +2109,31 @@ impl<T: EventListener> ansi::Handler for Term<T> {
|
||||||
trace!("Setting cursor style {:?}", style);
|
trace!("Setting cursor style {:?}", style);
|
||||||
self.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 {
|
struct TabStops {
|
||||||
|
@ -2301,6 +2346,57 @@ mod tests {
|
||||||
scrolled_grid.scroll_display(Scroll::Top);
|
scrolled_grid.scroll_display(Scroll::Top);
|
||||||
assert_eq!(term.grid, scrolled_grid);
|
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"))]
|
#[cfg(all(test, feature = "bench"))]
|
||||||
|
|
|
@ -143,7 +143,7 @@ pub fn new<'a, C>(
|
||||||
|
|
||||||
let mut startup_info_ex: STARTUPINFOEXW = Default::default();
|
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();
|
let title = U16CString::from_str(title).unwrap();
|
||||||
startup_info_ex.StartupInfo.lpTitle = title.as_ptr() as LPWSTR;
|
startup_info_ex.StartupInfo.lpTitle = title.as_ptr() as LPWSTR;
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue