fix(gpu): preserve session isolation during target publish
Keep foreign BufferSnapshot publications out of existing semantic GPU sessions while retaining grid-replica coherence. Treat dead peer writes as peer-local failures, restore active-frontend cleanup, deterministic probe readiness, GPU logging, shared tilde expansion, and accurate docs. Add focused publication and cleanup coverage and record the two-window Wayland/Vulkan smoke plus the complete post-review gate results.
This commit is contained in:
parent
4f1651c8f0
commit
bef1c08133
|
|
@ -107,6 +107,7 @@ pmacs --gpu README.md # default instance; open one file
|
|||
pmacs --gpu --socket NAME FILE # named instance; bare NAME →
|
||||
# <runtime>/pmacs/NAME.sock
|
||||
pmacs --gpu -- --leading-dash # `--` ends option parsing
|
||||
```
|
||||
|
||||
`pmacs --gpu` requires the root `pmacs` binary to be built with the
|
||||
`crdt` feature. It discovers a sibling `pmacs-gpu` binary first, then
|
||||
|
|
|
|||
|
|
@ -64,17 +64,18 @@ If it does not, stop and repair the remote/fetch configuration.
|
|||
(folding Stage 1 #142); current canonical `main` @ `47581f4` is integrated
|
||||
conflict-free by merge `d6d4be6`. Protocol was v19 before this work.
|
||||
- State: implementation checkpoint `2dd30ec`; integrated, smoke-tested, fully
|
||||
gated, and published on 2026-07-23 at protocol v20. PR #148 is open for user
|
||||
review: `https://github.com/levineuwirth/pmacs/pull/148`.
|
||||
gated, and published on 2026-07-23 at protocol v20. PR #148 review fixes are
|
||||
complete locally and awaiting publication; PR:
|
||||
`https://github.com/levineuwirth/pmacs/pull/148`.
|
||||
- Scope delivered: one session-scoped `pmacs --gpu [--socket …] FILE` target,
|
||||
protocol-v20 semantic bootstrap, launcher-owned tilde/cwd resolution, exact
|
||||
Unix path transport, pre-window target readiness, replica coherence, and the
|
||||
approved behavioral acceptance matrix.
|
||||
- Verification after current-main integration: formatting and strict Clippy;
|
||||
1,800 default + 1,976 CRDT library tests; target gate 1 default + 13 CRDT;
|
||||
M4 121; required GPU 152; Vterm Stage 3 5 default + 7 CRDT; workspace sweep
|
||||
3,268 across 87 suites.
|
||||
A coherent release launch displayed `README.md` first at protocol v20.
|
||||
- Post-review verification: formatting and strict Clippy; 1,800 default + 1,977
|
||||
CRDT library tests; target gate 1 default + 13 CRDT; M4 121; required GPU 152;
|
||||
Vterm Stage 3 5 default + 7 CRDT; isolated-config workspace sweep 3,269 across
|
||||
87 suites. Two concurrent real Wayland/Vulkan GPU windows remained on distinct
|
||||
target buffers after the second attach.
|
||||
- Deferred unchanged: automatic GUI selection, multiple files, general
|
||||
live-open commands, packaging, and remote GPU paths.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
# Agent handoff — cross-machine continuity
|
||||
|
||||
**Last updated: 2026-07-23, after GPU initial-target implementation completed
|
||||
on branch `gpu-initial-target` (protocol v20, PR pending), following one-command
|
||||
GPU invocation (#141), the documentation refresh (#140), Vterm Stage 3 (#135),
|
||||
tab-width rendering parity (#137), locals-query processing (#134), modeline
|
||||
detection (#132), mode system wiring (#129), config registry (#127), Vterm
|
||||
Stages 1–2 (#126/#130), and completed Themes Arc 4 (#120/#124/#125).**
|
||||
**Last updated: 2026-07-23, after GPU initial-target PR #148 review fixes and
|
||||
verification completed on branch `gpu-initial-target` (protocol v20), following
|
||||
one-command GPU invocation (#141), the documentation refresh (#140), Vterm
|
||||
Stage 3 (#135), tab-width rendering parity (#137), locals-query processing
|
||||
(#134), modeline detection (#132), mode system wiring (#129), config registry
|
||||
(#127), Vterm Stages 1–2 (#126/#130), and completed Themes Arc 4
|
||||
(#120/#124/#125).**
|
||||
This file is the
|
||||
bridge between development machines. If you are an agent reading
|
||||
this on a fresh clone: this document plus the `docs/*-framing.md`
|
||||
|
|
@ -22,7 +23,7 @@ commands, read `docs/active-work.md` immediately after this file.
|
|||
- `main` @ `47581f4` (web grammars #146 atop folding Stage 1 #142,
|
||||
inline-math framing #145, and one-command GPU invocation #141), protocol
|
||||
**v19** (`SUPPORTED=[6..=19]`; v19 = terminal frames/events).
|
||||
- **GPU INITIAL TARGET IMPLEMENTED — PR pending**
|
||||
- **GPU INITIAL TARGET IMPLEMENTED — PR #148 under user review**
|
||||
(`docs/gpu-initial-target-framing.md` rev 3; branch `gpu-initial-target`).
|
||||
`pmacs --gpu [--socket NAME|PATH] FILE` now transports exact Unix path bytes
|
||||
plus launcher cwd to the managed GPU client. Protocol v20 adds a
|
||||
|
|
@ -30,12 +31,14 @@ commands, read `docs/active-work.md` immediately after this file.
|
|||
appended `InitialTargetResult` readiness barrier; v6–v19 wire encodings stay
|
||||
pinned. The daemon resolves the path lexically, deduplicates or loads/creates
|
||||
it in the authenticated frontend's view, runs the established load/switch
|
||||
hooks, upgrades the buffer for CRDT, publishes fresh buffers to existing
|
||||
replicas, and sends the target snapshot before readiness. Failed bootstrap
|
||||
removes the provisional session without poisoning the daemon. Existing
|
||||
no-target managed launch, direct attach, TUI, and legacy protocol behavior
|
||||
remain intact. See `docs/active-work.md` for the portable checkpoint and
|
||||
verification.
|
||||
hooks, upgrades the buffer for CRDT, publishes fresh buffers to existing grid
|
||||
replicas, and sends the target snapshot before readiness. Semantic replicas
|
||||
receive a publication only when displaying that buffer, so a second target
|
||||
launch cannot switch an existing GPU window; one dead peer cannot fail the
|
||||
new session. Failed bootstrap removes the provisional session and restores
|
||||
the ambient active frontend without poisoning the daemon. Existing no-target
|
||||
managed launch, direct attach, TUI, and legacy protocol behavior remain
|
||||
intact. See `docs/active-work.md` for the portable checkpoint and verification.
|
||||
- **One-command GPU invocation LANDED — #141**
|
||||
(`docs/gpu-invocation-framing.md` rev 6; merge `63fbc66`; two implementation
|
||||
reviews). The additive public path is `pmacs --gpu [--socket NAME|PATH]`;
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ the implementation base includes folding Stage 1 through `c49a8c7`.**
|
|||
Revision 3 records the completed implementation and verification. Revision 2
|
||||
pinned launcher-owned tilde expansion, required `after-switch` even when dedup
|
||||
selects the view's existing buffer, failed bootstrap when a hook kills the
|
||||
target, and recorded the deliberate stderr-only wait during slow pre-window
|
||||
bootstrap. It also sharpened the observed argv panic and negotiated
|
||||
target, and kept slow pre-window bootstrap terminal-only (no graphical
|
||||
progress surface). It also sharpened the observed argv panic and negotiated
|
||||
protocol-version echo.
|
||||
|
||||
One-command GPU startup landed in #141:
|
||||
|
|
@ -344,13 +344,12 @@ daemon detail. Managed startup returns nonzero; root reflects that status.
|
|||
The daemon itself remains alive, whether reused or newly spawned.
|
||||
|
||||
Because the connector waits before winit creates a window, slow dispatcher
|
||||
work has no graphical “Connecting…” surface. This is deliberate for the
|
||||
explicit terminal command: before blocking, `pmacs-gpu` writes one bounded,
|
||||
lossy-display-only `opening …` notice to stderr. There is no second target
|
||||
timeout beyond #141's bounded daemon-start retry; file I/O and user hooks may
|
||||
legitimately exceed five seconds, and timing out the client would not cancel
|
||||
dispatcher work. Ctrl-C remains the escape hatch and still cannot reach the
|
||||
isolated daemon process group.
|
||||
work has no graphical “Connecting…” surface. The managed GPU child waits
|
||||
silently and reports a failure on stderr if bootstrap fails. There is no
|
||||
second target timeout beyond #141's bounded daemon-start retry; file I/O and
|
||||
user hooks may legitimately exceed five seconds, and timing out the client
|
||||
would not cancel dispatcher work. Ctrl-C remains the escape hatch and still
|
||||
cannot reach the isolated daemon process group.
|
||||
|
||||
### Q#GT9 — Failure cleanup never creates a ghost session
|
||||
|
||||
|
|
@ -524,8 +523,9 @@ process behavior.
|
|||
pointed usage error rather than panic or replacement characters.
|
||||
3. **Private GPU grammar:** managed and headless modes accept either their
|
||||
unchanged no-target arity or exactly `--initial-target CWD FILE`; missing,
|
||||
trailing, duplicated, or relative-cwd forms exit 2. An option-like `FILE`
|
||||
after the marker remains literal.
|
||||
trailing, or duplicated forms exit 2. An option-like `FILE` after the marker
|
||||
remains literal. A relative `CWD` is syntactically accepted and then fails
|
||||
the daemon's bootstrap validation before readiness.
|
||||
4. **v20 wire and legacy pins:** bootstrap request/result round trips preserve
|
||||
arbitrary Unix bytes and enforce bounds. Every pinned v6–v19 encoding stays
|
||||
unchanged; the new result discriminant is appended. The supported ladder is
|
||||
|
|
@ -616,20 +616,25 @@ Also rerun the touched GPU invocation suite, protocol/transport tests, and
|
|||
Vterm Stage 3 acceptance in default and CRDT configurations where the suite
|
||||
supports both. The final full workspace sweep remains required before PR.
|
||||
|
||||
As-built verification on 2026-07-23:
|
||||
The named gate intentionally reuses the managed-lifecycle acceptance module,
|
||||
so a workspace sweep executes those 13 CRDT cases under both test-binary
|
||||
names. The duplicate runtime is retained to keep the approved named command
|
||||
and the complete #141 lifecycle fixture coverage together.
|
||||
|
||||
Post-review verification on 2026-07-23:
|
||||
|
||||
- `cargo fmt --check` and strict workspace Clippy passed.
|
||||
- Library gates passed 1,800 default and 1,976 CRDT tests.
|
||||
- Library gates passed 1,800 default and 1,977 CRDT tests.
|
||||
- The named initial-target gate passed 1 default and 13 CRDT tests; the
|
||||
underlying GPU invocation suite passed 13 CRDT tests.
|
||||
- M4 passed 121 tests with the documented basedpyright skip; required real-GPU
|
||||
tests passed 152.
|
||||
- Vterm Stage 3 passed 5 default and 7 CRDT tests.
|
||||
- The workspace CRDT sweep passed 3,268 tests across 87 suites, with 29 ignored
|
||||
and the documented basedpyright case filtered.
|
||||
- A coherent release build launched `target/release/pmacs --gpu --socket
|
||||
initial-target-smoke README.md` on the real Wayland/Vulkan workstation,
|
||||
attached at protocol v20, and displayed README rather than scratch.
|
||||
- The isolated-config workspace CRDT sweep passed 3,269 tests across 87 suites,
|
||||
with 29 ignored and the documented basedpyright case filtered.
|
||||
- A coherent release build launched two concurrent real Wayland/Vulkan GPU
|
||||
windows on one daemon, targeting distinct `alpha` and `beta` files. Both
|
||||
remained visible on their own buffer after the second target attached.
|
||||
|
||||
## Deferred (named)
|
||||
|
||||
|
|
|
|||
|
|
@ -478,10 +478,7 @@ fn read_initial_target_bootstrap(
|
|||
other => {
|
||||
return Err(AttachClientError::InitialTargetProtocol(format!(
|
||||
"unexpected {} before target readiness",
|
||||
match other {
|
||||
InstanceMessage::InitialTargetResult(_) => "InitialTargetResult",
|
||||
_ => "instance message",
|
||||
}
|
||||
crate::instance_message_label(&other)
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -593,6 +593,7 @@ fn decimal_digits(mut n: usize) -> u32 {
|
|||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
let mode = match parse_args(&std::env::args_os().skip(1).collect::<Vec<_>>()) {
|
||||
Ok(mode) => mode,
|
||||
Err(error) => {
|
||||
|
|
@ -861,8 +862,7 @@ fn run_headless_managed_probe(
|
|||
use std::sync::mpsc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
let (event_tx, event_rx) = mpsc::channel::<AttachEvent>();
|
||||
let connector_tx = event_tx.clone();
|
||||
let (connector_tx, event_rx) = mpsc::channel::<AttachEvent>();
|
||||
let managed = match attach::connect_managed_with_target_and_sink(
|
||||
socket,
|
||||
daemon_executable,
|
||||
|
|
@ -878,9 +878,10 @@ fn run_headless_managed_probe(
|
|||
}
|
||||
};
|
||||
let mut client = managed.client;
|
||||
if let Some(message) = client.take_initial_message() {
|
||||
let _ = event_tx.send(AttachEvent::Message(Box::new(message)));
|
||||
}
|
||||
let initial_target_ready = matches!(
|
||||
client.take_initial_message(),
|
||||
Some(InstanceMessage::BufferSnapshot { .. })
|
||||
);
|
||||
let daemon = managed.daemon;
|
||||
let protocol = client.server_protocol_version();
|
||||
|
||||
|
|
@ -895,12 +896,22 @@ fn run_headless_managed_probe(
|
|||
.expect("spawn managed probe stdin reader");
|
||||
|
||||
let deadline = Instant::now() + Duration::from_secs(20);
|
||||
let mut ready = false;
|
||||
let mut ready = initial_target_ready;
|
||||
let mut stdin_closed = false;
|
||||
let mut disconnect = String::new();
|
||||
let mut last_reaped = false;
|
||||
let mut last_wait_result = None;
|
||||
let mut last_disconnect = String::new();
|
||||
if ready
|
||||
&& let Err(error) =
|
||||
write_managed_probe_report(report, "ready", protocol, &daemon, &disconnect)
|
||||
{
|
||||
eprintln!(
|
||||
"pmacs-gpu managed probe: writing {} failed: {error}",
|
||||
report.display()
|
||||
);
|
||||
return 5;
|
||||
}
|
||||
loop {
|
||||
if stdin_rx.try_recv().is_ok() {
|
||||
stdin_closed = true;
|
||||
|
|
|
|||
173
src/daemon.rs
173
src/daemon.rs
|
|
@ -1085,12 +1085,12 @@ fn dispatcher_loop(
|
|||
.is_some_and(|s| s.negotiated_capabilities.crdt_replica)
|
||||
{
|
||||
// F29 — when a mid-session upgrade occurs, push a
|
||||
// `BufferSnapshot` for the newly-CRDT-backed buffer
|
||||
// to every currently-attached replica so their
|
||||
// `BufferMirror`s gain an entry for it. Without
|
||||
// this, replicas attached before the upgrade
|
||||
// permanently fall back to v0.1 round-trip on that
|
||||
// buffer.
|
||||
// `BufferSnapshot` to every grid replica so its
|
||||
// `BufferMirror` gains an entry for the buffer. A
|
||||
// semantic replica receives it only when that replica
|
||||
// is displaying this buffer: applying a foreign-buffer
|
||||
// snapshot would switch the GPU window away from its
|
||||
// own active view.
|
||||
if let Some(upgraded) = ensure_active_buffer_crdt_backed(editor, *fid) {
|
||||
broadcast_buffer_snapshot_to_replicas(
|
||||
editor,
|
||||
|
|
@ -1786,25 +1786,14 @@ fn handle_session_established(
|
|||
crdt_snapshot: snapshot,
|
||||
};
|
||||
if opened.publish_to_replicas {
|
||||
for (peer_id, peer_stream) in streams.iter_mut() {
|
||||
let is_replica = session_registry
|
||||
.session_state(*peer_id)
|
||||
.is_some_and(|state| state.negotiated_capabilities.crdt_replica);
|
||||
if is_replica && let Err(error) = write_message(peer_stream, &snapshot_message) {
|
||||
send_initial_target_failure(
|
||||
&mut write_stream,
|
||||
format!("cannot publish initial target snapshot to {peer_id:?}: {error}"),
|
||||
);
|
||||
editor
|
||||
.core
|
||||
.borrow_mut()
|
||||
.unregister_frontend_view(frontend_id);
|
||||
return;
|
||||
}
|
||||
if is_replica && let Some(state) = semantic_states.get_mut(peer_id) {
|
||||
state.on_buffer_snapshot_sent(opened.buffer_id);
|
||||
}
|
||||
}
|
||||
publish_buffer_snapshot_to_replicas(
|
||||
editor,
|
||||
opened.buffer_id,
|
||||
&snapshot_message,
|
||||
session_registry,
|
||||
streams,
|
||||
semantic_states,
|
||||
);
|
||||
}
|
||||
if write_message(&mut write_stream, &snapshot_message).is_err() {
|
||||
editor
|
||||
|
|
@ -2400,28 +2389,56 @@ fn broadcast_buffer_snapshot_to_replicas(
|
|||
streams: &mut HashMap<FrontendId, UnixStream>,
|
||||
semantic_states: &mut HashMap<FrontendId, crate::semantic_render::SemanticRenderState>,
|
||||
) {
|
||||
let Some(snapshot_bytes) = export_buffer_snapshot(editor, buffer_id) else {
|
||||
let Some(snapshot) = export_buffer_snapshot(editor, buffer_id) else {
|
||||
return;
|
||||
};
|
||||
let msg = InstanceMessage::BufferSnapshot {
|
||||
let message = InstanceMessage::BufferSnapshot {
|
||||
buffer_id,
|
||||
crdt_snapshot: snapshot_bytes,
|
||||
crdt_snapshot: snapshot,
|
||||
};
|
||||
for (fid, stream) in streams.iter_mut() {
|
||||
let is_replica = session_registry
|
||||
.session_state(*fid)
|
||||
.is_some_and(|s| s.negotiated_capabilities.crdt_replica);
|
||||
if !is_replica {
|
||||
publish_buffer_snapshot_to_replicas(
|
||||
editor,
|
||||
buffer_id,
|
||||
&message,
|
||||
session_registry,
|
||||
streams,
|
||||
semantic_states,
|
||||
);
|
||||
}
|
||||
|
||||
fn publish_buffer_snapshot_to_replicas(
|
||||
editor: &EditorState,
|
||||
buffer_id: crate::buffer::BufferId,
|
||||
message: &InstanceMessage,
|
||||
session_registry: &SessionRegistry,
|
||||
streams: &mut HashMap<FrontendId, UnixStream>,
|
||||
semantic_states: &mut HashMap<FrontendId, crate::semantic_render::SemanticRenderState>,
|
||||
) {
|
||||
for (peer_id, stream) in streams {
|
||||
let Some(session) = session_registry.session_state(*peer_id) else {
|
||||
continue;
|
||||
};
|
||||
if !session.negotiated_capabilities.crdt_replica {
|
||||
continue;
|
||||
}
|
||||
if let Err(e) = write_message(stream, &msg) {
|
||||
eprintln!("pmacs: F29 send BufferSnapshot for {buffer_id:?} to {fid:?} failed: {e}");
|
||||
if session.negotiated_capabilities.semantic_render {
|
||||
let displays_buffer = editor
|
||||
.core
|
||||
.borrow()
|
||||
.active_window_for(*peer_id)
|
||||
.is_some_and(|window| window.buffer_id == buffer_id);
|
||||
if !displays_buffer {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// PR #120 round 2 — same reset contract as the follow path:
|
||||
// the snapshot wiped this replica's buffer-scoped render
|
||||
// state, so its emission baselines for the buffer die too.
|
||||
if let Some(sem) = semantic_states.get_mut(fid) {
|
||||
sem.on_buffer_snapshot_sent(buffer_id);
|
||||
if let Err(error) = write_message(stream, message) {
|
||||
eprintln!(
|
||||
"pmacs: BufferSnapshot publish for {buffer_id:?} to {peer_id:?} failed: {error}"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if let Some(semantic) = semantic_states.get_mut(peer_id) {
|
||||
semantic.on_buffer_snapshot_sent(buffer_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3151,6 +3168,7 @@ mod tests {
|
|||
let semantic = crate::protocol::NegotiatedCapabilities {
|
||||
multi_frontend: true,
|
||||
crdt_replica: true,
|
||||
|
||||
semantic_render: true,
|
||||
};
|
||||
let old_peer = FrontendId(2);
|
||||
|
|
@ -3170,6 +3188,81 @@ mod tests {
|
|||
assert!(!peer_declared_terminal_support(®istry, FrontendId(99)));
|
||||
}
|
||||
|
||||
#[cfg(feature = "crdt")]
|
||||
#[test]
|
||||
fn snapshot_publication_skips_foreign_semantic_views_and_ignores_dead_peers() {
|
||||
let mut editor = EditorState::new();
|
||||
let semantic_peer = FrontendId(20);
|
||||
let live_grid_peer = FrontendId(21);
|
||||
let dead_grid_peer = FrontendId(22);
|
||||
let semantic_view = build_fresh_frontend_view(&mut editor);
|
||||
editor
|
||||
.core
|
||||
.borrow_mut()
|
||||
.register_frontend_view(semantic_peer, semantic_view);
|
||||
|
||||
let semantic_caps = crate::protocol::NegotiatedCapabilities {
|
||||
multi_frontend: true,
|
||||
crdt_replica: true,
|
||||
semantic_render: true,
|
||||
};
|
||||
let grid_caps = crate::protocol::NegotiatedCapabilities {
|
||||
semantic_render: false,
|
||||
..semantic_caps
|
||||
};
|
||||
let mut registry = SessionRegistry::new();
|
||||
registry.register_session(
|
||||
semantic_peer,
|
||||
crate::presence::SessionState::new(PROTOCOL_VERSION, semantic_caps, 0),
|
||||
);
|
||||
registry.register_session(
|
||||
live_grid_peer,
|
||||
crate::presence::SessionState::new(PROTOCOL_VERSION, grid_caps, 1),
|
||||
);
|
||||
registry.register_session(
|
||||
dead_grid_peer,
|
||||
crate::presence::SessionState::new(PROTOCOL_VERSION, grid_caps, 2),
|
||||
);
|
||||
|
||||
let (semantic_server, mut semantic_client) =
|
||||
UnixStream::pair().expect("semantic socketpair");
|
||||
let (live_grid_server, mut live_grid_client) =
|
||||
UnixStream::pair().expect("live grid socketpair");
|
||||
let (dead_grid_server, dead_grid_client) =
|
||||
UnixStream::pair().expect("dead grid socketpair");
|
||||
drop(dead_grid_client);
|
||||
let mut streams = HashMap::from([
|
||||
(semantic_peer, semantic_server),
|
||||
(live_grid_peer, live_grid_server),
|
||||
(dead_grid_peer, dead_grid_server),
|
||||
]);
|
||||
let published_buffer = crate::buffer::BufferId::from_raw(900);
|
||||
let message = InstanceMessage::BufferSnapshot {
|
||||
buffer_id: published_buffer,
|
||||
crdt_snapshot: vec![1, 2, 3],
|
||||
};
|
||||
|
||||
publish_buffer_snapshot_to_replicas(
|
||||
&editor,
|
||||
published_buffer,
|
||||
&message,
|
||||
®istry,
|
||||
&mut streams,
|
||||
&mut HashMap::new(),
|
||||
);
|
||||
|
||||
let delivered: InstanceMessage =
|
||||
read_message(&mut live_grid_client).expect("live grid snapshot");
|
||||
assert_eq!(delivered, message);
|
||||
semantic_client
|
||||
.set_read_timeout(Some(Duration::from_millis(50)))
|
||||
.expect("semantic timeout");
|
||||
assert!(
|
||||
read_message::<InstanceMessage>(&mut semantic_client).is_err(),
|
||||
"a semantic peer displaying another buffer must receive no snapshot"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_identity_includes_version_and_uptime() {
|
||||
let s = DaemonState::new(Some("research".into()));
|
||||
|
|
|
|||
|
|
@ -548,6 +548,9 @@ impl EditorCore {
|
|||
/// closing a window left others intact).
|
||||
pub fn unregister_frontend_view(&mut self, fid: FrontendId) {
|
||||
self.views.remove(&fid);
|
||||
if self.active_frontend == fid {
|
||||
self.active_frontend = FrontendId::LOCAL;
|
||||
}
|
||||
}
|
||||
|
||||
/// [`BufferId`] of the active window's buffer.
|
||||
|
|
@ -3214,7 +3217,7 @@ fn normalize_buffer_path(path: PathBuf) -> PathBuf {
|
|||
/// `~` becomes `$HOME`; `~/x` becomes `$HOME/x`. `~user` is left
|
||||
/// untouched (no passwd lookup). Returns the input unchanged if it
|
||||
/// has no leading `~`, isn't valid UTF-8, or `$HOME` is unset.
|
||||
fn expand_tilde(path: PathBuf) -> PathBuf {
|
||||
pub fn expand_tilde(path: PathBuf) -> PathBuf {
|
||||
let Some(s) = path.to_str() else {
|
||||
return path;
|
||||
};
|
||||
|
|
@ -3713,13 +3716,16 @@ mod tests {
|
|||
// we don't need a fresh window allocation in this test.
|
||||
let local_view = s.views[&FrontendId::LOCAL].clone();
|
||||
s.register_frontend_view(fid, local_view);
|
||||
s.active_frontend = fid;
|
||||
assert!(s.active_window_for(fid).is_some());
|
||||
|
||||
// Unregister drops the entry; explicit lookup returns None.
|
||||
s.unregister_frontend_view(fid);
|
||||
assert!(s.active_window_for(fid).is_none());
|
||||
|
||||
// LOCAL invariant survives unrelated register/unregister.
|
||||
// Removing the selected frontend restores the always-registered
|
||||
// LOCAL view as the ambient fallback.
|
||||
assert_eq!(s.active_frontend, FrontendId::LOCAL);
|
||||
assert!(s.views.contains_key(&FrontendId::LOCAL));
|
||||
}
|
||||
|
||||
|
|
|
|||
26
src/main.rs
26
src/main.rs
|
|
@ -315,21 +315,6 @@ fn gpu_binary(current_exe: &Path, override_bin: Option<PathBuf>) -> (PathBuf, Pa
|
|||
(PathBuf::from("pmacs-gpu"), sibling)
|
||||
}
|
||||
|
||||
fn expand_launcher_tilde(path: &Path) -> PathBuf {
|
||||
let Some(path_text) = path.to_str() else {
|
||||
return path.to_owned();
|
||||
};
|
||||
if path_text == "~" {
|
||||
return std::env::var_os("HOME").map_or_else(|| path.to_owned(), PathBuf::from);
|
||||
}
|
||||
if let Some(rest) = path_text.strip_prefix("~/")
|
||||
&& let Some(home) = std::env::var_os("HOME")
|
||||
{
|
||||
return Path::new(&home).join(rest);
|
||||
}
|
||||
path.to_owned()
|
||||
}
|
||||
|
||||
fn run_gpu(socket: Option<&str>, file: Option<&Path>) -> ExitCode {
|
||||
if !cfg!(feature = "crdt") {
|
||||
eprintln!("pmacs: --gpu requires pmacs built with --features crdt");
|
||||
|
|
@ -353,7 +338,7 @@ fn run_gpu(socket: Option<&str>, file: Option<&Path>) -> ExitCode {
|
|||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
Some((cwd, expand_launcher_tilde(path)))
|
||||
Some((cwd, pmacs::editor_core::expand_tilde(path.to_owned())))
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
|
@ -909,13 +894,16 @@ mod tests {
|
|||
}
|
||||
|
||||
let home = std::env::var_os("HOME").expect("test HOME");
|
||||
assert_eq!(expand_launcher_tilde(Path::new("~")), PathBuf::from(&home));
|
||||
assert_eq!(
|
||||
expand_launcher_tilde(Path::new("~/notes")),
|
||||
pmacs::editor_core::expand_tilde(PathBuf::from("~")),
|
||||
PathBuf::from(&home)
|
||||
);
|
||||
assert_eq!(
|
||||
pmacs::editor_core::expand_tilde(PathBuf::from("~/notes")),
|
||||
PathBuf::from(home).join("notes")
|
||||
);
|
||||
assert_eq!(
|
||||
expand_launcher_tilde(Path::new("~other/notes")),
|
||||
pmacs::editor_core::expand_tilde(PathBuf::from("~other/notes")),
|
||||
PathBuf::from("~other/notes")
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue