Skip to content
Draft
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased]

### Added
- synchronized output (DEC mode `?2026`): frames are held while a program is mid-update, eliminating tearing
- add `--maximized` and `--fullscreen` flags to start the window in that mode

### Changed
Expand Down
15 changes: 15 additions & 0 deletions src/renderer/render_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,27 @@ pub(super) fn draw_overlays(
}

impl App {
/// True while the active pane's program holds synchronized output (?2026h).
fn active_grid_synchronized(&self) -> bool {
let tab = &self.state.tabs[self.state.active_tab];
tab.panes
.get(&tab.active)
.and_then(|e| e.pane.grid_read().map(|g| g.synchronized_output))
.unwrap_or(false)
}

pub(crate) fn redraw(&mut self) {
// No tabs means startup failed to spawn any pane and an exit is pending;
// skip the frame rather than indexing an empty `tabs`.
if self.state.tabs.is_empty() {
return;
}
// Synchronized output (?2026): the running program is mid-update, so skip
// the frame entirely — the window keeps the last presented buffer and never
// shows a partial screen. The next PTY byte (the ?2026l reset) redraws.
if self.active_grid_synchronized() {
return;
}
if self.state.blink_last.elapsed()
>= Duration::from_millis(self.state.config.window.cursor_blink_ms as u64)
{
Expand Down
4 changes: 4 additions & 0 deletions src/terminal/grid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ pub struct Grid {
pub cursor_shape: CursorShape,
// Bracketed paste mode (?2004)
pub bracketed_paste: bool,
// Synchronized output (?2026): while true, the renderer skips presenting frames
pub synchronized_output: bool,
// Mouse reporting mode: 0=off, 1000=click, 1002=button-motion, 1003=any-motion
pub mouse_mode: u16,
// SGR extended mouse encoding (?1006)
Expand Down Expand Up @@ -281,6 +283,7 @@ impl Grid {
cursor_visible: true,
cursor_shape: CursorShape::Block,
bracketed_paste: false,
synchronized_output: false,
mouse_mode: 0,
mouse_sgr: false,
alternate_saved: None,
Expand Down Expand Up @@ -1033,6 +1036,7 @@ impl Grid {
self.cursor_visible = true;
self.cursor_shape = CursorShape::Block;
self.bracketed_paste = false;
self.synchronized_output = false;
self.mouse_mode = 0;
self.mouse_sgr = false;
self.application_cursor_keys = false;
Expand Down
29 changes: 11 additions & 18 deletions src/terminal/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,27 +66,20 @@ struct Performer<'a> {

impl Performer<'_> {
fn handle_dec_private_modes(&mut self, action: char, p0: u16) {
// Only DECSET (h) / DECRST (l) toggle modes; the `('h' | 'l', _)` patterns
// keep other actions out of these arms, so `on` is only used when relevant.
let on = action == 'h';
match (action, p0) {
('h', 1) => self.grid.application_cursor_keys = true,
('l', 1) => self.grid.application_cursor_keys = false,
('h', 7) => self.grid.autowrap = true,
('l', 7) => self.grid.autowrap = false,
('h', 25) => self.grid.cursor_visible = true,
('l', 25) => self.grid.cursor_visible = false,
('h', 1000) => self.grid.mouse_mode = 1000,
('l', 1000) => self.grid.mouse_mode = 0,
('h', 1002) => self.grid.mouse_mode = 1002,
('l', 1002) => self.grid.mouse_mode = 0,
('h', 1003) => self.grid.mouse_mode = 1003,
('l', 1003) => self.grid.mouse_mode = 0,
('h', 1004) => self.grid.focus_report = true,
('l', 1004) => self.grid.focus_report = false,
('h', 1006) => self.grid.mouse_sgr = true,
('l', 1006) => self.grid.mouse_sgr = false,
('h' | 'l', 1) => self.grid.application_cursor_keys = on,
('h' | 'l', 7) => self.grid.autowrap = on,
('h' | 'l', 25) => self.grid.cursor_visible = on,
('h' | 'l', 1000 | 1002 | 1003) => self.grid.mouse_mode = if on { p0 } else { 0 },
('h' | 'l', 1004) => self.grid.focus_report = on,
('h' | 'l', 1006) => self.grid.mouse_sgr = on,
('h' | 'l', 2004) => self.grid.bracketed_paste = on,
('h' | 'l', 2026) => self.grid.synchronized_output = on,
('h', 1049) => self.grid.enter_alternate_screen(),
('l', 1049) => self.grid.exit_alternate_screen(),
('h', 2004) => self.grid.bracketed_paste = true,
('l', 2004) => self.grid.bracketed_paste = false,
_ => {}
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/terminal/parser_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,15 @@ fn bracketed_paste_mode() {
assert!(!p.grid.bracketed_paste);
}

#[test]
fn synchronized_output_mode() {
let mut p = make_parser(10, 5);
p.process(b"\x1b[?2026h");
assert!(p.grid.synchronized_output);
p.process(b"\x1b[?2026l");
assert!(!p.grid.synchronized_output);
}

#[test]
fn scroll_region_sets_top_and_bottom() {
let mut p = make_parser(10, 10);
Expand Down