Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add synchronized output/update #756

Merged
merged 4 commits into from
Feb 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
jcdickinson marked this conversation as resolved.
Show resolved Hide resolved
///
/// # 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