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 1 commit
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(())
}
58 changes: 58 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,62 @@ 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.
/// })
///
/// // 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.
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
85 changes: 85 additions & 0 deletions src/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,91 @@ 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.
///
/// # 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)?;
/// }
/// ```
///
#[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
}
}

///
jcdickinson marked this conversation as resolved.
Show resolved Hide resolved
/// 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.
///
/// # 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)?;
/// }
/// ```
///
#[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