src/terminal.rs
Rama main · réplica de lectura del repositorio original.
//! Optional synchronized frames, using only capabilities advertised by the
//! current terminal. No stdin probes, terminal-name guesses or passthrough.
use std::io::{self, Write};
/// A terminal-configured audible or visual bell. Kept separate from frame
/// rendering so tests and redraws never play it accidentally.
pub fn bell(writer: &mut impl Write) -> io::Result<()> {
writer.write_all(b"\x07")?;
writer.flush()
}
const BEGIN: &[u8] = b"\x1b[?2026h";
const END: &[u8] = b"\x1b[?2026l";
/// The Sync terminfo extension advertises the actual sequence. Missing,
/// older, or non-DEC capabilities keep the ordinary renderer, including on
/// platforms without terminfo. Inside tmux this consults tmux's TERM, not
/// the outer terminal's inherited TERM_PROGRAM.
pub fn synchronized_output_supported() -> bool {
#[cfg(unix)]
{
terminfo::Database::from_env()
.ok()
.is_some_and(|db| supports_sync(&db))
}
#[cfg(not(unix))]
{
false
}
}
#[cfg(unix)]
fn supports_sync(db: &terminfo::Database) -> bool {
let Some(terminfo::capability::Value::String(sequence)) = db.raw("Sync") else {
return false;
};
terminfo::expand!(sequence.as_slice(); 1).ok().as_deref() == Some(BEGIN)
&& terminfo::expand!(sequence.as_slice(); 0).ok().as_deref() == Some(END)
}
/// End the synchronized frame on draw errors and unwinding too. Keep the
/// guard outside the draw closure, so a failed render cannot freeze output.
pub fn draw_frame<T>(
writer: &mut impl Write,
synchronized: bool,
draw: impl FnOnce() -> io::Result<T>,
) -> io::Result<T> {
let mut frame = FrameGuard {
writer,
active: synchronized,
};
if synchronized {
frame.writer.write_all(BEGIN)?;
frame.writer.flush()?;
}
let result = draw();
let end = frame.finish();
result.and_then(|value| end.map(|()| value))
}
struct FrameGuard<'a, W: Write> {
writer: &'a mut W,
active: bool,
}
impl<W: Write> FrameGuard<'_, W> {
fn finish(&mut self) -> io::Result<()> {
if self.active {
self.writer.write_all(END)?;
self.writer.flush()?;
self.active = false;
}
Ok(())
}
}
impl<W: Write> Drop for FrameGuard<'_, W> {
fn drop(&mut self) {
let _ = self.finish();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bell_is_flushed_after_the_synchronized_frame() {
#[derive(Default)]
struct Output {
bytes: Vec<u8>,
flushes: usize,
}
impl Write for Output {
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
self.bytes.extend_from_slice(bytes);
Ok(bytes.len())
}
fn flush(&mut self) -> io::Result<()> {
self.flushes += 1;
Ok(())
}
}
let mut output = Output::default();
draw_frame(&mut output, true, || Ok(())).unwrap();
let before = output.flushes;
bell(&mut output).unwrap();
assert_eq!(output.bytes, [BEGIN, END, b"\x07"].concat());
assert_eq!(output.flushes, before + 1);
}
#[test]
fn ordinary_frames_emit_no_extensions() {
let mut bytes = Vec::new();
assert_eq!(draw_frame(&mut bytes, false, || Ok(42)).unwrap(), 42);
assert!(bytes.is_empty());
}
#[test]
fn synchronized_frames_end_on_success_error_and_panic() {
for fail in [false, true] {
let mut bytes = Vec::new();
let result = draw_frame(&mut bytes, true, || {
if fail {
Err(io::Error::other("draw failed"))
} else {
Ok(())
}
});
assert_eq!(result.is_err(), fail);
assert_eq!(bytes, [BEGIN, END].concat());
}
let mut bytes = Vec::new();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _ = draw_frame::<()>(&mut bytes, true, || panic!("draw failed"));
}));
assert!(result.is_err());
assert_eq!(bytes, [BEGIN, END].concat());
}
#[cfg(unix)]
#[test]
fn only_an_advertised_dec_sync_pair_enables_frames() {
for (sequence, expected) in [
(None, false),
(Some("unrecognized"), false),
(Some("\x1b[?2026h"), false),
(Some("\x1b[?2026%?%p1%{1}%-%tl%eh%;"), true),
] {
let mut builder = terminfo::Database::new();
builder.name("alphai-test");
if let Some(sequence) = sequence {
builder.raw("Sync", sequence);
}
assert_eq!(supports_sync(&builder.build().unwrap()), expected);
}
}
}