forked from NotAShelf/beer
vt: honour synchronized output (DECSET 2026) with a present timeout
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I173dc842d89d96ea39154e1fde95be816a6a6964
This commit is contained in:
parent
f1c8271d31
commit
72044c21fd
3 changed files with 64 additions and 2 deletions
12
src/grid.rs
12
src/grid.rs
|
|
@ -151,6 +151,9 @@ pub struct Grid {
|
||||||
selection: Option<(Point, Point)>,
|
selection: Option<(Point, Point)>,
|
||||||
/// Bracketed paste mode (DECSET 2004): wrap pasted text in `ESC[200~`/`201~`.
|
/// Bracketed paste mode (DECSET 2004): wrap pasted text in `ESC[200~`/`201~`.
|
||||||
bracketed_paste: bool,
|
bracketed_paste: bool,
|
||||||
|
/// Synchronized output (DECSET 2026): hold presentation while a frame is
|
||||||
|
/// being assembled, so the screen never shows a half-drawn update.
|
||||||
|
sync: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_tabs(cols: usize) -> Vec<bool> {
|
fn default_tabs(cols: usize) -> Vec<bool> {
|
||||||
|
|
@ -185,6 +188,7 @@ impl Grid {
|
||||||
app_cursor: false,
|
app_cursor: false,
|
||||||
selection: None,
|
selection: None,
|
||||||
bracketed_paste: false,
|
bracketed_paste: false,
|
||||||
|
sync: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -882,6 +886,14 @@ impl Grid {
|
||||||
self.bracketed_paste
|
self.bracketed_paste
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_sync(&mut self, on: bool) {
|
||||||
|
self.sync = on;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sync_active(&self) -> bool {
|
||||||
|
self.sync
|
||||||
|
}
|
||||||
|
|
||||||
// --- inspection (logging + tests) ---
|
// --- inspection (logging + tests) ---
|
||||||
|
|
||||||
/// The visible text of one row, trailing blanks trimmed.
|
/// The visible text of one row, trailing blanks trimmed.
|
||||||
|
|
|
||||||
17
src/vt.rs
17
src/vt.rs
|
|
@ -180,6 +180,7 @@ impl Term {
|
||||||
(true, 1) => self.grid.set_app_cursor(on),
|
(true, 1) => self.grid.set_app_cursor(on),
|
||||||
(true, 25) => self.grid.set_cursor_visible(on),
|
(true, 25) => self.grid.set_cursor_visible(on),
|
||||||
(true, 2004) => self.grid.set_bracketed_paste(on),
|
(true, 2004) => self.grid.set_bracketed_paste(on),
|
||||||
|
(true, 2026) => self.grid.set_sync(on),
|
||||||
// App-cursor/bracketed-paste/mouse/sync modes affect input and
|
// App-cursor/bracketed-paste/mouse/sync modes affect input and
|
||||||
// rendering, which arrive with the keyboard and renderer.
|
// rendering, which arrive with the keyboard and renderer.
|
||||||
_ => tracing::trace!("unhandled mode {code} private={private} on={on}"),
|
_ => tracing::trace!("unhandled mode {code} private={private} on={on}"),
|
||||||
|
|
@ -281,6 +282,8 @@ impl Term {
|
||||||
(true, 6) => set_reset(self.grid.origin()),
|
(true, 6) => set_reset(self.grid.origin()),
|
||||||
(true, 7) => set_reset(self.grid.autowrap()),
|
(true, 7) => set_reset(self.grid.autowrap()),
|
||||||
(true, 47 | 1047 | 1049) => set_reset(self.grid.alt_active()),
|
(true, 47 | 1047 | 1049) => set_reset(self.grid.alt_active()),
|
||||||
|
(true, 2004) => set_reset(self.grid.bracketed_paste()),
|
||||||
|
(true, 2026) => set_reset(self.grid.sync_active()),
|
||||||
(false, 4) => set_reset(self.grid.insert()),
|
(false, 4) => set_reset(self.grid.insert()),
|
||||||
_ => 0,
|
_ => 0,
|
||||||
};
|
};
|
||||||
|
|
@ -713,6 +716,20 @@ mod tests {
|
||||||
assert_eq!(t.take_response(), b"\x1bP0+r6162\x1b\\");
|
assert_eq!(t.take_response(), b"\x1bP0+r6162\x1b\\");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bracketed_paste_and_sync_modes() {
|
||||||
|
let mut t = Term::new(20, 2);
|
||||||
|
feed(&mut t, b"\x1b[?2004h");
|
||||||
|
assert!(t.grid().bracketed_paste());
|
||||||
|
feed(&mut t, b"\x1b[?2004$p");
|
||||||
|
assert_eq!(t.take_response(), b"\x1b[?2004;1$y");
|
||||||
|
feed(&mut t, b"\x1b[?2026h");
|
||||||
|
assert!(t.grid().sync_active());
|
||||||
|
feed(&mut t, b"\x1b[?2026l\x1b[?2026$p");
|
||||||
|
assert!(!t.grid().sync_active());
|
||||||
|
assert_eq!(t.take_response(), b"\x1b[?2026;2$y");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn title_stack_push_pop() {
|
fn title_stack_push_pop() {
|
||||||
let mut t = Term::new(20, 4);
|
let mut t = Term::new(20, 4);
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ use std::time::Duration;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use calloop::generic::Generic;
|
use calloop::generic::Generic;
|
||||||
use calloop::timer::{TimeoutAction, Timer};
|
use calloop::timer::{TimeoutAction, Timer};
|
||||||
use calloop::{EventLoop, Interest, LoopHandle, Mode, PostAction};
|
use calloop::{EventLoop, Interest, LoopHandle, Mode, PostAction, RegistrationToken};
|
||||||
use calloop_wayland_source::WaylandSource;
|
use calloop_wayland_source::WaylandSource;
|
||||||
|
|
||||||
use crate::font::Fonts;
|
use crate::font::Fonts;
|
||||||
|
|
@ -99,6 +99,10 @@ const BLINK_MS: u64 = 500;
|
||||||
/// Buffers kept for double/triple buffering before we wait for a release.
|
/// Buffers kept for double/triple buffering before we wait for a release.
|
||||||
const MAX_BUFFERS: usize = 3;
|
const MAX_BUFFERS: usize = 3;
|
||||||
|
|
||||||
|
/// How long synchronized output (DECSET 2026) may hold the screen before we
|
||||||
|
/// present anyway, so a misbehaving app cannot freeze the window.
|
||||||
|
const SYNC_TIMEOUT_MS: u64 = 150;
|
||||||
|
|
||||||
/// What determines one rendered row's pixels: its cells, the cursor on it, the
|
/// What determines one rendered row's pixels: its cells, the cursor on it, the
|
||||||
/// selection span over it, and the blink phase. Two equal `RowSnap`s render
|
/// selection span over it, and the blink phase. Two equal `RowSnap`s render
|
||||||
/// identically, so a buffer holding an equal snapshot needs no repaint.
|
/// identically, so a buffer holding an equal snapshot needs no repaint.
|
||||||
|
|
@ -220,6 +224,7 @@ pub fn run() -> anyhow::Result<ExitCode> {
|
||||||
frames: Vec::new(),
|
frames: Vec::new(),
|
||||||
buf_dims: (0, 0),
|
buf_dims: (0, 0),
|
||||||
blink_on: true,
|
blink_on: true,
|
||||||
|
sync_timeout: None,
|
||||||
focused: true,
|
focused: true,
|
||||||
exit: false,
|
exit: false,
|
||||||
exit_code: ExitCode::SUCCESS,
|
exit_code: ExitCode::SUCCESS,
|
||||||
|
|
@ -325,6 +330,8 @@ struct App {
|
||||||
buf_dims: (u32, u32),
|
buf_dims: (u32, u32),
|
||||||
/// Current blink phase, toggled by a timer; off hides blinking ink.
|
/// Current blink phase, toggled by a timer; off hides blinking ink.
|
||||||
blink_on: bool,
|
blink_on: bool,
|
||||||
|
/// Armed while synchronized output holds the screen, to force it open.
|
||||||
|
sync_timeout: Option<RegistrationToken>,
|
||||||
/// Whether the toplevel currently has keyboard focus (drives the cursor).
|
/// Whether the toplevel currently has keyboard focus (drives the cursor).
|
||||||
focused: bool,
|
focused: bool,
|
||||||
exit: bool,
|
exit: bool,
|
||||||
|
|
@ -696,8 +703,34 @@ impl App {
|
||||||
|
|
||||||
/// Present a frame if one is wanted and the compositor is ready for it.
|
/// Present a frame if one is wanted and the compositor is ready for it.
|
||||||
/// Called after every event-loop wake; the frame-callback gate keeps draws
|
/// Called after every event-loop wake; the frame-callback gate keeps draws
|
||||||
/// paced to the display instead of one per PTY read.
|
/// paced to the display instead of one per PTY read. While the app holds
|
||||||
|
/// synchronized output (DECSET 2026) we withhold the frame, but arm a
|
||||||
|
/// timeout so a stuck `2026h` cannot freeze the window.
|
||||||
fn flush(&mut self) {
|
fn flush(&mut self) {
|
||||||
|
let sync = self
|
||||||
|
.session
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|s| s.term.grid().sync_active());
|
||||||
|
if sync {
|
||||||
|
if self.sync_timeout.is_none() {
|
||||||
|
let timer = Timer::from_duration(Duration::from_millis(SYNC_TIMEOUT_MS));
|
||||||
|
self.sync_timeout = self
|
||||||
|
.loop_handle
|
||||||
|
.insert_source(timer, |_, _, app: &mut App| {
|
||||||
|
if let Some(session) = app.session.as_mut() {
|
||||||
|
session.term.grid_mut().set_sync(false);
|
||||||
|
}
|
||||||
|
app.sync_timeout = None;
|
||||||
|
app.needs_draw = true;
|
||||||
|
TimeoutAction::Drop
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(token) = self.sync_timeout.take() {
|
||||||
|
self.loop_handle.remove(token);
|
||||||
|
}
|
||||||
if self.needs_draw && !self.frame_pending && self.session.is_some() {
|
if self.needs_draw && !self.frame_pending && self.session.is_some() {
|
||||||
self.present();
|
self.present();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue