This repository has been archived on 2026-10-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
maestro/crates/maestro-tui/src/emulators.rs
T
Miguel PalhasandClaude Opus 5 b36d571b21
ci / web (push) Successful in 32s
ci / flake (push) Successful in 2m19s
ci / rust (push) Successful in 5m26s
perf(maestro): buffer TUI output and skip unchanged frames
The TUI repainted on every 33ms tick whether or not anything had
changed — measured at ~725 bytes/s of escape sequences with nothing
attached and nothing happening. Over a remote terminal that is constant
traffic for no visible change.

Output was also unbuffered: `CrosstermBackend::new(std::io::stdout())`
issues one `write!` per cell run, 3551 of them for a 200x50 repaint,
each taking the global stdout lock. A BufWriter collapses a frame into
one flush (measured: 3551 writes -> 2).

Frames are now keyed on the catalog version, the active pane's screen
revision, an input epoch and the terminal size, and skipped when the key
is unchanged. Session cards carry an advancing duration, so a frame
still never stands for longer than a second.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 08:11:17 +01:00

190 lines
6.4 KiB
Rust

//! `SessionEmulators` — `SessionId → SessionHandle` registry, the single
//! source of truth for "which sessions are currently attached".
//!
//! Plus [`ResizeTracker`] — every pane reports its allocated `(cols, rows)`
//! per render, and the tracker computes the per-dimension minimum across all
//! panes bound to the same session. That min is what the dispatcher sends
//! to the daemon as a `ClientControl::Resize` so the PTY matches what every
//! viewer can actually display.
use std::collections::HashMap;
use uuid::Uuid;
use crate::session_task::SessionHandle;
#[derive(Debug, Default)]
pub struct SessionEmulators {
handles: HashMap<Uuid, SessionHandle>,
resize: ResizeTracker,
}
impl SessionEmulators {
pub fn get(&self, id: Uuid) -> Option<&SessionHandle> {
self.handles.get(&id)
}
pub fn insert(&mut self, id: Uuid, handle: SessionHandle) {
self.handles.insert(id, handle);
}
/// Remove the handle for `id` (typically because the session ended or
/// the user closed the last pane bound to it). Drops the resize entries
/// for the session at the same time so a re-attach starts fresh.
pub fn remove(&mut self, id: Uuid) {
if let Some(h) = self.handles.remove(&id) {
h.end();
}
self.resize.drop_session(id);
}
pub fn ids(&self) -> impl Iterator<Item = Uuid> + '_ {
self.handles.keys().copied()
}
pub fn resize_tracker(&self) -> &ResizeTracker {
&self.resize
}
pub fn resize_tracker_mut(&mut self) -> &mut ResizeTracker {
&mut self.resize
}
}
/// Per-pane size table for a multi-pane view of the same session. The
/// renderer calls [`set_pane_size`](ResizeTracker::set_pane_size) once per
/// frame; the dispatcher calls [`session_size`](ResizeTracker::session_size)
/// to discover the per-dimension minimum to wire to the daemon.
#[derive(Debug, Default)]
pub struct ResizeTracker {
per_pane: HashMap<(Uuid, String), (u16, u16)>,
}
impl ResizeTracker {
/// Record a pane's currently-allocated `(cols, rows)`. `pane_id` is any
/// stable identifier for the pane — caller's choice. v1 has one pane per
/// tab, so a `Tab N` identifier is fine.
pub fn set_pane_size(&mut self, sid: Uuid, pane_id: &str, size: (u16, u16)) {
self.per_pane.insert((sid, pane_id.to_string()), size);
}
/// Forget a pane's recorded size. Use when a pane is destroyed (tab
/// closed) but other panes may still be displaying the same session.
pub fn drop_pane(&mut self, sid: Uuid, pane_id: &str) {
self.per_pane.remove(&(sid, pane_id.to_string()));
}
/// Per-dimension minimum across every pane bound to `sid`. Returns
/// `None` if no pane is currently tracking this session.
pub fn session_size(&self, sid: Uuid) -> Option<(u16, u16)> {
let mut cols: Option<u16> = None;
let mut rows: Option<u16> = None;
for ((s, _), (c, r)) in &self.per_pane {
if *s == sid {
cols = Some(cols.map_or(*c, |x| x.min(*c)));
rows = Some(rows.map_or(*r, |x| x.min(*r)));
}
}
match (cols, rows) {
(Some(c), Some(r)) => Some((c, r)),
_ => None,
}
}
/// Forget every pane for a given session. Called by
/// [`SessionEmulators::remove`] so a future re-attach gets a fresh size.
pub fn drop_session(&mut self, sid: Uuid) {
self.per_pane.retain(|(s, _), _| *s != sid);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn uid(n: u128) -> Uuid {
Uuid::from_u128(n)
}
#[test]
fn single_pane_yields_its_own_size() {
let mut tracker = ResizeTracker::default();
tracker.set_pane_size(uid(1), "pane-a", (120, 40));
assert_eq!(tracker.session_size(uid(1)), Some((120, 40)));
}
#[test]
fn multi_pane_takes_per_dimension_min() {
let mut tracker = ResizeTracker::default();
tracker.set_pane_size(uid(1), "pane-a", (120, 30));
tracker.set_pane_size(uid(1), "pane-b", (80, 50));
// Independent dims: cols come from b, rows from a.
assert_eq!(tracker.session_size(uid(1)), Some((80, 30)));
}
#[test]
fn sessions_dont_cross_contaminate() {
let mut tracker = ResizeTracker::default();
tracker.set_pane_size(uid(1), "pane-a", (100, 30));
tracker.set_pane_size(uid(2), "pane-b", (50, 20));
assert_eq!(tracker.session_size(uid(1)), Some((100, 30)));
assert_eq!(tracker.session_size(uid(2)), Some((50, 20)));
}
#[test]
fn drop_pane_recomputes_min() {
let mut tracker = ResizeTracker::default();
tracker.set_pane_size(uid(1), "a", (120, 30));
tracker.set_pane_size(uid(1), "b", (80, 50));
tracker.drop_pane(uid(1), "b");
assert_eq!(tracker.session_size(uid(1)), Some((120, 30)));
}
#[test]
fn drop_session_clears_all_panes() {
let mut tracker = ResizeTracker::default();
tracker.set_pane_size(uid(1), "a", (120, 30));
tracker.set_pane_size(uid(1), "b", (80, 50));
tracker.drop_session(uid(1));
assert_eq!(tracker.session_size(uid(1)), None);
}
#[test]
fn unset_session_has_no_size() {
let tracker = ResizeTracker::default();
assert_eq!(tracker.session_size(uid(99)), None);
}
#[test]
fn emulators_insert_and_get() {
use crate::session_task::{SessionHandle, SessionState};
use std::sync::atomic::AtomicU8;
use std::sync::{Arc, Mutex};
use tokio::sync::mpsc;
let mut emulators = SessionEmulators::default();
let id = uid(42);
let handle = SessionHandle {
tx: mpsc::channel(1).0,
screen: Arc::new(Mutex::new(vt100::Parser::new(24, 80, 0))),
state: Arc::new(AtomicU8::new(SessionState::Live as u8)),
revision: Arc::new(std::sync::atomic::AtomicU64::new(0)),
};
emulators.insert(id, handle);
assert!(emulators.get(id).is_some());
emulators.remove(id);
assert!(emulators.get(id).is_none());
}
#[test]
fn emulators_remove_also_drops_resize_entries() {
let mut emulators = SessionEmulators::default();
let id = uid(7);
emulators
.resize_tracker_mut()
.set_pane_size(id, "p", (80, 24));
emulators.remove(id);
assert_eq!(emulators.resize_tracker().session_size(id), None);
}
}