// instance_buffer.rs --- T M5.6f *pmacs-instance* observability buffer renderer. //! `*pmacs-instance*` buffer rendering ([T M5.6f]). //! //! The Rust side of the `editor.describe-instance` and //! `editor.describe-instance-buffer` commands. Mirrors the renderer //! pattern in [`crate::workers_buffer`]: a description goes in, //! formatted text replaces the buffer's contents, the buffer is marked //! clean. //! //! # Two surfaces //! //! * [`format_echo_line`] returns a single-line summary suitable for //! the status line. Used by `editor.describe-instance`. //! * [`render`] writes a multi-section detail view into the //! `*pmacs-instance*` buffer. Used by //! `editor.describe-instance-buffer`. //! //! Both consume an [`InstanceIdentity`] for the running process plus //! an optional [`AttachmentHandle`]. In v0.1 Local mode the attachment //! is always `None` (the process is its own instance), so the //! formatters degrade gracefully to "describe self." Once the v0.2 //! reconnect / outbound attach path lands, the same formatters work //! for the attached case without changes. use std::fmt::Write; use crate::buffer::{Buffer, BufferId, EditOp}; use crate::buffer_registry::BufferRegistry; use crate::protocol::{AttachmentHandle, InstanceIdentity}; /// Canonical name for the instance description buffer. pub const INSTANCE_BUFFER_NAME: &str = "*pmacs-instance*"; /// One-line summary for the status row. /// /// Two shapes, depending on whether `attachment` is set: /// /// * `None`: `pmacs []: (uptime )` — the /// running process describes itself. /// * `Some(_)`: `pmacs attached to () /// from , uptime ` — the running process is talking /// to a remote instance whose identity it has captured. #[must_use] pub fn format_echo_line( identity: &InstanceIdentity, attachment: Option<&AttachmentHandle>, ) -> String { let uptime = format_uptime(identity.uptime_secs); match attachment { None => { let name = identity.instance_name.as_deref().unwrap_or("local"); format!( "pmacs {ver} [{name}]: {cwd} (uptime {uptime})", ver = identity.pmacs_version, cwd = identity.working_directory, ) } Some(h) => { let attached_name = h.identity.instance_name.as_deref().unwrap_or("anonymous"); format!( "pmacs {ver} attached to {attached_name} ({target}) from {cwd}, uptime {uptime}", ver = identity.pmacs_version, target = h.target, cwd = identity.working_directory, ) } } } /// Render the full description into the `*pmacs-instance*` buffer /// (creating it if absent), replacing its full contents. Returns /// the buffer id and the Edits produced by the replacement. /// /// # Post-audit-round-6 F31 — broadcast queueing /// /// Returning the Edits lets the caller queue any `crdt_op` they /// carry via `EditorCore::queue_daemon_origin_crdt_op`. See the /// equivalent doc on `workers_buffer::render`. /// /// The buffer is marked clean — the modeline shouldn't claim unsaved /// changes for a generated buffer. pub fn render( registry: &mut BufferRegistry, identity: &InstanceIdentity, attachment: Option<&AttachmentHandle>, ) -> (BufferId, Vec) { let text = format_full_text(identity, attachment); let id = registry .find_by_name(INSTANCE_BUFFER_NAME) .unwrap_or_else(|| registry.create(INSTANCE_BUFFER_NAME)); let buf = registry.get_mut(id).expect("just resolved"); let mut edits = Vec::new(); if buffer_contents_equal(buf, &text) { buf.mark_clean(); return (id, edits); } if !buf.is_empty() { let len = buf.len(); if let Ok(edit) = buf.apply_edit(EditOp::Delete { range: crate::rope::Range::new(0, len), }) { edits.push(edit); } } if !text.is_empty() && let Ok(edit) = buf.apply_edit(EditOp::Insert { pos: 0, bytes: text.as_bytes(), }) { edits.push(edit); } buf.mark_clean(); (id, edits) } fn buffer_contents_equal(buf: &Buffer, text: &str) -> bool { if buf.len() != text.len() as u64 { return false; } let mut bytes = vec![0u8; text.len()]; if !bytes.is_empty() { buf.snapshot_rope().slice(0, buf.len(), &mut bytes); } bytes == text.as_bytes() } /// Multi-section text payload for the buffer view. Sections: /// /// 1. **This instance** — version, build hash, name, cwd, uptime. /// 2. **Attachment** — only when `attachment` is `Some`. Shows the /// target string and the remote identity. /// 3. **Notes** — short hints about how to dismiss the buffer /// (`q` keybinding) and about the v0.1 single-instance mode. #[must_use] pub fn format_full_text( identity: &InstanceIdentity, attachment: Option<&AttachmentHandle>, ) -> String { let mut text = String::new(); let _ = writeln!(text, "This instance"); let _ = writeln!(text, " version: {}", identity.pmacs_version); let _ = writeln!( text, " build hash: {}", identity.build_hash.as_deref().unwrap_or("(unknown)") ); let _ = writeln!( text, " instance name: {}", identity.instance_name.as_deref().unwrap_or("(unnamed)") ); let _ = writeln!(text, " working directory: {}", identity.working_directory); let _ = writeln!( text, " uptime: {} ({} seconds)", format_uptime(identity.uptime_secs), identity.uptime_secs ); let _ = writeln!(text); match attachment { None => { let _ = writeln!(text, "Attachment"); let _ = writeln!( text, " (no outbound attachment — this process is its own instance)" ); } Some(h) => { let _ = writeln!(text, "Attachment"); let _ = writeln!(text, " frontend id: {}", h.frontend_id.0); let _ = writeln!(text, " target: {}", h.target); let _ = writeln!(text, " kind: {}", h.target.kind_name()); let _ = writeln!(text, " remote version: {}", h.identity.pmacs_version); let _ = writeln!( text, " remote name: {}", h.identity.instance_name.as_deref().unwrap_or("(unnamed)") ); let _ = writeln!( text, " remote cwd: {}", h.identity.working_directory ); let _ = writeln!( text, " remote uptime: {} ({} seconds)", format_uptime(h.identity.uptime_secs), h.identity.uptime_secs ); if let Some(hash) = h.identity.build_hash.as_deref() { let _ = writeln!(text, " remote build hash: {hash}"); } } } let _ = writeln!(text); let _ = writeln!(text, "Notes"); let _ = writeln!(text, " press `q` to kill this buffer."); let _ = writeln!( text, " the buffer is regenerated each time you run `editor.describe-instance-buffer`." ); text } /// Render `seconds` as a short human-readable duration. Mirrors the /// scale buckets in `crate::workers_buffer::format_duration_ms` but in /// seconds (uptimes are rarely sub-second-interesting). fn format_uptime(seconds: u64) -> String { if seconds < 60 { format!("{seconds}s") } else if seconds < 3_600 { format!("{}m{:02}s", seconds / 60, seconds % 60) } else if seconds < 86_400 { let hours = seconds / 3_600; let minutes = (seconds % 3_600) / 60; format!("{hours}h{minutes:02}m") } else { let days = seconds / 86_400; let hours = (seconds % 86_400) / 3_600; format!("{days}d{hours:02}h") } } #[cfg(test)] mod tests { use super::*; use crate::protocol::{AttachTarget, FrontendId, InstanceIdentity}; use std::path::PathBuf; fn local_identity() -> InstanceIdentity { InstanceIdentity { pmacs_version: "0.1.0".into(), build_hash: Some("abc123".into()), instance_name: None, uptime_secs: 47, working_directory: "/tmp/proj".into(), } } fn named_identity() -> InstanceIdentity { InstanceIdentity { pmacs_version: "0.1.0".into(), build_hash: None, instance_name: Some("work".into()), uptime_secs: 4_500, working_directory: "/srv/www".into(), } } fn sample_attachment() -> AttachmentHandle { AttachmentHandle::new( FrontendId(2), named_identity(), AttachTarget::LocalSocket(PathBuf::from("/run/pmacs/work.sock")), ) } // ---- format_echo_line --------------------------------------------------- #[test] fn echo_unattached_shows_local_name_when_no_instance_name() { let line = format_echo_line(&local_identity(), None); assert!(line.starts_with("pmacs 0.1.0 [local]"), "{line}"); assert!(line.contains("/tmp/proj")); assert!(line.contains("uptime 47s")); } #[test] fn echo_unattached_uses_instance_name_when_present() { let line = format_echo_line(&named_identity(), None); assert!(line.starts_with("pmacs 0.1.0 [work]"), "{line}"); assert!(line.contains("/srv/www")); // 4500 seconds → 1h15m. assert!(line.contains("uptime 1h15m"), "{line}"); } #[test] fn echo_attached_shows_remote_name_and_target() { let h = sample_attachment(); let line = format_echo_line(&local_identity(), Some(&h)); assert!(line.contains("attached to work"), "{line}"); assert!(line.contains("local:/run/pmacs/work.sock"), "{line}"); assert!(line.contains("from /tmp/proj")); } #[test] fn echo_attached_falls_back_to_anonymous_when_remote_name_missing() { let mut id = named_identity(); id.instance_name = None; let h = AttachmentHandle::new( FrontendId(2), id, AttachTarget::LocalSocket(PathBuf::from("/x")), ); let line = format_echo_line(&local_identity(), Some(&h)); assert!(line.contains("attached to anonymous"), "{line}"); } #[test] fn echo_line_is_single_line() { let line = format_echo_line(&local_identity(), None); assert!(!line.contains('\n'), "echo line must not contain newlines"); let h = sample_attachment(); let line = format_echo_line(&local_identity(), Some(&h)); assert!(!line.contains('\n'), "echo line must not contain newlines"); } // ---- format_full_text --------------------------------------------------- #[test] fn full_text_unattached_includes_self_section_and_no_attachment_marker() { let text = format_full_text(&local_identity(), None); assert!(text.contains("This instance")); assert!(text.contains("version: 0.1.0")); assert!(text.contains("build hash: abc123")); assert!(text.contains("instance name: (unnamed)")); assert!(text.contains("working directory: /tmp/proj")); assert!(text.contains("uptime:")); assert!(text.contains("(no outbound attachment")); assert!(text.contains("Notes")); assert!(text.contains("press `q` to kill")); } #[test] fn full_text_unnamed_build_hash_renders_as_unknown() { let text = format_full_text(&named_identity(), None); assert!(text.contains("build hash: (unknown)")); assert!(text.contains("instance name: work")); } #[test] fn full_text_attached_renders_remote_section() { let h = sample_attachment(); let text = format_full_text(&local_identity(), Some(&h)); assert!(text.contains("Attachment")); assert!(text.contains("frontend id: 2")); assert!(text.contains("target: local:/run/pmacs/work.sock")); assert!(text.contains("kind: local")); assert!(text.contains("remote version: 0.1.0")); assert!(text.contains("remote name: work")); assert!(text.contains("remote cwd: /srv/www")); assert!(text.contains("remote uptime: 1h15m (4500 seconds)")); // build_hash on the remote is None, so the line should be omitted. assert!( !text.contains("remote build hash:"), "expected no build-hash line for None, got: {text}" ); } #[test] fn full_text_attached_includes_remote_build_hash_when_present() { let mut id = named_identity(); id.build_hash = Some("def456".into()); let h = AttachmentHandle::new( FrontendId(3), id, AttachTarget::LocalSocket(PathBuf::from("/x")), ); let text = format_full_text(&local_identity(), Some(&h)); assert!(text.contains("remote build hash: def456")); } // ---- format_uptime ------------------------------------------------------ #[test] fn format_uptime_buckets() { assert_eq!(format_uptime(0), "0s"); assert_eq!(format_uptime(47), "47s"); assert_eq!(format_uptime(60), "1m00s"); assert_eq!(format_uptime(125), "2m05s"); assert_eq!(format_uptime(3_600), "1h00m"); assert_eq!(format_uptime(4_500), "1h15m"); assert_eq!(format_uptime(86_400), "1d00h"); assert_eq!(format_uptime(90_000), "1d01h"); } // ---- render ------------------------------------------------------------- #[test] fn render_creates_named_buffer_and_writes_text() { let mut reg = BufferRegistry::new(); let (id, _edits) = render(&mut reg, &local_identity(), None); let buf = reg.get(id).expect("just rendered"); assert_eq!(buf.name(), INSTANCE_BUFFER_NAME); let len = buf.len(); let mut bytes = vec![0u8; usize::try_from(len).unwrap()]; if len > 0 { buf.snapshot_rope().slice(0, len, &mut bytes); } let body = String::from_utf8(bytes).unwrap(); assert!(body.contains("This instance")); assert!(body.contains("version: 0.1.0")); assert!(body.contains("(no outbound attachment")); assert!(!buf.is_modified(), "rendered buffer must be marked clean"); } #[test] fn render_replaces_existing_contents_on_second_call() { let mut reg = BufferRegistry::new(); let (id1, _) = render(&mut reg, &local_identity(), None); let (id2, _) = render(&mut reg, &named_identity(), None); assert_eq!(id1, id2, "render must reuse the named buffer"); let buf = reg.get(id2).expect("rendered"); let len = buf.len(); let mut bytes = vec![0u8; usize::try_from(len).unwrap()]; if len > 0 { buf.snapshot_rope().slice(0, len, &mut bytes); } let body = String::from_utf8(bytes).unwrap(); assert!(body.contains("instance name: work")); assert!( !body.contains("instance name: (unnamed)"), "stale text from first render leaked: {body}" ); } #[test] fn render_same_identity_is_no_op() { let mut reg = BufferRegistry::new(); let (_id, first_edits) = render(&mut reg, &local_identity(), None); assert!( !first_edits.is_empty(), "initial render should create buffer contents" ); let (_id, second_edits) = render(&mut reg, &local_identity(), None); assert!( second_edits.is_empty(), "unchanged instance render must not emit delete/insert edits" ); } #[test] fn render_with_attachment_includes_remote_section() { let mut reg = BufferRegistry::new(); let h = sample_attachment(); let (id, _) = render(&mut reg, &local_identity(), Some(&h)); let buf = reg.get(id).expect("rendered"); let len = buf.len(); let mut bytes = vec![0u8; usize::try_from(len).unwrap()]; if len > 0 { buf.snapshot_rope().slice(0, len, &mut bytes); } let body = String::from_utf8(bytes).unwrap(); assert!(body.contains("frontend id: 2")); assert!(body.contains("target: local:/run/pmacs/work.sock")); } }