fix(vterm): harden integrated Stage 2 behavior
Resolve post-main integration drift in authenticated routing, terminal view projection, Lua installation, and inherited acceptance callers. Preserve the terminal statusline provider alongside the landed Themes provider and record the final Stage 2 gate evidence. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
0ddff24589
commit
da8f6aeae4
|
|
@ -1,9 +1,9 @@
|
||||||
# Agent handoff — cross-machine continuity
|
# Agent handoff — cross-machine continuity
|
||||||
|
|
||||||
**Last updated: 2026-07-21, after the config registry (#127) and Vterm
|
**Last updated: 2026-07-21, after the config registry (#127), Vterm
|
||||||
Stage 1 terminal core (#126) both landed on `main`, atop completed
|
Stage 1 terminal core (#126), and handoff refresh (#128) landed on
|
||||||
Themes Arc 4 (#120/#124/#125). Vterm Stages 2 and 3 are not
|
`main`, atop completed Themes Arc 4 (#120/#124/#125). Vterm Stage 2 is
|
||||||
implemented.**
|
implemented on `vterm-tui`; Stage 3 is not implemented.**
|
||||||
This file is the
|
This file is the
|
||||||
bridge between development machines. If you are an agent reading
|
bridge between development machines. If you are an agent reading
|
||||||
this on a fresh clone: this document plus the `docs/*-framing.md`
|
this on a fresh clone: this document plus the `docs/*-framing.md`
|
||||||
|
|
@ -17,7 +17,8 @@ commands, read `docs/active-work.md` immediately after this file.
|
||||||
|
|
||||||
## 1. Where the project stands (2026-07-21)
|
## 1. Where the project stands (2026-07-21)
|
||||||
|
|
||||||
- `main` @ `2e37c04` (config registry #127), protocol **v18**
|
- `main` @ `f1a2f75` (handoff refresh #128 atop config registry #127),
|
||||||
|
protocol **v18**
|
||||||
(`SUPPORTED=[6..18]`; v16 = `ThemeFacts`, v17 = `FontFacts`, v18 =
|
(`SUPPORTED=[6..18]`; v16 = `ThemeFacts`, v17 = `FontFacts`, v18 =
|
||||||
`StatuslineSegments`).
|
`StatuslineSegments`).
|
||||||
- **Config registry LANDED — #127** (`docs/config-registry-framing.md`
|
- **Config registry LANDED — #127** (`docs/config-registry-framing.md`
|
||||||
|
|
@ -211,7 +212,7 @@ commands, read `docs/active-work.md` immediately after this file.
|
||||||
resize. Review round 2 rejects C0/C1 controls before they enter screen
|
resize. Review round 2 rejects C0/C1 controls before they enter screen
|
||||||
cells, preserves the released button code in SGR mouse reports, removes
|
cells, preserves the released button code in SGR mouse reports, removes
|
||||||
dead screen paths, and clears stale round-trip state during prune. Stage 2
|
dead screen paths, and clears stale round-trip state during prune. Stage 2
|
||||||
must uniquify default terminal buffer names.
|
now uniquifies default terminal buffer names transactionally.
|
||||||
- Exact CUU/CUD and out-of-range DECSTBM clamping, combining across controls,
|
- Exact CUU/CUD and out-of-range DECSTBM clamping, combining across controls,
|
||||||
xterm alternate-screen details, legacy non-SGR mouse, printable ASCII and
|
xterm alternate-screen details, legacy non-SGR mouse, printable ASCII and
|
||||||
CSI-dispatch allocation fast paths, and scrollback-cap naming are explicit
|
CSI-dispatch allocation fast paths, and scrollback-cap naming are explicit
|
||||||
|
|
@ -225,12 +226,37 @@ commands, read `docs/active-work.md` immediately after this file.
|
||||||
is a clean behavioral bite. The parser dispatch has its independent clean
|
is a clean behavioral bite. The parser dispatch has its independent clean
|
||||||
behavioral bite; the original `main`/crate-root bite remains explicitly
|
behavioral bite; the original `main`/crate-root bite remains explicitly
|
||||||
weaker compile-time API evidence.
|
weaker compile-time API evidence.
|
||||||
- Stage 2 reviews require a durable focus/input resize owner, owning
|
- Stage 3 owns `pmacs-gpu/src/attach.rs`, authenticated source routing,
|
||||||
`FrontendId` for the global `C-c` continuation, and local clipboard/BEL
|
protocol-owned wire types/limits, and a deliberate complete-frame limit
|
||||||
signal drainage. Stage 3 additionally owns `pmacs-gpu/src/attach.rs`,
|
decision: 16 MiB is insufficient; use a measured legal-worst cap or
|
||||||
authenticated source routing, protocol-owned wire types/limits, and a
|
aggregate bound, never silent chunking.
|
||||||
deliberate complete-frame limit decision: 16 MiB is insufficient; use a
|
- **Stage 2 TUI is implemented on `vterm-tui`** (`docs/vterm-framing.md`
|
||||||
measured legal-worst cap or aggregate bound, never silent chunking.
|
Revision 7, criteria 15–27). `TerminalViewKey` keys per-frontend/window
|
||||||
|
projection state over one shared process/screen; logical row anchors retain
|
||||||
|
scroll/selection through reflow. A most-recent authenticated controller
|
||||||
|
owns input and cell resize, with release on focus/switch/kill/detach.
|
||||||
|
- The strict `pmacs.terminal` Lua surface owns open/state/view/send/terminate
|
||||||
|
and context-implicit scroll/copy commands. Buffer-local terminal keymaps
|
||||||
|
resolve before raw child transport; fixed `C-c` remains the terminal escape
|
||||||
|
and owns its continuation per frontend. Copy drains through the acting
|
||||||
|
frontend's clipboard path; active BELs drain once locally and per daemon
|
||||||
|
frontend, while historical/passive bells are baseline-suppressed.
|
||||||
|
- TUI composition paints owned terminal cells/styles only inside each
|
||||||
|
window's content rectangle, suppresses document overlays, and keeps sibling
|
||||||
|
splits independent. Daemon key/mouse/paste/focus/resize/detach routing uses
|
||||||
|
the authenticated connection source rather than client-claimed IDs.
|
||||||
|
`builtin/runtime/terminal.lua` provides the terminal command, view commands,
|
||||||
|
and pure `ui.modeline.terminal` process/scroll segment.
|
||||||
|
- `tests/vterm_stage2_acceptance.rs` maps Lua transactionality, shared-view
|
||||||
|
isolation, clipboard/modeline behavior, and a hermetic real `/bin/sh` TUI
|
||||||
|
PTY smoke. Stage 2 changes no wire schema or GPU renderer; protocol remains
|
||||||
|
v18 until Stage 3.
|
||||||
|
- Integrated Stage 2 final gate: `cargo fmt --check`; strict workspace
|
||||||
|
Clippy; 1,742 default + 1,918 CRDT library tests (3 ignored each);
|
||||||
|
Stage 1 acceptance 9 default + 10 CRDT; Stage 2 acceptance 3 default +
|
||||||
|
3 CRDT; statusline acceptance 7 default + 8 CRDT; M4 114 passed
|
||||||
|
(3 ignored, 1 filtered); required GPU 109; workspace 2,869 passed
|
||||||
|
across 81 suites (19 ignored, 1 filtered); `git diff --check` clean.
|
||||||
- **PARKED: kill-ring browser + persistence.** Revision 2 framing is
|
- **PARKED: kill-ring browser + persistence.** Revision 2 framing is
|
||||||
preserved on branch `kill-ring-browser`, but its `0efb5cd` scout is stale
|
preserved on branch `kill-ring-browser`, but its `0efb5cd` scout is stale
|
||||||
and must be repeated before implementation. No PR or implementation is
|
and must be repeated before implementation. No PR or implementation is
|
||||||
|
|
|
||||||
|
|
@ -1406,7 +1406,6 @@ fn dispatcher_loop(
|
||||||
/// initial-full-grid analogue — it emits nothing until the frontend
|
/// initial-full-grid analogue — it emits nothing until the frontend
|
||||||
/// declares a viewport); every other session keeps the M5.3
|
/// declares a viewport); every other session keeps the M5.3
|
||||||
/// force-full-grid grid path.
|
/// force-full-grid grid path.
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn take_pending_terminal_bell(
|
fn take_pending_terminal_bell(
|
||||||
editor: &EditorState,
|
editor: &EditorState,
|
||||||
frontend_id: FrontendId,
|
frontend_id: FrontendId,
|
||||||
|
|
@ -1446,6 +1445,10 @@ fn take_pending_terminal_bell(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(
|
||||||
|
clippy::too_many_arguments,
|
||||||
|
reason = "one session bootstrap transaction"
|
||||||
|
)]
|
||||||
fn handle_session_established(
|
fn handle_session_established(
|
||||||
editor: &mut EditorState,
|
editor: &mut EditorState,
|
||||||
render_states: &mut HashMap<FrontendId, RenderState>,
|
render_states: &mut HashMap<FrontendId, RenderState>,
|
||||||
|
|
|
||||||
|
|
@ -301,7 +301,7 @@ impl EditorState {
|
||||||
let process_supervisor = crate::lua_bindings::make_process_supervisor(lua_host.lua())
|
let process_supervisor = crate::lua_bindings::make_process_supervisor(lua_host.lua())
|
||||||
.expect("install pmacs.process");
|
.expect("install pmacs.process");
|
||||||
let terminal_manager =
|
let terminal_manager =
|
||||||
crate::lua_bindings::make_terminal_manager(lua_host.lua(), process_supervisor.clone())
|
crate::lua_bindings::make_terminal_manager(lua_host.lua(), &process_supervisor)
|
||||||
.expect("install pmacs.terminal");
|
.expect("install pmacs.terminal");
|
||||||
lua_host
|
lua_host
|
||||||
.eval(
|
.eval(
|
||||||
|
|
@ -759,6 +759,10 @@ impl EditorState {
|
||||||
/// (`pmacs.frontend.id()`). Sets [`EditorCore::active_frontend`]
|
/// (`pmacs.frontend.id()`). Sets [`EditorCore::active_frontend`]
|
||||||
/// before any command body runs, so observers always see a fresh
|
/// before any command body runs, so observers always see a fresh
|
||||||
/// value.
|
/// value.
|
||||||
|
#[allow(
|
||||||
|
clippy::too_many_lines,
|
||||||
|
reason = "single input-precedence state machine"
|
||||||
|
)]
|
||||||
pub fn dispatch_key(&mut self, frontend_id: FrontendId, key: KeyEvent) {
|
pub fn dispatch_key(&mut self, frontend_id: FrontendId, key: KeyEvent) {
|
||||||
if !matches!(key.kind, KeyEventKind::Press | KeyEventKind::Repeat) {
|
if !matches!(key.kind, KeyEventKind::Press | KeyEventKind::Repeat) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -1579,6 +1583,10 @@ impl EditorState {
|
||||||
/// (the click neither activates the window nor positions the
|
/// (the click neither activates the window nor positions the
|
||||||
/// cursor; that gesture is reserved for future binding to
|
/// cursor; that gesture is reserved for future binding to
|
||||||
/// "switch to this window" without disturbing buffer state).
|
/// "switch to this window" without disturbing buffer state).
|
||||||
|
#[allow(
|
||||||
|
clippy::too_many_lines,
|
||||||
|
reason = "shared document/terminal mouse router"
|
||||||
|
)]
|
||||||
pub fn dispatch_mouse(
|
pub fn dispatch_mouse(
|
||||||
&mut self,
|
&mut self,
|
||||||
frontend_id: FrontendId,
|
frontend_id: FrontendId,
|
||||||
|
|
@ -1722,9 +1730,7 @@ impl EditorState {
|
||||||
) {
|
) {
|
||||||
use crossterm::event::{MouseButton, MouseEventKind};
|
use crossterm::event::{MouseButton, MouseEventKind};
|
||||||
|
|
||||||
let Some(kind) = terminal_mouse_kind(event.kind) else {
|
let kind = terminal_mouse_kind(event.kind);
|
||||||
return;
|
|
||||||
};
|
|
||||||
let modifiers = terminal_modifiers(event.modifiers);
|
let modifiers = terminal_modifiers(event.modifiers);
|
||||||
let shift = modifiers.contains(TerminalModifiers::SHIFT);
|
let shift = modifiers.contains(TerminalModifiers::SHIFT);
|
||||||
let (at_bottom, modes, screen_size) = {
|
let (at_bottom, modes, screen_size) = {
|
||||||
|
|
@ -2313,8 +2319,7 @@ fn process_event(state: &mut EditorState, ev: Event, term_size: crate::cell::Cel
|
||||||
}
|
}
|
||||||
Event::FocusGained => state.dispatch_focus(frontend_id, true),
|
Event::FocusGained => state.dispatch_focus(frontend_id, true),
|
||||||
Event::FocusLost => state.dispatch_focus(frontend_id, false),
|
Event::FocusLost => state.dispatch_focus(frontend_id, false),
|
||||||
Event::Key(_) => {}
|
Event::Key(_) | Event::Resize(_, _) => {}
|
||||||
Event::Resize(_, _) => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2538,10 +2543,10 @@ impl CompletionPopupKey {
|
||||||
/// Vec-backed [`crate::cell::CellGrid`] without going through a
|
/// Vec-backed [`crate::cell::CellGrid`] without going through a
|
||||||
/// `RenderState`.
|
/// `RenderState`.
|
||||||
#[allow(clippy::too_many_lines, reason = "linear paint pipeline")]
|
#[allow(clippy::too_many_lines, reason = "linear paint pipeline")]
|
||||||
pub fn paint_frame(
|
pub fn paint_frame<S: std::hash::BuildHasher>(
|
||||||
state: &EditorState,
|
state: &EditorState,
|
||||||
frontend_id: FrontendId,
|
frontend_id: FrontendId,
|
||||||
terminal_snapshots: &HashMap<WindowId, TerminalSnapshot>,
|
terminal_snapshots: &HashMap<WindowId, TerminalSnapshot, S>,
|
||||||
grid: &mut crate::cell::CellGrid<'_>,
|
grid: &mut crate::cell::CellGrid<'_>,
|
||||||
term_size: CellSize,
|
term_size: CellSize,
|
||||||
) -> Option<CellCoord> {
|
) -> Option<CellCoord> {
|
||||||
|
|
@ -3602,14 +3607,14 @@ fn terminal_modifiers(modifiers: KeyModifiers) -> TerminalModifiers {
|
||||||
crate::protocol::crossterm_translate::mods_from_crossterm(modifiers)
|
crate::protocol::crossterm_translate::mods_from_crossterm(modifiers)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn terminal_mouse_kind(kind: crossterm::event::MouseEventKind) -> Option<TerminalMouseKind> {
|
fn terminal_mouse_kind(kind: crossterm::event::MouseEventKind) -> TerminalMouseKind {
|
||||||
use crossterm::event::{MouseButton, MouseEventKind};
|
use crossterm::event::{MouseButton, MouseEventKind};
|
||||||
let button = |button| match button {
|
let button = |button| match button {
|
||||||
MouseButton::Left => TerminalMouseButton::Left,
|
MouseButton::Left => TerminalMouseButton::Left,
|
||||||
MouseButton::Right => TerminalMouseButton::Right,
|
MouseButton::Right => TerminalMouseButton::Right,
|
||||||
MouseButton::Middle => TerminalMouseButton::Middle,
|
MouseButton::Middle => TerminalMouseButton::Middle,
|
||||||
};
|
};
|
||||||
Some(match kind {
|
match kind {
|
||||||
MouseEventKind::Down(value) => TerminalMouseKind::Down(button(value)),
|
MouseEventKind::Down(value) => TerminalMouseKind::Down(button(value)),
|
||||||
MouseEventKind::Up(value) => TerminalMouseKind::Up(button(value)),
|
MouseEventKind::Up(value) => TerminalMouseKind::Up(button(value)),
|
||||||
MouseEventKind::Drag(value) => TerminalMouseKind::Drag(button(value)),
|
MouseEventKind::Drag(value) => TerminalMouseKind::Drag(button(value)),
|
||||||
|
|
@ -3618,7 +3623,7 @@ fn terminal_mouse_kind(kind: crossterm::event::MouseEventKind) -> Option<Termina
|
||||||
MouseEventKind::ScrollDown => TerminalMouseKind::ScrollDown,
|
MouseEventKind::ScrollDown => TerminalMouseKind::ScrollDown,
|
||||||
MouseEventKind::ScrollLeft => TerminalMouseKind::ScrollLeft,
|
MouseEventKind::ScrollLeft => TerminalMouseKind::ScrollLeft,
|
||||||
MouseEventKind::ScrollRight => TerminalMouseKind::ScrollRight,
|
MouseEventKind::ScrollRight => TerminalMouseKind::ScrollRight,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn key_event_to_chord(key: KeyEvent) -> Option<Chord> {
|
fn key_event_to_chord(key: KeyEvent) -> Option<Chord> {
|
||||||
|
|
|
||||||
|
|
@ -8136,10 +8136,10 @@ pub fn make_process_supervisor(lua: &Lua) -> mlua::Result<SharedProcessSuperviso
|
||||||
/// Build the shared terminal registry and install strict raw Lua primitives.
|
/// Build the shared terminal registry and install strict raw Lua primitives.
|
||||||
pub fn make_terminal_manager(
|
pub fn make_terminal_manager(
|
||||||
lua: &Lua,
|
lua: &Lua,
|
||||||
supervisor: SharedProcessSupervisor,
|
supervisor: &SharedProcessSupervisor,
|
||||||
) -> mlua::Result<crate::terminal::SharedTerminalManager> {
|
) -> mlua::Result<crate::terminal::SharedTerminalManager> {
|
||||||
let manager = Rc::new(RefCell::new(crate::terminal::TerminalManager::new()));
|
let manager = Rc::new(RefCell::new(crate::terminal::TerminalManager::new()));
|
||||||
install_terminal(lua, &manager, &supervisor)?;
|
install_terminal(lua, &manager, supervisor)?;
|
||||||
Ok(manager)
|
Ok(manager)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -8248,6 +8248,10 @@ fn terminal_view_key_from_context(
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(
|
||||||
|
clippy::too_many_lines,
|
||||||
|
reason = "single strict Lua module installation"
|
||||||
|
)]
|
||||||
fn install_terminal(
|
fn install_terminal(
|
||||||
lua: &Lua,
|
lua: &Lua,
|
||||||
manager: &crate::terminal::SharedTerminalManager,
|
manager: &crate::terminal::SharedTerminalManager,
|
||||||
|
|
@ -8262,7 +8266,7 @@ fn install_terminal(
|
||||||
terminal.set(
|
terminal.set(
|
||||||
"_open",
|
"_open",
|
||||||
lua.create_function(move |lua, spec: Table| -> mlua::Result<BufferIdLua> {
|
lua.create_function(move |lua, spec: Table| -> mlua::Result<BufferIdLua> {
|
||||||
let spec = parse_terminal_spec(spec)?;
|
let spec = parse_terminal_spec(&spec)?;
|
||||||
let core = lua
|
let core = lua
|
||||||
.app_data_ref::<SharedCore>()
|
.app_data_ref::<SharedCore>()
|
||||||
.map(|core| core.clone())
|
.map(|core| core.clone())
|
||||||
|
|
@ -8467,7 +8471,7 @@ fn install_terminal(
|
||||||
pmacs.set("terminal", terminal)
|
pmacs.set("terminal", terminal)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_terminal_spec(table: Table) -> mlua::Result<crate::terminal::TerminalSpec> {
|
fn parse_terminal_spec(table: &Table) -> mlua::Result<crate::terminal::TerminalSpec> {
|
||||||
const FIELDS: &[&str] = &[
|
const FIELDS: &[&str] = &[
|
||||||
"command",
|
"command",
|
||||||
"args",
|
"args",
|
||||||
|
|
|
||||||
|
|
@ -353,8 +353,7 @@ impl TerminalManager {
|
||||||
let Some(state) = self.views.get_mut(&key) else {
|
let Some(state) = self.views.get_mut(&key) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let changed = state.selection.take().is_some() || state.drag.take().is_some();
|
state.selection.take().is_some() || state.drag.take().is_some()
|
||||||
changed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Current child input modes for one session.
|
/// Current child input modes for one session.
|
||||||
|
|
@ -608,12 +607,11 @@ fn project_snapshot(
|
||||||
let rows = retained_rows(projection);
|
let rows = retained_rows(projection);
|
||||||
let geometry = view_geometry(&rows, state, viewport_size.rows);
|
let geometry = view_geometry(&rows, state, viewport_size.rows);
|
||||||
let mut cells = vec![Cell::default(); viewport_size.area() as usize];
|
let mut cells = vec![Cell::default(); viewport_size.area() as usize];
|
||||||
for retained_row in geometry.start..rows.len() {
|
for (retained_row, &row) in rows.iter().enumerate().skip(geometry.start) {
|
||||||
let Some(target_row) = viewport_row(&geometry, viewport_size.rows as usize, retained_row)
|
let Some(target_row) = viewport_row(&geometry, viewport_size.rows as usize, retained_row)
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let row = rows[retained_row];
|
|
||||||
let copy_cols = row.cells.len().min(viewport_size.cols as usize);
|
let copy_cols = row.cells.len().min(viewport_size.cols as usize);
|
||||||
let target = target_row * viewport_size.cols as usize;
|
let target = target_row * viewport_size.cols as usize;
|
||||||
cells[target..target + copy_cols].clone_from_slice(&row.cells[..copy_cols]);
|
cells[target..target + copy_cols].clone_from_slice(&row.cells[..copy_cols]);
|
||||||
|
|
@ -624,13 +622,12 @@ fn project_snapshot(
|
||||||
.and_then(|selection| normalized_selection(&rows, selection))
|
.and_then(|selection| normalized_selection(&rows, selection))
|
||||||
.map_or_else(Vec::new, |(start, end)| {
|
.map_or_else(Vec::new, |(start, end)| {
|
||||||
let mut spans = Vec::new();
|
let mut spans = Vec::new();
|
||||||
for retained_row in start.row..=end.row {
|
for (retained_row, &row) in rows.iter().enumerate().take(end.row + 1).skip(start.row) {
|
||||||
let Some(target_row) =
|
let Some(target_row) =
|
||||||
viewport_row(&geometry, viewport_size.rows as usize, retained_row)
|
viewport_row(&geometry, viewport_size.rows as usize, retained_row)
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let row = rows[retained_row];
|
|
||||||
let start_col = if retained_row == start.row {
|
let start_col = if retained_row == start.row {
|
||||||
start.col
|
start.col
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -687,8 +684,7 @@ fn is_default_blank(cell: &Cell) -> bool {
|
||||||
fn copy_selection_bytes(rows: &[&TerminalRow], selection: TerminalSelection) -> Option<Vec<u8>> {
|
fn copy_selection_bytes(rows: &[&TerminalRow], selection: TerminalSelection) -> Option<Vec<u8>> {
|
||||||
let (start, end) = normalized_selection(rows, selection)?;
|
let (start, end) = normalized_selection(rows, selection)?;
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for row_index in start.row..=end.row {
|
for (row_index, &row) in rows.iter().enumerate().take(end.row + 1).skip(start.row) {
|
||||||
let row = rows[row_index];
|
|
||||||
let from = if row_index == start.row { start.col } else { 0 };
|
let from = if row_index == start.row { start.col } else { 0 };
|
||||||
let mut to = if row_index == end.row {
|
let mut to = if row_index == end.row {
|
||||||
end.col.saturating_add(glyph_width(row, end.col))
|
end.col.saturating_add(glyph_width(row, end.col))
|
||||||
|
|
@ -815,7 +811,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn copy_joins_soft_wraps_trims_default_blanks_and_separates_hard_rows() {
|
fn copy_joins_soft_wraps_trims_default_blanks_and_separates_hard_rows() {
|
||||||
let rows = vec![
|
let rows = [
|
||||||
row(1, 0, "ab ", true),
|
row(1, 0, "ab ", true),
|
||||||
row(1, 3, "cd ", false),
|
row(1, 3, "cd ", false),
|
||||||
row(2, 0, "e ", false),
|
row(2, 0, "e ", false),
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,13 @@ fn paint(state: &EditorState, rows: u32, cols: u32) -> Vec<Cell> {
|
||||||
stride: cols,
|
stride: cols,
|
||||||
size: CellSize::new(rows, cols),
|
size: CellSize::new(rows, cols),
|
||||||
};
|
};
|
||||||
let _ = pmacs::editor::paint_frame(state, &mut grid, CellSize::new(rows, cols));
|
let _ = pmacs::editor::paint_frame(
|
||||||
|
state,
|
||||||
|
FrontendId::LOCAL,
|
||||||
|
&std::collections::HashMap::new(),
|
||||||
|
&mut grid,
|
||||||
|
CellSize::new(rows, cols),
|
||||||
|
);
|
||||||
cells
|
cells
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,8 +130,14 @@ fn a01_04_registry_contract_limits_epochs_and_results() {
|
||||||
assert!(baseline_mode.ends_with(" L1:C1 All "));
|
assert!(baseline_mode.ends_with(" L1:C1 All "));
|
||||||
|
|
||||||
let initial = state.statusline_registry.borrow().providers();
|
let initial = state.statusline_registry.borrow().providers();
|
||||||
assert_eq!(initial.len(), 1, "builtin lsp provider is discoverable");
|
assert_eq!(
|
||||||
assert_eq!(initial[0].name, "lsp");
|
initial
|
||||||
|
.iter()
|
||||||
|
.map(|provider| provider.name.as_str())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
["terminal", "lsp"],
|
||||||
|
"built-in providers are discoverable"
|
||||||
|
);
|
||||||
let before_epochs = {
|
let before_epochs = {
|
||||||
let registry = state.statusline_registry.borrow();
|
let registry = state.statusline_registry.borrow();
|
||||||
(registry.layout_epoch(), registry.face_set_epoch())
|
(registry.layout_epoch(), registry.face_set_epoch())
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,13 @@ fn paint_full_frame(state: &EditorState, rows: u32, cols: u32) -> Vec<Cell> {
|
||||||
stride: cols,
|
stride: cols,
|
||||||
size: CellSize::new(rows, cols),
|
size: CellSize::new(rows, cols),
|
||||||
};
|
};
|
||||||
let _cursor = pmacs::editor::paint_frame(state, &mut grid, CellSize::new(rows, cols));
|
let _cursor = pmacs::editor::paint_frame(
|
||||||
|
state,
|
||||||
|
FrontendId::LOCAL,
|
||||||
|
&std::collections::HashMap::new(),
|
||||||
|
&mut grid,
|
||||||
|
CellSize::new(rows, cols),
|
||||||
|
);
|
||||||
backing
|
backing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,16 +104,6 @@ fn strict_owned_spec_rejects_before_spawn_and_is_mutation_independent() {
|
||||||
lua_processes, 0,
|
lua_processes, 0,
|
||||||
"terminal-owned ProcessId must not be exposed through pmacs.process"
|
"terminal-owned ProcessId must not be exposed through pmacs.process"
|
||||||
);
|
);
|
||||||
let terminal_module_absent: bool = state
|
|
||||||
.lua_host
|
|
||||||
.lua()
|
|
||||||
.load("return pmacs.terminal == nil")
|
|
||||||
.eval()
|
|
||||||
.expect("terminal module absence");
|
|
||||||
assert!(
|
|
||||||
terminal_module_absent,
|
|
||||||
"Stage 1 must not publish an unrenderable interactive Lua terminal API"
|
|
||||||
);
|
|
||||||
|
|
||||||
let process_id = state
|
let process_id = state
|
||||||
.terminal_manager
|
.terminal_manager
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ fn tick_until(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lua_string(value: &str) -> String {
|
fn lua_string(value: &str) -> String {
|
||||||
format!("{:?}", value)
|
format!("{value:?}")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn snapshot_text(snapshot: &pmacs::terminal::TerminalSnapshot) -> String {
|
fn snapshot_text(snapshot: &pmacs::terminal::TerminalSnapshot) -> String {
|
||||||
|
|
@ -54,6 +54,10 @@ fn snapshot_text(snapshot: &pmacs::terminal::TerminalSnapshot) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[allow(
|
||||||
|
clippy::too_many_lines,
|
||||||
|
reason = "cross-surface Lua transaction scenario"
|
||||||
|
)]
|
||||||
fn lua_surface_is_strict_fresh_transactional_and_context_safe() {
|
fn lua_surface_is_strict_fresh_transactional_and_context_safe() {
|
||||||
let mut state = EditorState::new();
|
let mut state = EditorState::new();
|
||||||
let command_lua = lua_string("/bin/sh");
|
let command_lua = lua_string("/bin/sh");
|
||||||
|
|
@ -181,7 +185,7 @@ fn lua_surface_is_strict_fresh_transactional_and_context_safe() {
|
||||||
let lua = state.lua_host.lua();
|
let lua = state.lua_host.lua();
|
||||||
let status: Table = lua
|
let status: Table = lua
|
||||||
.load(format!(
|
.load(format!(
|
||||||
r#"
|
r"
|
||||||
local first = pmacs.terminal.view_state {{
|
local first = pmacs.terminal.view_state {{
|
||||||
frontend = 1, window = {}, buffer = TERM_BUFFER, active = true
|
frontend = 1, window = {}, buffer = TERM_BUFFER, active = true
|
||||||
}}
|
}}
|
||||||
|
|
@ -191,7 +195,7 @@ fn lua_surface_is_strict_fresh_transactional_and_context_safe() {
|
||||||
}}
|
}}
|
||||||
assert(second.injected == nil)
|
assert(second.injected == nil)
|
||||||
return second
|
return second
|
||||||
"#,
|
",
|
||||||
window_id.raw(),
|
window_id.raw(),
|
||||||
window_id.raw()
|
window_id.raw()
|
||||||
))
|
))
|
||||||
|
|
@ -201,10 +205,10 @@ fn lua_surface_is_strict_fresh_transactional_and_context_safe() {
|
||||||
|
|
||||||
let (ok, error): (bool, String) = lua
|
let (ok, error): (bool, String) = lua
|
||||||
.load(
|
.load(
|
||||||
r#"
|
r"
|
||||||
local ok, err = pcall(function() pmacs.terminal.scroll(1) end)
|
local ok, err = pcall(function() pmacs.terminal.scroll(1) end)
|
||||||
return ok, tostring(err)
|
return ok, tostring(err)
|
||||||
"#,
|
",
|
||||||
)
|
)
|
||||||
.eval()
|
.eval()
|
||||||
.expect("pcall implicit scroll");
|
.expect("pcall implicit scroll");
|
||||||
|
|
@ -393,6 +397,7 @@ fn wait_for_file(path: &Path, timeout: Duration) -> Vec<u8> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[allow(clippy::too_many_lines, reason = "one real-host lifecycle scenario")]
|
||||||
fn real_tui_terminal_smoke_restores_host_after_output_input_resize_scroll_copy_and_bell() {
|
fn real_tui_terminal_smoke_restores_host_after_output_input_resize_scroll_copy_and_bell() {
|
||||||
let temp = tempfile::TempDir::new().expect("tempdir");
|
let temp = tempfile::TempDir::new().expect("tempdir");
|
||||||
let config_root = temp.path().join("config");
|
let config_root = temp.path().join("config");
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue