Skip to content

Commit

Permalink
Add synchronized output/update (#756)
Browse files Browse the repository at this point in the history
  • Loading branch information
jcdickinson authored Feb 26, 2023
1 parent e7fc698 commit e065a56
Show file tree
Hide file tree
Showing 6 changed files with 223 additions and 1 deletion.
2 changes: 2 additions & 0 deletions examples/interactive-demo/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"#;
Expand Down Expand Up @@ -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,
_ => {}
};
Expand Down
1 change: 1 addition & 0 deletions examples/interactive-demo/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ pub mod attribute;
pub mod color;
pub mod cursor;
pub mod event;
pub mod synchronized_output;
43 changes: 43 additions & 0 deletions examples/interactive-demo/src/test/synchronized_output.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
70 changes: 70 additions & 0 deletions src/command.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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.
fn write_command_ansi<C: Command>(
io: &mut (impl io::Write + ?Sized),
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
};

Expand Down
106 changes: 106 additions & 0 deletions src/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ScrollDown);
impl_display!(for SetSize);
Expand Down

0 comments on commit e065a56

Please sign in to comment.