Add synchronized output/update (#756)

This commit is contained in:
Jonathan Dickinson 2023-02-26 10:40:13 -05:00 committed by GitHub
parent e7fc698f24
commit e065a56536
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 223 additions and 1 deletions

View File

@ -27,6 +27,7 @@ Available tests:
2. color (foreground, background) 2. color (foreground, background)
3. attributes (bold, italic, ...) 3. attributes (bold, italic, ...)
4. input 4. input
5. synchronized output
Select test to run ('1', '2', ...) or hit 'q' to quit. Select test to run ('1', '2', ...) or hit 'q' to quit.
"#; "#;
@ -59,6 +60,7 @@ where
'2' => test::color::run(w)?, '2' => test::color::run(w)?,
'3' => test::attribute::run(w)?, '3' => test::attribute::run(w)?,
'4' => test::event::run(w)?, '4' => test::event::run(w)?,
'5' => test::synchronized_output::run(w)?,
'q' => break, 'q' => break,
_ => {} _ => {}
}; };

View File

@ -2,3 +2,4 @@ pub mod attribute;
pub mod color; pub mod color;
pub mod cursor; pub mod cursor;
pub mod event; pub mod event;
pub mod synchronized_output;

View File

@ -0,0 +1,43 @@
use std::io::Write;
use crossterm::{cursor, execute, style::Print, SynchronizedUpdate};
use crate::Result;
fn render_slowly<W>(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>(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>(w: &mut W) -> Result<()>
where
W: Write,
{
run_tests!(w, test_slow_rendering,);
Ok(())
}

View File

@ -1,6 +1,8 @@
use std::fmt; use std::fmt;
use std::io::{self, Write}; use std::io::{self, Write};
use crate::terminal::{BeginSynchronizedUpdate, EndSynchronizedUpdate};
use super::error::Result; use super::error::Result;
/// An interface for a command that performs an action on the terminal. /// An interface for a command that performs an action on the terminal.
@ -184,6 +186,74 @@ impl<T: Write + ?Sized> 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<T>(&mut self, operations: impl FnOnce(&mut Self) -> T) -> Result<T>;
}
impl<W: std::io::Write + ?Sized> 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<T>(&mut self, operations: impl FnOnce(&mut Self) -> T) -> Result<T> {
self.queue(BeginSynchronizedUpdate)?;
let result = operations(self);
self.execute(EndSynchronizedUpdate)?;
Ok(result)
}
}
/// Writes the ANSI representation of a command to the given writer. /// Writes the ANSI representation of a command to the given writer.
fn write_command_ansi<C: Command>( fn write_command_ansi<C: Command>(
io: &mut (impl io::Write + ?Sized), io: &mut (impl io::Write + ?Sized),

View File

@ -235,7 +235,7 @@
//! [flush]: https://doc.rust-lang.org/std/io/trait.Write.html#tymethod.flush //! [flush]: https://doc.rust-lang.org/std/io/trait.Write.html#tymethod.flush
pub use crate::{ pub use crate::{
command::{Command, ExecutableCommand, QueueableCommand}, command::{Command, ExecutableCommand, QueueableCommand, SynchronizedUpdate},
error::{ErrorKind, Result}, error::{ErrorKind, Result},
}; };

View File

@ -378,6 +378,112 @@ impl<T: fmt::Display> Command for SetTitle<T> {
} }
} }
/// 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 ScrollUp);
impl_display!(for ScrollDown); impl_display!(for ScrollDown);
impl_display!(for SetSize); impl_display!(for SetSize);