diff --git a/CHANGELOG.md b/CHANGELOG.md index f0fa7e32..37eb741f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - IME composition preview not appearing on Windows +- Synchronized terminal updates using `DCS = 1 s ST`/`DCS = 2 s ST` ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 43e30741..e3d25717 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -75,7 +75,7 @@ dependencies = [ "mio-anonymous-pipes", "mio-extras", "miow 0.3.6", - "nix", + "nix 0.19.1", "parking_lot", "regex-automata", "serde", @@ -192,14 +192,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b036167e76041694579972c28cf4877b4f92da222560ddb49008937b6a6727c" dependencies = [ "log", - "nix", + "nix 0.18.0", ] [[package]] name = "cc" -version = "1.0.66" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" +checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" [[package]] name = "cfg-if" @@ -394,9 +394,9 @@ dependencies = [ [[package]] name = "core-text" -version = "19.1.0" +version = "19.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c7f46e8b820fd5f4b28528104b28b0a91cbe9e9c5bde8017087fb44bc93a60" +checksum = "99d74ada66e07c1cefa18f8abfba765b486f250de2e4a999e5727fc0dd4b4a25" dependencies = [ "core-foundation 0.9.1", "core-graphics 0.22.2", @@ -428,13 +428,14 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d" +checksum = "bae8f328835f8f5a6ceb6a7842a7f2d0c03692adb5c889347235d59194731fe3" dependencies = [ "autocfg", "cfg-if 1.0.0", "lazy_static", + "loom", ] [[package]] @@ -506,11 +507,10 @@ dependencies = [ [[package]] name = "dirs" -version = "2.0.2" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" +checksum = "142995ed02755914747cc6ca76fc7e4583cd18578746716d0508ea6ed558b9ff" dependencies = [ - "cfg-if 0.1.10", "dirs-sys", ] @@ -568,9 +568,9 @@ dependencies = [ [[package]] name = "embed-resource" -version = "1.5.1" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bd6a41d89e233bcd6978fe7333191a2054d518d105a1165ada1d2ebc445ce98" +checksum = "04e03c3dae04b8f252f2866d25e0ec8f2f488efba43bc6e307274b65c4321f94" dependencies = [ "cc", "vswhom", @@ -595,7 +595,7 @@ checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall 0.2.4", + "redox_syscall 0.2.5", "winapi 0.3.9", ] @@ -704,6 +704,19 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" +[[package]] +name = "generator" +version = "0.6.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9fed24fd1e18827652b4d55652899a1e9da8e54d91624dc3437a5bc3a9f9a9c" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "winapi 0.3.9", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -938,6 +951,17 @@ dependencies = [ "serde", ] +[[package]] +name = "loom" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d44c73b4636e497b4917eb21c33539efa3816741a2d3ff26c6316f1b529481a4" +dependencies = [ + "cfg-if 1.0.0", + "generator", + "scoped-tls", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -1111,10 +1135,22 @@ dependencies = [ ] [[package]] -name = "nom" -version = "6.1.0" +name = "nix" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab6f70b46d6325aa300f1c7bb3d470127dfc27806d8ea6bf294ee0ce643ce2b1" +checksum = "b2ccba0cfe4fdf15982d1674c69b1fd80bad427d293849982668dfe454bd61f2" +dependencies = [ + "bitflags", + "cc", + "cfg-if 1.0.0", + "libc", +] + +[[package]] +name = "nom" +version = "6.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" dependencies = [ "memchr", "version_check", @@ -1233,7 +1269,7 @@ dependencies = [ "cfg-if 1.0.0", "instant", "libc", - "redox_syscall 0.2.4", + "redox_syscall 0.2.5", "smallvec", "winapi 0.3.9", ] @@ -1305,9 +1341,9 @@ checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" [[package]] name = "redox_syscall" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ec8ca9416c5ea37062b502703cd7fcb207736bc294f6e0cf367ac6fc234570" +checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" dependencies = [ "bitflags", ] @@ -1361,6 +1397,12 @@ dependencies = [ "owned_ttf_parser", ] +[[package]] +name = "rustversion" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5d2a036dc6d2d8fd16fde3498b04306e29bd193bf306a57427019b823d5acd" + [[package]] name = "ryu" version = "1.0.5" @@ -1507,7 +1549,7 @@ dependencies = [ "lazy_static", "log", "memmap2", - "nix", + "nix 0.18.0", "wayland-client", "wayland-cursor", "wayland-protocols", @@ -1574,18 +1616,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76cc616c6abf8c8928e2fdcc0dbfab37175edd8fb49a4641066ad1364fdab146" +checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be73a2caec27583d0046ef3796c3794f868a5bc813db689eed00c7631275cd1" +checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" dependencies = [ "proc-macro2", "quote", @@ -1726,7 +1768,7 @@ dependencies = [ "bitflags", "downcast-rs", "libc", - "nix", + "nix 0.18.0", "scoped-tls", "wayland-commons", "wayland-scanner", @@ -1739,7 +1781,7 @@ version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "480450f76717edd64ad04a4426280d737fc3d10a236b982df7b1aee19f0e2d56" dependencies = [ - "nix", + "nix 0.18.0", "once_cell", "smallvec", "wayland-sys", @@ -1751,7 +1793,7 @@ version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6eb122c160223a7660feeaf949d0100281d1279acaaed3720eb3c9894496e5f" dependencies = [ - "nix", + "nix 0.18.0", "wayland-client", "xcursor", ] diff --git a/alacritty/Cargo.toml b/alacritty/Cargo.toml index 923659d2..febfbdc7 100644 --- a/alacritty/Cargo.toml +++ b/alacritty/Cargo.toml @@ -34,7 +34,7 @@ copypasta = { version = "0.7.0", default-features = false } libc = "0.2" unicode-width = "0.1" bitflags = "1" -dirs = "2.0.2" +dirs = "3.0.1" [build-dependencies] gl_generator = "0.14.0" diff --git a/alacritty_terminal/Cargo.toml b/alacritty_terminal/Cargo.toml index 41b8b1f8..9fae1557 100644 --- a/alacritty_terminal/Cargo.toml +++ b/alacritty_terminal/Cargo.toml @@ -25,10 +25,10 @@ log = "0.4" unicode-width = "0.1" base64 = "0.12.0" regex-automata = "0.1.9" -dirs = "2.0.2" +dirs = "3.0.1" [target.'cfg(unix)'.dependencies] -nix = "0.18.0" +nix = "0.19.0" signal-hook = { version = "0.1", features = ["mio-support"] } [target.'cfg(windows)'.dependencies] diff --git a/alacritty_terminal/src/ansi.rs b/alacritty_terminal/src/ansi.rs index 59877c14..b46aec0b 100644 --- a/alacritty_terminal/src/ansi.rs +++ b/alacritty_terminal/src/ansi.rs @@ -1,6 +1,7 @@ //! ANSI Terminal Stream Parsing. use std::convert::TryFrom; +use std::time::{Duration, Instant}; use std::{io, iter, str}; use log::{debug, trace}; @@ -12,6 +13,21 @@ use alacritty_config_derive::ConfigDeserialize; use crate::index::{Column, Line}; use crate::term::color::Rgb; +/// Maximum time before a synchronized update is aborted. +const SYNC_UPDATE_TIMEOUT: Duration = Duration::from_millis(150); + +/// Maximum number of bytes read in one synchronized update (2MiB). +const SYNC_BUFFER_SIZE: usize = 0x20_0000; + +/// Number of bytes in the synchronized update DCS sequence before the passthrough parameters. +const SYNC_ESCAPE_START_LEN: usize = 5; + +/// Start of the DCS sequence for beginning synchronized updates. +const SYNC_START_ESCAPE_START: [u8; SYNC_ESCAPE_START_LEN] = [b'\x1b', b'P', b'=', b'1', b's']; + +/// Start of the DCS sequence for terminating synchronized updates. +const SYNC_END_ESCAPE_START: [u8; SYNC_ESCAPE_START_LEN] = [b'\x1b', b'P', b'=', b'2', b's']; + /// Parse colors in XParseColor format. fn xparse_color(color: &[u8]) -> Option { if !color.is_empty() && color[0] == b'#' { @@ -81,15 +97,157 @@ fn parse_number(input: &[u8]) -> Option { Some(num) } +/// Internal state for VTE processor. +#[derive(Debug, Default)] +struct ProcessorState { + /// Last processed character for repetition. + preceding_char: Option, + + /// DCS sequence waiting for termination. + dcs: Option, + + /// State for synchronized terminal updates. + sync_state: SyncState, +} + +#[derive(Debug)] +struct SyncState { + /// Expiration time of the synchronized update. + timeout: Option, + + /// Sync DCS waiting for termination sequence. + pending_dcs: Option, + + /// Bytes read during the synchronized update. + buffer: Vec, +} + +impl Default for SyncState { + fn default() -> Self { + Self { buffer: Vec::with_capacity(SYNC_BUFFER_SIZE), pending_dcs: None, timeout: None } + } +} + +/// Pending DCS sequence. +#[derive(Debug)] +enum Dcs { + /// Begin of the synchronized update. + SyncStart, + + /// End of the synchronized update. + SyncEnd, +} + /// The processor wraps a `vte::Parser` to ultimately call methods on a Handler. +#[derive(Default)] pub struct Processor { state: ProcessorState, parser: vte::Parser, } -/// Internal state for VTE processor. -struct ProcessorState { - preceding_char: Option, +impl Processor { + #[inline] + pub fn new() -> Self { + Self::default() + } + + /// Process a new byte from the PTY. + #[inline] + pub fn advance(&mut self, handler: &mut H, byte: u8, writer: &mut W) + where + H: Handler, + W: io::Write, + { + if self.state.sync_state.timeout.is_none() { + let mut performer = Performer::new(&mut self.state, handler, writer); + self.parser.advance(&mut performer, byte); + } else { + self.advance_sync(handler, byte, writer); + } + } + + /// End a synchronized update. + pub fn stop_sync(&mut self, handler: &mut H, writer: &mut W) + where + H: Handler, + W: io::Write, + { + // Process all synchronized bytes. + for i in 0..self.state.sync_state.buffer.len() { + let byte = self.state.sync_state.buffer[i]; + let mut performer = Performer::new(&mut self.state, handler, writer); + self.parser.advance(&mut performer, byte); + } + + // Resetting state after processing makes sure we don't interpret buffered sync escapes. + self.state.sync_state.buffer.clear(); + self.state.sync_state.timeout = None; + } + + /// Synchronized update expiration time. + #[inline] + pub fn sync_timeout(&self) -> Option<&Instant> { + self.state.sync_state.timeout.as_ref() + } + + /// Number of bytes in the synchronization buffer. + #[inline] + pub fn sync_bytes_count(&self) -> usize { + self.state.sync_state.buffer.len() + } + + /// Process a new byte during a synchronized update. + #[cold] + fn advance_sync(&mut self, handler: &mut H, byte: u8, writer: &mut W) + where + H: Handler, + W: io::Write, + { + self.state.sync_state.buffer.push(byte); + + // Handle sync DCS escape sequences. + match self.state.sync_state.pending_dcs { + Some(_) => self.advance_sync_dcs_end(handler, byte, writer), + None => self.advance_sync_dcs_start(), + } + } + + /// Find the start of sync DCS sequences. + fn advance_sync_dcs_start(&mut self) { + // Get the last few bytes for comparison. + let len = self.state.sync_state.buffer.len(); + let offset = len.saturating_sub(SYNC_ESCAPE_START_LEN); + let end = &self.state.sync_state.buffer[offset..]; + + // Check for extension/termination of the synchronized update. + if end == SYNC_START_ESCAPE_START { + self.state.sync_state.pending_dcs = Some(Dcs::SyncStart); + } else if end == SYNC_END_ESCAPE_START || len >= SYNC_BUFFER_SIZE - 1 { + self.state.sync_state.pending_dcs = Some(Dcs::SyncEnd); + } + } + + /// Parse the DCS termination sequence for synchronized updates. + fn advance_sync_dcs_end(&mut self, handler: &mut H, byte: u8, writer: &mut W) + where + H: Handler, + W: io::Write, + { + match byte { + // Ignore DCS passthrough characters. + 0x00..=0x17 | 0x19 | 0x1c..=0x7f | 0xa0..=0xff => (), + // Cancel the DCS sequence. + 0x18 | 0x1a | 0x80..=0x9f => self.state.sync_state.pending_dcs = None, + // Dispatch on ESC. + 0x1b => match self.state.sync_state.pending_dcs.take() { + Some(Dcs::SyncStart) => { + self.state.sync_state.timeout = Some(Instant::now() + SYNC_UPDATE_TIMEOUT); + }, + Some(Dcs::SyncEnd) => self.stop_sync(handler, writer), + None => (), + }, + } + } } /// Helper type that implements `vte::Perform`. @@ -114,28 +272,6 @@ impl<'a, H: Handler + 'a, W: io::Write> Performer<'a, H, W> { } } -impl Default for Processor { - fn default() -> Processor { - Processor { state: ProcessorState { preceding_char: None }, parser: vte::Parser::new() } - } -} - -impl Processor { - pub fn new() -> Processor { - Default::default() - } - - #[inline] - pub fn advance(&mut self, handler: &mut H, byte: u8, writer: &mut W) - where - H: Handler, - W: io::Write, - { - let mut performer = Performer::new(&mut self.state, handler, writer); - self.parser.advance(&mut performer, byte); - } -} - /// Type that handles actions from the parser. /// /// XXX Should probably not provide default impls for everything, but it makes @@ -172,8 +308,6 @@ pub trait Handler { fn move_down(&mut self, _: Line) {} /// Identify the terminal (should write back to the pty stream). - /// - /// TODO this should probably return an io::Result fn identify_terminal(&mut self, _: &mut W, _intermediate: Option) {} /// Report device status. @@ -428,8 +562,6 @@ pub enum Mode { impl Mode { /// Create mode from a primitive. - /// - /// TODO lots of unhandled values. pub fn from_primitive(intermediate: Option<&u8>, num: u16) -> Option { let private = match intermediate { Some(b'?') => true, @@ -779,10 +911,18 @@ where #[inline] fn hook(&mut self, params: &Params, intermediates: &[u8], ignore: bool, action: char) { - debug!( - "[unhandled hook] params={:?}, ints: {:?}, ignore: {:?}, action: {:?}", - params, intermediates, ignore, action - ); + match (action, intermediates) { + ('s', [b'=']) => { + // Start a synchronized update. The end is handled with a separate parser. + if params.iter().next().map_or(false, |param| param[0] == 1) { + self.state.dcs = Some(Dcs::SyncStart); + } + }, + _ => debug!( + "[unhandled hook] params={:?}, ints: {:?}, ignore: {:?}, action: {:?}", + params, intermediates, ignore, action + ), + } } #[inline] @@ -792,10 +932,15 @@ where #[inline] fn unhook(&mut self) { - debug!("[unhandled unhook]"); + match self.state.dcs { + Some(Dcs::SyncStart) => { + self.state.sync_state.timeout = Some(Instant::now() + SYNC_UPDATE_TIMEOUT); + }, + Some(Dcs::SyncEnd) => (), + _ => debug!("[unhandled unhook]"), + } } - // TODO replace OSC parsing with parser combinators. #[inline] fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) { let terminator = if bell_terminated { "\x07" } else { "\x1b\\" }; @@ -1172,6 +1317,7 @@ where } } +#[inline] fn attrs_from_sgr_parameters(params: &mut ParamsIter<'_>) -> Vec> { let mut attrs = Vec::with_capacity(params.size_hint().0); diff --git a/alacritty_terminal/src/event_loop.rs b/alacritty_terminal/src/event_loop.rs index 8a6441ce..09c71668 100644 --- a/alacritty_terminal/src/event_loop.rs +++ b/alacritty_terminal/src/event_loop.rs @@ -7,6 +7,7 @@ use std::io::{self, ErrorKind, Read, Write}; use std::marker::Send; use std::sync::Arc; use std::thread::JoinHandle; +use std::time::Instant; use log::error; #[cfg(not(windows))] @@ -58,16 +59,6 @@ struct Writing { written: usize, } -/// All of the mutable state needed to run the event loop. -/// -/// Contains list of items to write, current write state, etc. Anything that -/// would otherwise be mutated on the `EventLoop` goes here. -pub struct State { - write_list: VecDeque>, - writing: Option, - parser: ansi::Processor, -} - pub struct Notifier(pub Sender); impl event::Notify for Notifier { @@ -91,10 +82,15 @@ impl event::OnResize for Notifier { } } -impl Default for State { - fn default() -> State { - State { write_list: VecDeque::new(), parser: ansi::Processor::new(), writing: None } - } +/// All of the mutable state needed to run the event loop. +/// +/// Contains list of items to write, current write state, etc. Anything that +/// would otherwise be mutated on the `EventLoop` goes here. +#[derive(Default)] +pub struct State { + write_list: VecDeque>, + writing: Option, + parser: ansi::Processor, } impl State { @@ -261,8 +257,8 @@ where } } - if processed > 0 { - // Queue terminal redraw. + // Queue terminal redraw unless all processed bytes were synchronized. + if state.parser.sync_bytes_count() < processed && processed > 0 { self.event_proxy.send_event(Event::Wakeup); } @@ -325,13 +321,24 @@ where }; 'event_loop: loop { - if let Err(err) = self.poll.poll(&mut events, None) { + // Wakeup the event loop when a synchronized update timeout was reached. + let sync_timeout = state.parser.sync_timeout(); + let timeout = sync_timeout.map(|st| st.saturating_duration_since(Instant::now())); + + if let Err(err) = self.poll.poll(&mut events, timeout) { match err.kind() { ErrorKind::Interrupted => continue, _ => panic!("EventLoop polling error: {:?}", err), } } + // Handle synchronized update timeout. + if events.is_empty() { + state.parser.stop_sync(&mut *self.terminal.lock(), &mut self.pty.writer()); + self.event_proxy.send_event(Event::Wakeup); + continue; + } + for event in events.iter() { match event.token() { token if token == channel_token => {