//! Terminal configuration acceptance (Stage 1 of //! `docs/terminal-config-and-copy-mode-framing.md`, criteria 1-12). //! //! Deliberately NOT `#[cfg(feature = "crdt")]`: CI never enables that //! feature, so a gated suite is written and then never run. use std::thread; use std::time::{Duration, Instant}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use mlua::Value; use pmacs::cell::{CellSize, Glyph}; use pmacs::editor::EditorState; use pmacs::protocol::FrontendId; use pmacs::terminal::TerminalViewKey; use pmacs::window::WindowId; fn exec(state: &EditorState, src: &str) { state .lua_host .lua() .load(src) .exec() .unwrap_or_else(|e| panic!("lua failed: {src}\n{e}")); } fn eval_err(state: &EditorState, src: &str) -> String { let result: mlua::Result = state.lua_host.lua().load(src).eval(); match result { Ok(_) => panic!("expected an error from: {src}"), Err(e) => e.to_string(), } } /// The viewport every test projects through. Deliberately SHORTER than /// the 24-row screen a terminal opens with, so "scroll to the oldest /// retained row" has somewhere to go even when nothing is retained — /// which is what makes the two scrollback arms differ by content rather /// than by whether scrolling was possible at all. fn viewport() -> CellSize { CellSize::new(10, 40) } fn cells_to_text(cells: &[pmacs::cell::Cell]) -> String { let mut text = String::new(); for cell in cells { match &cell.glyph { Glyph::Char(c) => text.push(*c), Glyph::Cluster(b) => text.push_str(&String::from_utf8_lossy(b)), Glyph::Continuation => {} } } text } fn screen_text(state: &EditorState, buffer: pmacs::buffer::BufferId) -> String { let manager = state.terminal_manager.borrow(); let Some(snapshot) = manager.snapshot(buffer) else { return String::new(); }; cells_to_text(&snapshot.cells) } /// Text a view actually shows, which is where retained history is /// visible at all — the live `screen_text` above always reads the tail. fn view_text(state: &EditorState, key: TerminalViewKey) -> String { let mut manager = state.terminal_manager.borrow_mut(); manager .snapshot_for_view(key, viewport()) .map(|snapshot| cells_to_text(&snapshot.cells)) .unwrap_or_default() } /// Scroll a view to its OLDEST retained row and read it back. fn oldest_view_text(state: &EditorState, key: TerminalViewKey) -> String { state .terminal_manager .borrow_mut() .scroll_view(key, viewport(), i32::MAX); view_text(state, key) } fn tick_until(state: &mut EditorState, needle: &str, buffer: pmacs::buffer::BufferId) -> bool { let deadline = Instant::now() + Duration::from_secs(5); loop { state.tick_processes(); if screen_text(state, buffer).contains(needle) { return true; } if Instant::now() >= deadline { return false; } thread::sleep(Duration::from_millis(20)); } } /// Give LOCAL a window on `buffer` and register/claim its terminal view, /// which is what makes `dispatch_key`'s terminal arm reachable. fn focus_terminal(state: &EditorState, buffer: pmacs::buffer::BufferId) -> WindowId { state.core.borrow_mut().switch_active_buffer(buffer).ok(); let window = state.core.borrow().active_window_id(); let key = TerminalViewKey::new(FrontendId::LOCAL, window, buffer); let mut manager = state.terminal_manager.borrow_mut(); manager.register_view(key); manager.claim_controller(key); let _ = manager.snapshot_for_view(key, viewport()); window } fn terminal_buffers(state: &EditorState) -> Vec { let manager = state.terminal_manager.borrow(); state .core .borrow() .registry .borrow() .ids() .iter() .copied() .filter(|id| manager.is_terminal(*id)) .collect() } /// Open a terminal from Lua and return the identity buffer it created. /// /// The id is derived by diffing the manager's terminal set rather than /// returned through Lua: `BufferIdLua` exposes no id accessor, and /// diffing also asserts in passing that exactly one terminal appeared. fn open_cat_terminal(state: &EditorState, lua_spec: &str) -> pmacs::buffer::BufferId { let before = terminal_buffers(state); exec( state, &format!("TERM_BUF = pmacs.terminal.open {{ {lua_spec} }}"), ); let after = terminal_buffers(state); let mut fresh: Vec<_> = after .into_iter() .filter(|id| !before.contains(id)) .collect(); assert_eq!(fresh.len(), 1, "exactly one terminal must have opened"); fresh.remove(0) } /// `cat -v` is the echo instrument, deliberately: the terminal screen /// rejects C0/C1 controls before they enter cells (Vterm Stage 1 /// criterion 2), so a raw echoed `Ctrl-X` would be invisible and a test /// probing for it could never pass. `-v` renders it as the printable /// two-character `^X`, which is what makes "the configured chord reached /// the child" observable at all. const CAT_PROFILE: &str = r#" pmacs.terminal.profiles.echo = { command = "/bin/sh", args = { "-c", "printf 'READY\r\n'; exec cat -v" }, } "#; /// Did the last key ARM the terminal escape? /// /// Observed behaviorally rather than through an accessor: while the /// escape is armed the next key goes to ordinary dispatch, so it never /// reaches the child. `cat` echoes anything that does reach it, which /// makes "the probe character did not appear" the exact observable for /// "that chord was consumed as the escape". fn escape_was_armed(state: &mut EditorState, buffer: pmacs::buffer::BufferId, probe: char) -> bool { // Count occurrences rather than testing for presence: the screen // already holds the child's own output, and a single-character probe // like 'R' collides with the "READY" banner. Only an INCREASE proves // this keystroke reached the child. let before = screen_text(state, buffer).matches(probe).count(); state.dispatch_key( FrontendId::LOCAL, KeyEvent::new(KeyCode::Char(probe), KeyModifiers::NONE), ); let deadline = Instant::now() + Duration::from_secs(2); loop { state.tick_processes(); if screen_text(state, buffer).matches(probe).count() > before { return false; } if Instant::now() >= deadline { return true; } thread::sleep(Duration::from_millis(20)); } } /// Acceptance 1: a profile spec is strict, and rejects before anything spawns. #[test] fn acc1_profile_specs_are_strict_and_reject_before_spawning() { let state = EditorState::new_with_roots(&crate::iso::roots()); let before = state.core.borrow().registry.borrow().ids().len(); exec( &state, r#"pmacs.terminal.profiles.bad = { command = "/bin/sh", nonsense = true }"#, ); let err = eval_err(&state, r#"return pmacs.terminal.open { profile = "bad" }"#); assert!( err.contains("unknown field") && err.contains("nonsense"), "the error must name the offending field: {err}" ); exec(&state, "pmacs.terminal.profiles.wrong = { command = 42 }"); let err = eval_err( &state, r#"return pmacs.terminal.open { profile = "wrong" }"#, ); assert!(err.contains("must be a string"), "typed field error: {err}"); assert_eq!( state.core.borrow().registry.borrow().ids().len(), before, "a rejected profile must create no buffer" ); assert_eq!(state.terminal_manager.borrow().len(), 0); } /// Acceptance 2: an unknown profile names the known ones and creates nothing. #[test] fn acc2_unknown_profile_lists_known_names_and_creates_nothing() { let state = EditorState::new_with_roots(&crate::iso::roots()); exec(&state, CAT_PROFILE); exec( &state, r#"pmacs.terminal.profiles.other = { command = "/bin/sh" }"#, ); let before = state.core.borrow().registry.borrow().ids().len(); // Via the default setting. exec( &state, r#"pmacs.config.set("terminal.default-profile", "ghost")"#, ); let err = eval_err(&state, "return pmacs.terminal.open {}"); assert!(err.contains("ghost"), "names the missing profile: {err}"); assert!( err.contains("echo") && err.contains("other"), "must LIST the known profiles: {err}" ); // An explicit bad profile fails even though the default is now valid — // a typo must not silently fall back (Q#TC3a). exec( &state, r#"pmacs.config.set("terminal.default-profile", "echo")"#, ); let err = eval_err(&state, r#"return pmacs.terminal.open { profile = "typo" }"#); assert!(err.contains("typo"), "explicit bad profile errors: {err}"); assert_eq!( state.core.borrow().registry.borrow().ids().len(), before, "no buffer, session, or process is created" ); assert_eq!(state.terminal_manager.borrow().len(), 0); } /// Acceptance 2 (malformed table): `pmacs.terminal.profiles` is a raw /// user table, so a diagnostic that walks its keys must be total over /// them. A table holding both a string and a numeric key made /// `table.sort` raise "attempt to compare number with string" — on the /// unknown-profile path, replacing the exact error being asked for. #[test] fn acc2_malformed_profile_keys_do_not_mask_the_unknown_profile_error() { let state = EditorState::new_with_roots(&crate::iso::roots()); exec(&state, CAT_PROFILE); exec( &state, r#"pmacs.terminal.profiles[1] = { command = "/bin/sh" }"#, ); let err = eval_err( &state, r#"return pmacs.terminal.open { profile = "ghost" }"#, ); assert!( err.contains("ghost") && err.contains("echo"), "the unknown-profile error must survive a malformed table: {err}" ); assert!( !err.contains("attempt to compare"), "listing known profiles must not raise: {err}" ); // Rendering the REQUESTED name is partial too: `%q` raises on a // table, and the name arrives straight from the caller. let err = eval_err(&state, r"return pmacs.terminal.open { profile = {} }"); assert!( err.contains("is not defined") && err.contains("known profiles"), "a non-string profile name must render, not raise: {err}" ); assert_eq!(state.terminal_manager.borrow().len(), 0); } /// Acceptance 3: explicit beats profile beats setting beats `$SHELL`, and /// `env` MERGES rather than replacing. #[test] fn acc3_field_resolution_order_and_env_merge() { let mut state = EditorState::new_with_roots(&crate::iso::roots()); exec( &state, r#" pmacs.terminal.profiles.merged = { command = "/bin/sh", args = { "-c", "printf 'PROFILE:%s:%s\r\n' \"$FROM_PROFILE\" \"$SHARED\"; exec cat" }, env = { FROM_PROFILE = "p", SHARED = "profile" }, } "#, ); let buffer = open_cat_terminal( &state, r#"profile = "merged", env = { SHARED = "explicit" }"#, ); assert!( tick_until(&mut state, "PROFILE:p:explicit", buffer), "profile env survives and explicit env overrides the same key: {:?}", screen_text(&state, buffer) ); state.process_supervisor.borrow_mut().shutdown(); } /// Acceptance 3 (explicit command wins) and 4 (`""` means no profile). #[test] fn acc3_acc4_explicit_command_wins_and_empty_default_means_no_profile() { let mut state = EditorState::new_with_roots(&crate::iso::roots()); exec(&state, CAT_PROFILE); exec( &state, r#"pmacs.config.set("terminal.default-profile", "echo")"#, ); // Explicit command beats the profile's. let explicit = open_cat_terminal( &state, r#"command = "/bin/sh", args = { "-c", "printf 'EXPLICIT\r\n'; exec cat" }"#, ); assert!(tick_until(&mut state, "EXPLICIT", explicit)); // `""` is the no-profile sentinel: falls through to $SHELL. exec( &state, r#"pmacs.config.set("terminal.default-profile", "")"#, ); let bare = open_cat_terminal(&state, ""); let spec_ok = state.terminal_manager.borrow().is_terminal(bare); assert!(spec_ok, "an empty default must open a $SHELL terminal"); assert!( !screen_text(&state, bare).contains("READY"), "the echo profile must NOT have been applied" ); state.process_supervisor.borrow_mut().shutdown(); } /// A child that overflows the 24-row screen and then goes quiet, so its /// early output can only still be found in RETAINED HISTORY. Zero-padded /// so `LINE001` is not a substring of `LINE100`. const FILL_PROFILE: &str = r#" pmacs.terminal.profiles.fill = { command = "/bin/sh", args = { "-c", "i=1; while [ $i -le 200 ]; do printf 'LINE%03d\r\n' $i; i=$((i+1)); done; printf 'DONE\r\n'; exec cat" }, } "#; /// Acceptance 5: the scrollback SETTING reaches the screen's retained /// history, an explicit spec value overrides it, and `0` is legal. /// /// Asserted end to end, through a real child and a real view, rather /// than by reading the value back out of the registry: a registry /// round-trip is a test of the registry, and would stay green with the /// setting's only consumer (`terminal.lua`'s `resolved.scrollback_rows` /// fallback) deleted outright. #[test] fn acc5_scrollback_setting_reaches_retained_history() { let mut state = EditorState::new_with_roots(&crate::iso::roots()); exec(&state, FILL_PROFILE); // Arm 1: `0` is legal, and means the early rows are GONE. exec(&state, r#"pmacs.config.set("terminal.scrollback-rows", 0)"#); let none = open_cat_terminal(&state, r#"profile = "fill""#); assert!(tick_until(&mut state, "DONE", none), "child finished"); let window = focus_terminal(&state, none); let none_key = TerminalViewKey::new(FrontendId::LOCAL, window, none); let oldest = oldest_view_text(&state, none_key); assert!( !oldest.contains("LINE001"), "with scrollback 0 the oldest retained row must not be the \ child's first line: {oldest:?}" ); // Arm 2: a large setting retains it, reachable by scrolling back. exec( &state, r#"pmacs.config.set("terminal.scrollback-rows", 10000)"#, ); let kept = open_cat_terminal(&state, r#"profile = "fill""#); assert!(tick_until(&mut state, "DONE", kept), "child finished"); let window = focus_terminal(&state, kept); let kept_key = TerminalViewKey::new(FrontendId::LOCAL, window, kept); let oldest = oldest_view_text(&state, kept_key); assert!( oldest.contains("LINE001"), "with scrollback 10000 the first line must survive in history: \ {oldest:?}" ); // Arm 3: an explicit spec value beats the setting, which is still 10000. let overridden = open_cat_terminal(&state, r#"profile = "fill", scrollback_rows = 0"#); assert!(tick_until(&mut state, "DONE", overridden), "child finished"); let window = focus_terminal(&state, overridden); let overridden_key = TerminalViewKey::new(FrontendId::LOCAL, window, overridden); let oldest = oldest_view_text(&state, overridden_key); assert!( !oldest.contains("LINE001"), "an explicit scrollback_rows = 0 must beat the setting: {oldest:?}" ); state.process_supervisor.borrow_mut().shutdown(); } /// Acceptance 5 (bounds): the registered range rejects out-of-range /// values, and `0` is inside it rather than a disabled sentinel. #[test] fn acc5_scrollback_bounds() { let state = EditorState::new_with_roots(&crate::iso::roots()); exec(&state, r#"pmacs.config.set("terminal.scrollback-rows", 0)"#); assert_eq!( state .lua_host .lua() .load(r#"return pmacs.config.get("terminal.scrollback-rows")"#) .eval::() .unwrap(), 0, "0 is a legal scrollback value meaning 'retain no history'" ); let err = eval_err( &state, r#"return pmacs.config.set("terminal.scrollback-rows", -1)"#, ); assert!( err.contains("-1") || err.contains("min"), "below range: {err}" ); let err = eval_err( &state, r#"return pmacs.config.set("terminal.scrollback-rows", 4000001)"#, ); assert!( err.contains("4000001") || err.contains("max"), "above range: {err}" ); } /// Acceptance 6 and 9: the configured chord escapes, repeating it sends /// THAT chord to the child, and an ordinary `C-c` still reaches the child. #[test] fn acc6_acc9_configured_escape_chord_and_literal_repeat() { let mut state = EditorState::new_with_roots(&crate::iso::roots()); exec(&state, CAT_PROFILE); let buffer = open_cat_terminal(&state, r#"profile = "echo""#); assert!(tick_until(&mut state, "READY", buffer)); focus_terminal(&state, buffer); exec(&state, r#"pmacs.config.set("terminal.escape-key", "C-x")"#); // `C-x C-x` must send Ctrl-X (0x18), which `cat` echoes back. Against // the pre-change hardcoded `&[0x03]` this sends Ctrl-C instead. state.dispatch_key( FrontendId::LOCAL, KeyEvent::new(KeyCode::Char('x'), KeyModifiers::CONTROL), ); state.dispatch_key( FrontendId::LOCAL, KeyEvent::new(KeyCode::Char('x'), KeyModifiers::CONTROL), ); assert!( tick_until(&mut state, "^X", buffer), "C-x C-x must send literal Ctrl-X: {:?}", screen_text(&state, buffer) ); // With the escape moved, an ordinary C-c is just another key. state.dispatch_key( FrontendId::LOCAL, KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL), ); assert!( tick_until(&mut state, "^C", buffer), "plain C-c must reach the child once the escape moved: {:?}", screen_text(&state, buffer) ); state.process_supervisor.borrow_mut().shutdown(); } /// Acceptance 7, 8 and 8a: per-terminal escape resolution, an A→B→A parse /// count that does not grow, and a cache that dies with its terminal. #[test] fn acc7_acc8_acc8a_per_terminal_escape_cache_identity_and_lifecycle() { let mut state = EditorState::new_with_roots(&crate::iso::roots()); exec(&state, CAT_PROFILE); let a = open_cat_terminal(&state, r#"profile = "echo""#); exec(&state, "TERM_A = TERM_BUF"); let b = open_cat_terminal(&state, r#"profile = "echo""#); exec(&state, "TERM_B = TERM_BUF"); assert!(tick_until(&mut state, "READY", a)); assert!(tick_until(&mut state, "READY", b)); // Different buffer-local escapes, then NO further writes. exec( &state, r#"pmacs.config.set_local(TERM_A, "terminal.escape-key", "C-x")"#, ); exec( &state, r#"pmacs.config.set_local(TERM_B, "terminal.escape-key", "C-b")"#, ); // Prime both caches. Each priming press ARMS the escape, so it is // consumed with a probe — otherwise the next chord would be read as // the escape repeat rather than a fresh escape. focus_terminal(&state, a); state.dispatch_key( FrontendId::LOCAL, KeyEvent::new(KeyCode::Char('x'), KeyModifiers::CONTROL), ); assert!(escape_was_armed(&mut state, a, 'M'), "A primes on its C-x"); focus_terminal(&state, b); state.dispatch_key( FrontendId::LOCAL, KeyEvent::new(KeyCode::Char('b'), KeyModifiers::CONTROL), ); assert!(escape_was_armed(&mut state, b, 'N'), "B primes on its C-b"); let primed = state.terminal_manager.borrow().escape_parses(); assert_eq!( state.terminal_manager.borrow().escape_caches(), 2, "each primed terminal holds its own cache" ); // Acceptance 7 — BOTH directions. Asserting only that A still works // after A->B->A is not enough: an epoch-only cache hands whichever // entry it finds to every terminal, so A keeps working by accident // while B silently inherits A's chord. The discriminating assertion // is that EACH terminal honors its OWN chord and NOT the other's. focus_terminal(&state, b); state.dispatch_key( FrontendId::LOCAL, KeyEvent::new(KeyCode::Char('b'), KeyModifiers::CONTROL), ); assert!( escape_was_armed(&mut state, b, 'R'), "terminal B must escape on its own C-b" ); // ...and A's chord must be ordinary input in B, not an escape. focus_terminal(&state, b); state.dispatch_key( FrontendId::LOCAL, KeyEvent::new(KeyCode::Char('x'), KeyModifiers::CONTROL), ); assert!( !escape_was_armed(&mut state, b, 'S'), "terminal A's C-x must NOT escape terminal B" ); focus_terminal(&state, a); state.dispatch_key( FrontendId::LOCAL, KeyEvent::new(KeyCode::Char('x'), KeyModifiers::CONTROL), ); assert!( escape_was_armed(&mut state, a, 'Q'), "terminal A must still escape on its own C-x after A->B->A" ); // Acceptance 8: that round trip parsed nothing new. A single // last-entry cache would have reparsed twice. assert_eq!( state.terminal_manager.borrow().escape_parses(), primed, "A->B->A with no setting written must not reparse" ); // Acceptance 8a: the cache dies with its terminal. // // Waiting for the SESSION count to fall is not the assertion — a // session set that drains while an editor-side `HashMap` keeps its entry (the rejected implementation named // in Q#TC4c, which has no purge hook) satisfies it exactly. The // discriminating observable is the CACHE count, which such a map // would hold at its high-water mark of 2. let sessions_before = state.terminal_manager.borrow().len(); exec(&state, "pmacs.terminal.terminate(TERM_A)"); exec(&state, "pmacs.buffer.kill(TERM_A)"); // Pruning is tick-driven (the manager reaps on the process tick), so // the session outlives the kill call by design. let deadline = Instant::now() + Duration::from_secs(5); while state.terminal_manager.borrow().len() >= sessions_before { state.tick_processes(); assert!( Instant::now() < deadline, "killing the terminal must remove its session" ); thread::sleep(Duration::from_millis(20)); } assert_eq!( state.terminal_manager.borrow().escape_caches(), 1, "killing terminal A must drop ITS cache, not merely its session" ); // ...and the surviving cache is B's, so the right one was dropped. focus_terminal(&state, b); state.dispatch_key( FrontendId::LOCAL, KeyEvent::new(KeyCode::Char('b'), KeyModifiers::CONTROL), ); assert!( escape_was_armed(&mut state, b, 'T'), "terminal B must still escape on its own C-b after A was killed" ); assert_eq!( state.terminal_manager.borrow().escape_parses(), primed, "B's surviving cache must not have been reparsed" ); state.process_supervisor.borrow_mut().shutdown(); } /// Acceptance 10 and 10a: an unparseable value falls back, reports through /// the status line, and reports once per terminal per effective bad value. #[test] fn acc10_acc10a_invalid_escape_falls_back_and_reports_once() { let mut state = EditorState::new_with_roots(&crate::iso::roots()); exec(&state, CAT_PROFILE); let buffer = open_cat_terminal(&state, r#"profile = "echo""#); assert!(tick_until(&mut state, "READY", buffer)); focus_terminal(&state, buffer); exec( &state, r#"pmacs.config.set("terminal.escape-key", "not-a-chord")"#, ); state.core.borrow_mut().status.clear(); // Acceptance 10: falls back to C-c, so the terminal stays escapable. state.dispatch_key( FrontendId::LOCAL, KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL), ); // Read the report BEFORE probing: `status` is a single slot, and the // probe key's own rejected self-insert would overwrite it. let reported = state.core.borrow().status.clone(); assert!( reported.contains("terminal.escape-key") && reported.contains("not-a-chord"), "the report must name the setting and the bad value: {reported:?}" ); assert!( escape_was_armed(&mut state, buffer, 'Q'), "an invalid escape-key must fall back to C-c, not leave the \ terminal unescapable" ); // Acceptance 10a: the same bad value does not report again. state.core.borrow_mut().status.clear(); state.dispatch_key( FrontendId::LOCAL, KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL), ); assert!( state.core.borrow().status.is_empty(), "an unchanged invalid value must not re-report: {:?}", state.core.borrow().status ); let _ = escape_was_armed(&mut state, buffer, 'W'); // A DIFFERENT bad value is new information, so it reports again. exec( &state, r#"pmacs.config.set("terminal.escape-key", "also-bad")"#, ); state.core.borrow_mut().status.clear(); state.dispatch_key( FrontendId::LOCAL, KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL), ); assert!( state.core.borrow().status.contains("also-bad"), "a different invalid value must report: {:?}", state.core.borrow().status ); state.process_supervisor.borrow_mut().shutdown(); } /// Acceptance 11: the opening binding exists, resolves to the command, and /// shadowed nothing (`keymap.bind` is strict, so loading the runtime at all /// proves the second half). #[test] fn acc11_terminal_opening_binding_is_bound_and_shadowed_nothing() { let state = EditorState::new_with_roots(&crate::iso::roots()); let command: Option = state .lua_host .lua() .load(r#"local d = pmacs.describe.key("C-c t"); return d and d.command"#) .eval() .expect("describe.key"); assert_eq!( command.as_deref(), Some("terminal"), "C-c t must open a terminal" ); } /// Acceptance 12: with no settings written and no profiles registered, the /// defaults reproduce the pre-arc behavior. #[test] fn acc12_defaults_reproduce_prior_behavior() { let state = EditorState::new_with_roots(&crate::iso::roots()); let lua = state.lua_host.lua(); assert_eq!( lua.load(r#"return pmacs.config.get("terminal.default-profile")"#) .eval::() .unwrap(), "" ); assert_eq!( lua.load(r#"return pmacs.config.get("terminal.scrollback-rows")"#) .eval::() .unwrap(), 10_000 ); assert_eq!( lua.load(r#"return pmacs.config.get("terminal.escape-key")"#) .eval::() .unwrap(), "C-c" ); assert!( lua.load("return next(pmacs.terminal.profiles) == nil") .eval::() .unwrap(), "no profiles are registered by default" ); } // Isolated bootstrap storage roots (see the module docs): an // integration test is compiled without `cfg(test)`, so a raw // `EditorState::new()` would read the developer's real `init.lua` and // write into their real data root. #[path = "common/iso.rs"] mod iso;