feat(vterm): expose atomic screen projections
Publish retained row metadata, alternate-screen identity, cursor, title, and cells through the same synchronized-output generation gate. This gives each Stage 2 view one coherent projection without borrowing or mirroring mutable screen state. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
1b32f81033
commit
39e07cbf9b
|
|
@ -97,6 +97,25 @@ pub struct ScreenSnapshot {
|
|||
pub generation: u64,
|
||||
}
|
||||
|
||||
/// Owned row projection published atomically to terminal views.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ScreenProjection {
|
||||
/// Terminal grid dimensions.
|
||||
pub size: CellSize,
|
||||
/// Whether the projected visible rows belong to the alternate screen.
|
||||
pub alternate_active: bool,
|
||||
/// Retained main-screen history, empty while the alternate screen is active.
|
||||
pub history: Vec<TerminalRow>,
|
||||
/// Active visible rows, including logical-line and soft-wrap metadata.
|
||||
pub visible_rows: Vec<TerminalRow>,
|
||||
/// Published cursor position when visible.
|
||||
pub cursor: Option<CellCoord>,
|
||||
/// Published terminal title.
|
||||
pub title: Option<String>,
|
||||
/// Screen generation represented by this projection.
|
||||
pub generation: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
struct Cursor {
|
||||
row: usize,
|
||||
|
|
@ -138,7 +157,7 @@ pub struct TerminalScreen {
|
|||
tab_stops: BTreeSet<usize>,
|
||||
title: Option<String>,
|
||||
generation: u64,
|
||||
published: ScreenSnapshot,
|
||||
published: ScreenProjection,
|
||||
sync_started: Option<Instant>,
|
||||
next_line_id: u64,
|
||||
scrollback_rows: usize,
|
||||
|
|
@ -159,9 +178,11 @@ impl TerminalScreen {
|
|||
let mut next_line_id = 1;
|
||||
let main = Grid::new(size, &mut next_line_id);
|
||||
let alt = Grid::new(size, &mut next_line_id);
|
||||
let published = ScreenSnapshot {
|
||||
let published = ScreenProjection {
|
||||
size,
|
||||
cells: flatten(&main.rows),
|
||||
alternate_active: false,
|
||||
history: Vec::new(),
|
||||
visible_rows: main.rows.clone(),
|
||||
cursor: Some(CellCoord::new(0, 0)),
|
||||
title: None,
|
||||
generation: 0,
|
||||
|
|
@ -502,15 +523,33 @@ impl TerminalScreen {
|
|||
|
||||
pub fn snapshot(&self) -> ScreenSnapshot {
|
||||
if self.modes.synchronized_output {
|
||||
self.published.clone()
|
||||
snapshot_from_projection(&self.published)
|
||||
} else {
|
||||
self.current_snapshot()
|
||||
}
|
||||
}
|
||||
/// Return one owned, publication-consistent row projection.
|
||||
#[must_use]
|
||||
pub fn projection(&self) -> ScreenProjection {
|
||||
if self.modes.synchronized_output {
|
||||
self.published.clone()
|
||||
} else {
|
||||
self.current_projection()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modes(&self) -> TerminalModes {
|
||||
self.modes
|
||||
}
|
||||
/// Return whether the published active screen is alternate.
|
||||
#[must_use]
|
||||
pub fn alternate_active(&self) -> bool {
|
||||
if self.modes.synchronized_output {
|
||||
self.published.alternate_active
|
||||
} else {
|
||||
self.alt_active
|
||||
}
|
||||
}
|
||||
pub fn bell_count(&self) -> u64 {
|
||||
self.bell_count
|
||||
}
|
||||
|
|
@ -1380,8 +1419,26 @@ impl TerminalScreen {
|
|||
generation: self.generation,
|
||||
}
|
||||
}
|
||||
fn current_projection(&self) -> ScreenProjection {
|
||||
ScreenProjection {
|
||||
size: self.size,
|
||||
alternate_active: self.alt_active,
|
||||
history: if self.alt_active {
|
||||
Vec::new()
|
||||
} else {
|
||||
self.main.history.iter().cloned().collect()
|
||||
},
|
||||
visible_rows: self.active().rows.clone(),
|
||||
cursor: self
|
||||
.modes
|
||||
.cursor_visible
|
||||
.then(|| CellCoord::new(self.cursor.row as u32, self.cursor.col as u32)),
|
||||
title: self.title.clone(),
|
||||
generation: self.generation,
|
||||
}
|
||||
}
|
||||
fn publish(&mut self) {
|
||||
self.published = self.current_snapshot();
|
||||
self.published = self.current_projection();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1491,6 +1548,15 @@ fn glyph_width(glyph: &Glyph) -> usize {
|
|||
Glyph::Continuation => 0,
|
||||
}
|
||||
}
|
||||
fn snapshot_from_projection(projection: &ScreenProjection) -> ScreenSnapshot {
|
||||
ScreenSnapshot {
|
||||
size: projection.size,
|
||||
cells: flatten(&projection.visible_rows),
|
||||
cursor: projection.cursor,
|
||||
title: projection.title.clone(),
|
||||
generation: projection.generation,
|
||||
}
|
||||
}
|
||||
fn resize_grid_clip(
|
||||
grid: &mut Grid,
|
||||
old: CellSize,
|
||||
|
|
@ -1625,18 +1691,21 @@ mod tests {
|
|||
#[test]
|
||||
fn alternate_screen_preserves_main_and_has_no_history() {
|
||||
let mut s = screen(2, 4);
|
||||
assert!(!s.alternate_active());
|
||||
s.apply_event(AnsiEvent::Text("main".into()));
|
||||
let main = s.snapshot();
|
||||
s.apply_event(AnsiEvent::AlternateScreen {
|
||||
mode: AlternateScreenMode::Mode1049,
|
||||
enabled: true,
|
||||
});
|
||||
assert!(s.alternate_active());
|
||||
s.apply_event(AnsiEvent::Text("alt\nmore".into()));
|
||||
assert!(s.history().is_empty());
|
||||
s.apply_event(AnsiEvent::AlternateScreen {
|
||||
mode: AlternateScreenMode::Mode1049,
|
||||
enabled: false,
|
||||
});
|
||||
assert!(!s.alternate_active());
|
||||
assert_eq!(&s.snapshot().cells[..4], &main.cells[..4]);
|
||||
}
|
||||
|
||||
|
|
@ -1668,17 +1737,35 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn synchronized_output_gates_snapshot_and_finish_releases() {
|
||||
fn synchronized_output_gates_snapshot_and_row_projection_until_release() {
|
||||
let mut s = screen(2, 4);
|
||||
let before = s.snapshot();
|
||||
s.apply_event(AnsiEvent::Text("main".into()));
|
||||
s.apply_event(AnsiEvent::LineFeed);
|
||||
s.apply_event(AnsiEvent::LineFeed);
|
||||
let before_snapshot = s.snapshot();
|
||||
let before_projection = s.projection();
|
||||
assert_eq!(before_projection.history.len(), 1);
|
||||
assert!(!before_projection.alternate_active);
|
||||
|
||||
s.apply_event(AnsiEvent::SetMode {
|
||||
mode: TerminalMode::SynchronizedOutput,
|
||||
enabled: true,
|
||||
});
|
||||
s.apply_event(AnsiEvent::Text("x".into()));
|
||||
assert_eq!(s.snapshot(), before);
|
||||
s.apply_event(AnsiEvent::AlternateScreen {
|
||||
mode: AlternateScreenMode::Mode1049,
|
||||
enabled: true,
|
||||
});
|
||||
s.apply_event(AnsiEvent::Text("alt".into()));
|
||||
assert_eq!(s.snapshot(), before_snapshot);
|
||||
assert_eq!(s.projection(), before_projection);
|
||||
assert!(!s.alternate_active());
|
||||
|
||||
s.finish_output();
|
||||
assert_ne!(s.snapshot(), before);
|
||||
let released = s.projection();
|
||||
assert!(released.alternate_active);
|
||||
assert!(released.history.is_empty());
|
||||
assert_eq!(s.snapshot().cells, flatten(&released.visible_rows));
|
||||
assert!(s.alternate_active());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in New Issue