From e065a56536ce88e9d857ed8b39bda8f69130f905 Mon Sep 17 00:00:00 2001 From: Jonathan Dickinson Date: Sun, 26 Feb 2023 10:40:13 -0500 Subject: [PATCH] Add synchronized output/update (#756) --- examples/interactive-demo/src/main.rs | 2 + examples/interactive-demo/src/test.rs | 1 + .../src/test/synchronized_output.rs | 43 +++++++ src/command.rs | 70 ++++++++++++ src/lib.rs | 2 +- src/terminal.rs | 106 ++++++++++++++++++ 6 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 examples/interactive-demo/src/test/synchronized_output.rs diff --git a/examples/interactive-demo/src/main.rs b/examples/interactive-demo/src/main.rs index 43ebf6807..2dd3105a6 100644 --- a/examples/interactive-demo/src/main.rs +++ b/examples/interactive-demo/src/main.rs @@ -27,6 +27,7 @@ Available tests: 2. color (foreground, background) 3. attributes (bold, italic, ...) 4. input +5. synchronized output Select test to run ('1', '2', ...) or hit 'q' to quit. "#; @@ -59,6 +60,7 @@ where '2' => test::color::run(w)?, '3' => test::attribute::run(w)?, '4' => test::event::run(w)?, + '5' => test::synchronized_output::run(w)?, 'q' => break, _ => {} }; diff --git a/examples/interactive-demo/src/test.rs b/examples/interactive-demo/src/test.rs index 89f1140b7..1db95a210 100644 --- a/examples/interactive-demo/src/test.rs +++ b/examples/interactive-demo/src/test.rs @@ -2,3 +2,4 @@ pub mod attribute; pub mod color; pub mod cursor; pub mod event; +pub mod synchronized_output; diff --git a/examples/interactive-demo/src/test/synchronized_output.rs b/examples/interactive-demo/src/test/synchronized_output.rs new file mode 100644 index 000000000..77d7c2a7f --- /dev/null +++ b/examples/interactive-demo/src/test/synchronized_output.rs @@ -0,0 +1,43 @@ +use std::io::Write; + +use crossterm::{cursor, execute, style::Print, SynchronizedUpdate}; + +use crate::Result; + +fn render_slowly(w: &mut W) -> Result<()> +where + W: Write, +{ + for i in 1..10 { + execute!(w, Print(format!("{}", i)))?; + std::thread::sleep(std::time::Duration::from_millis(50)); + } + Ok(()) +} + +fn test_slow_rendering(w: &mut W) -> Result<()> +where + W: Write, +{ + execute!(w, Print("Rendering without synchronized update:"))?; + execute!(w, cursor::MoveToNextLine(1))?; + std::thread::sleep(std::time::Duration::from_millis(50)); + render_slowly(w)?; + + execute!(w, cursor::MoveToNextLine(1))?; + execute!(w, Print("Rendering with synchronized update:"))?; + execute!(w, cursor::MoveToNextLine(1))?; + std::thread::sleep(std::time::Duration::from_millis(50)); + w.sync_update(render_slowly)??; + + execute!(w, cursor::MoveToNextLine(1))?; + Ok(()) +} + +pub fn run(w: &mut W) -> Result<()> +where + W: Write, +{ + run_tests!(w, test_slow_rendering,); + Ok(()) +} diff --git a/src/command.rs b/src/command.rs index e3cec4bbf..5482475d1 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,6 +1,8 @@ use std::fmt; use std::io::{self, Write}; +use crate::terminal::{BeginSynchronizedUpdate, EndSynchronizedUpdate}; + use super::error::Result; /// An interface for a command that performs an action on the terminal. @@ -184,6 +186,74 @@ impl ExecutableCommand for T { } } +/// An interface for types that support synchronized updates. +pub trait SynchronizedUpdate { + /// Performs a set of actions against the given type. + fn sync_update(&mut self, operations: impl FnOnce(&mut Self) -> T) -> Result; +} + +impl SynchronizedUpdate for W { + /// Performs a set of actions within a synchronous update. + /// + /// Updates will be suspended in the terminal, the function will be executed against self, + /// updates will be resumed, and a flush will be performed. + /// + /// # Arguments + /// + /// - Function + /// + /// A function that performs the operations that must execute in a synchronized update. + /// + /// # Examples + /// + /// ```rust + /// use std::io::{Write, stdout}; + /// + /// use crossterm::{Result, ExecutableCommand, SynchronizedUpdate, style::Print}; + /// + /// fn main() -> Result<()> { + /// let mut stdout = stdout(); + /// + /// stdout.sync_update(|stdout| { + /// stdout.execute(Print("foo 1\n".to_string()))?; + /// stdout.execute(Print("foo 2".to_string()))?; + /// // The effects of the print command will not be present in the terminal + /// // buffer, but not visible in the terminal. + /// crossterm::Result::Ok(()) + /// })?; + /// + /// // The effects of the commands will be visible. + /// + /// Ok(()) + /// + /// // ==== Output ==== + /// // foo 1 + /// // foo 2 + /// } + /// ``` + /// + /// # Notes + /// + /// This command is performed only using ANSI codes, and will do nothing on terminals that do not support ANSI + /// codes, or this specific extension. + /// + /// When rendering the screen of the terminal, the Emulator usually iterates through each visible grid cell and + /// renders its current state. With applications updating the screen a at higher frequency this can cause tearing. + /// + /// This mode attempts to mitigate that. + /// + /// When the synchronization mode is enabled following render calls will keep rendering the last rendered state. + /// The terminal Emulator keeps processing incoming text and sequences. When the synchronized update mode is disabled + /// again the renderer may fetch the latest screen buffer state again, effectively avoiding the tearing effect + /// by unintentionally rendering in the middle a of an application screen update. + /// + fn sync_update(&mut self, operations: impl FnOnce(&mut Self) -> T) -> Result { + self.queue(BeginSynchronizedUpdate)?; + let result = operations(self); + self.execute(EndSynchronizedUpdate)?; + Ok(result) + } +} /// Writes the ANSI representation of a command to the given writer. fn write_command_ansi( io: &mut (impl io::Write + ?Sized), diff --git a/src/lib.rs b/src/lib.rs index 2a2fc6c6a..0f6bcc1a3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -235,7 +235,7 @@ //! [flush]: https://doc.rust-lang.org/std/io/trait.Write.html#tymethod.flush pub use crate::{ - command::{Command, ExecutableCommand, QueueableCommand}, + command::{Command, ExecutableCommand, QueueableCommand, SynchronizedUpdate}, error::{ErrorKind, Result}, }; diff --git a/src/terminal.rs b/src/terminal.rs index ecc310308..282454dfe 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -378,6 +378,112 @@ impl Command for SetTitle { } } +/// A command that instructs the terminal emulator to being a synchronized frame. +/// +/// # Notes +/// +/// * Commands must be executed/queued for execution otherwise they do nothing. +/// * Use [EndSynchronizedUpdate](./struct.EndSynchronizedUpdate.html) command to leave the entered alternate screen. +/// +/// When rendering the screen of the terminal, the Emulator usually iterates through each visible grid cell and +/// renders its current state. With applications updating the screen a at higher frequency this can cause tearing. +/// +/// This mode attempts to mitigate that. +/// +/// When the synchronization mode is enabled following render calls will keep rendering the last rendered state. +/// The terminal Emulator keeps processing incoming text and sequences. When the synchronized update mode is disabled +/// again the renderer may fetch the latest screen buffer state again, effectively avoiding the tearing effect +/// by unintentionally rendering in the middle a of an application screen update. +/// +/// # Examples +/// +/// ```no_run +/// use std::io::{stdout, Write}; +/// use crossterm::{execute, Result, terminal::{BeginSynchronizedUpdate, EndSynchronizedUpdate}}; +/// +/// fn main() -> Result<()> { +/// execute!(stdout(), BeginSynchronizedUpdate)?; +/// +/// // Anything performed here will not be rendered until EndSynchronizedUpdate is called. +/// +/// execute!(stdout(), EndSynchronizedUpdate)?; +/// Ok(()) +/// } +/// ``` +/// +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BeginSynchronizedUpdate; + +impl Command for BeginSynchronizedUpdate { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + f.write_str(csi!("?2026h")) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> Result<()> { + Ok(()) + } + + #[cfg(windows)] + #[inline] + fn is_ansi_code_supported(&self) -> bool { + true + } +} + +/// A command that instructs the terminal to end a synchronized frame. +/// +/// # Notes +/// +/// * Commands must be executed/queued for execution otherwise they do nothing. +/// * Use [BeginSynchronizedUpdate](./struct.BeginSynchronizedUpdate.html) to enter the alternate screen. +/// +/// When rendering the screen of the terminal, the Emulator usually iterates through each visible grid cell and +/// renders its current state. With applications updating the screen a at higher frequency this can cause tearing. +/// +/// This mode attempts to mitigate that. +/// +/// When the synchronization mode is enabled following render calls will keep rendering the last rendered state. +/// The terminal Emulator keeps processing incoming text and sequences. When the synchronized update mode is disabled +/// again the renderer may fetch the latest screen buffer state again, effectively avoiding the tearing effect +/// by unintentionally rendering in the middle a of an application screen update. +/// +/// # Examples +/// +/// ```no_run +/// use std::io::{stdout, Write}; +/// use crossterm::{execute, Result, terminal::{BeginSynchronizedUpdate, EndSynchronizedUpdate}}; +/// +/// fn main() -> Result<()> { +/// execute!(stdout(), BeginSynchronizedUpdate)?; +/// +/// // Anything performed here will not be rendered until EndSynchronizedUpdate is called. +/// +/// execute!(stdout(), EndSynchronizedUpdate)?; +/// Ok(()) +/// } +/// ``` +/// +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EndSynchronizedUpdate; + +impl Command for EndSynchronizedUpdate { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + f.write_str(csi!("?2026l")) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> Result<()> { + Ok(()) + } + + #[cfg(windows)] + #[inline] + fn is_ansi_code_supported(&self) -> bool { + true + } +} + impl_display!(for ScrollUp); impl_display!(for ScrollDown); impl_display!(for SetSize);