1use std::collections::HashMap;
2use std::sync::atomic::{AtomicU32, Ordering};
3use std::sync::{Arc, Weak};
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use anyhow::{bail, Context, Result};
7use tokio::sync::{broadcast, RwLock};
8use aimux_common::config::AimuxConfig;
9use aimux_protocol::{
10 PaneId, PaneInfo, ProcessInfo, SessionId, SessionInfo, WindowId, WindowInfo,
11};
12
13use crate::screen::{Screen, DEFAULT_SCROLLBACK_LIMIT};
14
15const MIN_PANE_COLS: u16 = 2;
16const MIN_PANE_ROWS: u16 = 1;
17const MAX_PANE_COLS: u16 = 1000;
18const MAX_PANE_ROWS: u16 = 1000;
19
20pub trait PtyBackend: Send {
25 fn write(&self, data: &[u8]) -> Result<()>;
26 fn resize(&self, cols: u16, rows: u16) -> Result<()>;
27 fn close(&mut self) -> Result<()>;
28 fn pid(&self) -> u32;
29}
30
31pub type LayoutResult = Result<(Vec<PaneId>, Vec<Box<dyn PtyBackend>>)>;
33
34pub struct PtySpawnResult {
39 pub backend: Box<dyn PtyBackend>,
40 pub pid: u32,
41 pub output_rx: tokio::sync::mpsc::UnboundedReceiver<Vec<u8>>,
42 pub exit_rx: tokio::sync::oneshot::Receiver<i32>,
43}
44
45pub type PtyFactory = Box<dyn Fn(&str, u16, u16) -> Result<PtySpawnResult> + Send + Sync>;
47
48#[derive(Default)]
53pub struct PaneOptions {
54 pub remain_on_exit: bool,
55}
56
57pub struct Pane {
58 pub id: PaneId,
59 pub pty: Box<dyn PtyBackend>,
60 pub screen: Arc<RwLock<Screen>>,
61 pub process: ProcessInfo,
62 pub size: (u16, u16), pub title: String,
64 pub shell_command: String,
65 pub options: PaneOptions,
66 pub update_tx: broadcast::Sender<()>,
68 pub raw_tx: broadcast::Sender<Vec<u8>>,
70 pub exit_tx: broadcast::Sender<i32>,
72}
73
74impl Pane {
75 pub fn to_info(&self) -> PaneInfo {
76 PaneInfo {
77 id: self.id.clone(),
78 pid: self.process.pid,
79 running: self.process.running,
80 exit_code: self.process.exit_code,
81 size: self.size,
82 title: self.title.clone(),
83 marks: Vec::new(),
84 }
85 }
86}
87
88#[derive(Debug, Clone, PartialEq)]
93pub enum Layout {
94 Single,
95 EvenHorizontal,
96 EvenVertical,
97 MainVertical,
98 Tiled,
99 Custom(crate::layout_dsl::LayoutTree),
100}
101
102pub struct Window {
107 pub id: WindowId,
108 pub panes: Vec<Pane>,
109 pub active_pane: usize,
110 pub layout: Layout,
111}
112
113impl Window {
114 pub fn split_pane(&mut self, pane: Pane, horizontal: bool) {
115 self.panes.push(pane);
116 if matches!(self.layout, Layout::Custom(_)) {
118 self.layout = Layout::EvenHorizontal;
119 } else if self.panes.len() == 2 {
120 self.layout = if horizontal {
121 Layout::EvenHorizontal
122 } else {
123 Layout::EvenVertical
124 };
125 }
126 }
127
128 pub fn compute_pane_positions(
129 &self,
130 area_width: u16,
131 area_height: u16,
132 ) -> Vec<(PaneId, crate::layout::PanePosition)> {
133 let positions =
134 crate::layout::compute_layout(&self.layout, self.panes.len(), area_width, area_height);
135 self.panes
136 .iter()
137 .zip(positions)
138 .map(|(pane, pos)| (pane.id.clone(), pos))
139 .collect()
140 }
141
142 pub fn kill_pane(&mut self, pane_id: &str) -> Result<(bool, Box<dyn PtyBackend>)> {
145 let idx = self
146 .panes
147 .iter()
148 .position(|p| p.id == pane_id)
149 .ok_or_else(|| anyhow::anyhow!("pane not found: {}", pane_id))?;
150
151 let pane = self.panes.remove(idx);
152 let backend = pane.pty;
153
154 if self.panes.is_empty() {
156 return Ok((true, backend));
157 }
158 if self.active_pane >= self.panes.len() {
159 self.active_pane = self.panes.len() - 1;
160 }
161 if self.panes.len() == 1 {
162 self.layout = Layout::Single;
163 } else if matches!(self.layout, Layout::Custom(_)) {
164 self.layout = Layout::EvenHorizontal;
166 }
167 Ok((false, backend))
168 }
169
170 pub fn to_info(&self) -> WindowInfo {
171 let active_pane_id = self
172 .panes
173 .get(self.active_pane)
174 .map(|p| p.id.clone())
175 .unwrap_or_default();
176 WindowInfo {
177 id: self.id,
178 panes: self.panes.iter().map(|p| p.to_info()).collect(),
179 active_pane: active_pane_id,
180 }
181 }
182}
183
184pub struct SessionOptions {
189 pub default_shell: String,
190 pub prompt_pattern: String,
191 pub scrollback_limit: usize,
192}
193
194fn default_shell() -> String {
195 #[cfg(windows)]
196 {
197 "powershell.exe".to_string()
198 }
199 #[cfg(unix)]
200 {
201 std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string())
202 }
203}
204
205impl Default for SessionOptions {
206 fn default() -> Self {
207 Self {
208 default_shell: default_shell(),
209 prompt_pattern: ">".to_string(),
210 scrollback_limit: DEFAULT_SCROLLBACK_LIMIT,
211 }
212 }
213}
214
215pub struct Session {
220 pub name: SessionId,
221 pub windows: Vec<Window>,
222 pub active_window: usize,
223 pub options: SessionOptions,
224 pub created_at: SystemTime,
225 next_window_id: u32,
226}
227
228impl Session {
229 fn new(name: &str) -> Self {
230 Self {
231 name: name.to_string(),
232 windows: Vec::new(),
233 active_window: 0,
234 options: SessionOptions::default(),
235 created_at: SystemTime::now(),
236 next_window_id: 0,
237 }
238 }
239
240 pub fn new_window(&mut self, pane: Pane) -> WindowId {
241 let window_id = self.next_window_id;
242 self.next_window_id += 1;
243 self.windows.push(Window {
244 id: window_id,
245 panes: vec![pane],
246 active_pane: 0,
247 layout: Layout::Single,
248 });
249 window_id
250 }
251
252 pub fn kill_window(&mut self, window_id: WindowId) -> Result<(bool, Vec<Box<dyn PtyBackend>>)> {
255 let idx = self
256 .windows
257 .iter()
258 .position(|w| w.id == window_id)
259 .ok_or_else(|| anyhow::anyhow!("window not found: {}", window_id))?;
260
261 let window = self.windows.remove(idx);
262 let backends: Vec<Box<dyn PtyBackend>> =
263 window.panes.into_iter().map(|p| p.pty).collect();
264
265 if self.windows.is_empty() {
266 return Ok((true, backends));
267 }
268 if self.active_window >= self.windows.len() {
269 self.active_window = self.windows.len() - 1;
270 }
271 Ok((false, backends))
272 }
273
274 pub fn to_info(&self) -> SessionInfo {
275 let created_at = self
276 .created_at
277 .duration_since(UNIX_EPOCH)
278 .unwrap_or_default()
279 .as_secs();
280 SessionInfo {
281 name: self.name.clone(),
282 windows: self.windows.iter().map(|w| w.to_info()).collect(),
283 created_at,
284 }
285 }
286
287 pub fn find_window(&self, window_id: WindowId) -> Option<&Window> {
289 self.windows.iter().find(|w| w.id == window_id)
290 }
291
292 pub fn find_window_mut(&mut self, window_id: WindowId) -> Option<&mut Window> {
293 self.windows.iter_mut().find(|w| w.id == window_id)
294 }
295}
296
297async fn run_screen_feed(
303 mut output_rx: tokio::sync::mpsc::UnboundedReceiver<Vec<u8>>,
304 screen: Arc<RwLock<Screen>>,
305 update_tx: broadcast::Sender<()>,
306 raw_tx: broadcast::Sender<Vec<u8>>,
307) {
308 tracing::debug!("screen_feed: task started, waiting for PTY output");
309 while let Some(data) = output_rx.recv().await {
310 tracing::trace!("screen_feed: received {} bytes", data.len());
311 {
312 let mut s = screen.write().await;
313 s.feed(&data);
314 }
315 let _ = update_tx.send(());
316 let _ = raw_tx.send(data);
317 }
318 tracing::debug!("screen_feed: channel closed, task exiting");
319}
320
321#[allow(clippy::too_many_arguments)]
323fn spawn_pane_tasks(
324 output_rx: tokio::sync::mpsc::UnboundedReceiver<Vec<u8>>,
325 screen: Arc<RwLock<Screen>>,
326 exit_rx: tokio::sync::oneshot::Receiver<i32>,
327 update_tx: broadcast::Sender<()>,
328 raw_tx: broadcast::Sender<Vec<u8>>,
329 exit_tx: broadcast::Sender<i32>,
330 pane_id: PaneId,
331 manager: Option<Arc<tokio::sync::Mutex<SessionManager>>>,
332) {
333 tracing::debug!("spawn_pane_tasks: spawning screen feed for pane {}", pane_id);
335 tokio::spawn(run_screen_feed(output_rx, screen, update_tx, raw_tx));
336
337 tokio::spawn(async move {
339 if let Ok(code) = exit_rx.await {
340 let _ = exit_tx.send(code);
342
343 if let Some(mgr_arc) = manager {
344 let mut mgr = mgr_arc.lock().await;
345 if let Some(pane) = mgr.find_pane_mut(&pane_id) {
347 pane.process.running = false;
348 pane.process.exit_code = Some(code);
349 tracing::info!("pane {} exited with code {}", pane_id, code);
350
351 if !pane.options.remain_on_exit {
352 drop(mgr);
353 let backends = {
355 let mut mgr = mgr_arc.lock().await;
356 mgr.kill_pane(&pane_id).unwrap_or_default()
357 };
358 for mut b in backends {
359 let _ = b.close();
360 }
361 }
362 }
363 }
364 }
365 });
366}
367
368fn validate_mark_name(name: &str) -> Result<()> {
373 if name.is_empty() || name.len() > 32 {
374 bail!("mark name must be 1-32 characters, got {}", name.len());
375 }
376 if !name
377 .chars()
378 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
379 {
380 bail!(
381 "mark name must be alphanumeric, hyphens, or underscores: {}",
382 name
383 );
384 }
385 Ok(())
386}
387
388fn parse_layout_name(name: &str) -> Result<Layout> {
389 match name {
390 "single" => Ok(Layout::Single),
391 "even-horizontal" => Ok(Layout::EvenHorizontal),
392 "even-vertical" => Ok(Layout::EvenVertical),
393 "main-vertical" => Ok(Layout::MainVertical),
394 "tiled" => Ok(Layout::Tiled),
395 other => bail!(
396 "unknown layout: {} (valid: single, even-horizontal, even-vertical, main-vertical, tiled)",
397 other
398 ),
399 }
400}
401
402fn collect_leaf_info(
404 node: &aimux_common::config::LayoutNode,
405 out: &mut Vec<(Option<String>, Option<String>)>,
406) {
407 if node.direction.is_none() {
408 out.push((node.mark.clone(), node.command.clone()));
409 } else if let Some(ref children) = node.children {
410 for child in children {
411 collect_leaf_info(child, out);
412 }
413 }
414}
415
416pub struct SessionManager {
417 pub(crate) sessions: HashMap<SessionId, Session>,
418 next_pane_id: AtomicU32,
419 pty_factory: PtyFactory,
420 self_arc: Option<Weak<tokio::sync::Mutex<SessionManager>>>,
421 pub(crate) pane_index: HashMap<PaneId, (SessionId, WindowId)>,
422 config: AimuxConfig,
423 pub(crate) marks: HashMap<String, PaneId>,
424 start_time: std::time::Instant,
425}
426
427impl SessionManager {
428 pub fn new(pty_factory: PtyFactory, config: AimuxConfig) -> Self {
429 Self {
430 sessions: HashMap::new(),
431 next_pane_id: AtomicU32::new(0),
432 pty_factory,
433 self_arc: None,
434 pane_index: HashMap::new(),
435 config,
436 marks: HashMap::new(),
437 start_time: std::time::Instant::now(),
438 }
439 }
440
441 pub fn set_self_arc(&mut self, arc: &Arc<tokio::sync::Mutex<SessionManager>>) {
442 self.self_arc = Some(Arc::downgrade(arc));
443 }
444
445 pub fn allocate_pane_id(&self) -> PaneId {
446 let id = self.next_pane_id.fetch_add(1, Ordering::Relaxed);
447 format!("%{}", id)
448 }
449
450 pub fn default_session_options(&self) -> SessionOptions {
451 let mut opts = SessionOptions::default();
452 if let Some(ref shell) = self.config.default_shell {
453 opts.default_shell = shell.clone();
454 }
455 if let Some(ref pattern) = self.config.prompt_pattern {
456 opts.prompt_pattern = pattern.clone();
457 }
458 if let Some(limit) = self.config.scrollback_limit {
459 opts.scrollback_limit = limit;
460 }
461 opts
462 }
463
464 pub fn reload_config(&mut self, config: AimuxConfig) {
465 self.config = config;
466 }
467
468 pub fn server_status(&self) -> aimux_protocol::ServerStatusInfo {
469 let uptime_secs = self.start_time.elapsed().as_secs();
470 let mut windows: u32 = 0;
471 let mut panes: u32 = 0;
472 for session in self.sessions.values() {
473 windows += session.windows.len() as u32;
474 for window in &session.windows {
475 panes += window.panes.len() as u32;
476 }
477 }
478 aimux_protocol::ServerStatusInfo {
479 version: String::new(), uptime_secs,
481 sessions: self.sessions.len() as u32,
482 windows,
483 panes,
484 pid: 0, }
486 }
487
488 pub fn config(&self) -> &AimuxConfig {
489 &self.config
490 }
491
492 pub(crate) fn config_mut(&mut self) -> &mut AimuxConfig {
493 &mut self.config
494 }
495
496 pub fn resolve_pane_target(&self, target: &str) -> Result<String> {
499 if let Some(mark_name) = target.strip_prefix('@') {
500 self.marks
501 .get(mark_name)
502 .cloned()
503 .ok_or_else(|| anyhow::anyhow!("mark not found: @{}", mark_name))
504 } else {
505 Ok(target.to_string())
506 }
507 }
508
509 pub fn set_mark(&mut self, mark: &str, pane_id: &str) -> Result<()> {
510 validate_mark_name(mark)?;
511 if !self.pane_index.contains_key(pane_id) {
512 bail!("pane not found: {}", pane_id);
513 }
514 self.marks.insert(mark.to_string(), pane_id.to_string());
515 Ok(())
516 }
517
518 pub fn remove_mark(&mut self, mark: &str) -> Result<()> {
519 self.marks
520 .remove(mark)
521 .ok_or_else(|| anyhow::anyhow!("mark not found: {}", mark))?;
522 Ok(())
523 }
524
525 pub fn list_marks(&self) -> Vec<(String, String)> {
526 self.marks
527 .iter()
528 .map(|(k, v)| (k.clone(), v.clone()))
529 .collect()
530 }
531
532 pub fn marks_for_pane(&self, pane_id: &str) -> Vec<String> {
533 self.marks
534 .iter()
535 .filter(|(_, v)| v.as_str() == pane_id)
536 .map(|(k, _)| k.clone())
537 .collect()
538 }
539
540 fn inject_marks(&self, pane_infos: &mut [PaneInfo]) {
541 for info in pane_infos.iter_mut() {
542 info.marks = self.marks_for_pane(&info.id);
543 }
544 }
545
546 pub fn apply_layout(
550 &mut self,
551 session_name: &str,
552 window_id: WindowId,
553 layout_name: &str,
554 ) -> LayoutResult {
555 let layout_node = self
557 .config
558 .layouts
559 .as_ref()
560 .and_then(|layouts| layouts.get(layout_name))
561 .cloned();
562
563 let layout_node = match layout_node {
564 Some(node) => node,
565 None => {
566 return self.load_layout(session_name, window_id, layout_name);
568 }
569 };
570
571 let tree = crate::layout_dsl::config_to_tree(&layout_node)
572 .context("invalid layout definition")?;
573 let leaf_count = tree.leaf_count();
574 if leaf_count == 0 {
575 bail!("layout must have at least one leaf pane");
576 }
577
578 let mut leaf_info: Vec<(Option<String>, Option<String>)> = Vec::new();
580 collect_leaf_info(&layout_node, &mut leaf_info);
581
582 let session = self
583 .sessions
584 .get(session_name)
585 .ok_or_else(|| anyhow::anyhow!("session not found: {}", session_name))?;
586 let default_shell = session.options.default_shell.clone();
587 let scrollback_limit = session.options.scrollback_limit;
588 let window = session
589 .find_window(window_id)
590 .ok_or_else(|| anyhow::anyhow!("window not found: {}", window_id))?;
591 let current_count = window.panes.len();
592
593 let mut killed_backends = Vec::new();
595 if current_count > leaf_count {
596 let to_kill: Vec<PaneId> = {
597 let session = self.sessions.get(session_name).unwrap();
598 let window = session.find_window(window_id).unwrap();
599 window.panes[leaf_count..]
600 .iter()
601 .map(|p| p.id.clone())
602 .collect()
603 };
604 for pane_id in &to_kill {
605 self.pane_index.remove(pane_id);
606 self.marks.retain(|_, v| v.as_str() != pane_id.as_str());
607 }
608 let session = self.sessions.get_mut(session_name).unwrap();
609 let window = session.find_window_mut(window_id).unwrap();
610 for pane_id in &to_kill {
611 if let Some(idx) = window.panes.iter().position(|p| p.id == *pane_id) {
612 let pane = window.panes.remove(idx);
613 killed_backends.push(pane.pty);
614 }
615 }
616 }
617
618 if current_count < leaf_count {
620 let needed = leaf_count - current_count;
621 let mut new_panes = Vec::with_capacity(needed);
622 for _ in 0..needed {
623 let pane = self.create_pane(&default_shell, 80, 24, scrollback_limit)?;
624 new_panes.push(pane);
625 }
626 for pane in new_panes {
627 let pane_id = pane.id.clone();
628 self.pane_index
629 .insert(pane_id, (session_name.to_string(), window_id));
630 let session = self.sessions.get_mut(session_name).unwrap();
631 let window = session.find_window_mut(window_id).unwrap();
632 window.panes.push(pane);
633 }
634 }
635
636 let session = self.sessions.get_mut(session_name).unwrap();
638 let window = session.find_window_mut(window_id).unwrap();
639 window.layout = Layout::Custom(tree);
640 if window.active_pane >= window.panes.len() {
641 window.active_pane = window.panes.len().saturating_sub(1);
642 }
643
644 let pane_ids: Vec<PaneId> = window.panes.iter().map(|p| p.id.clone()).collect();
646 for (i, (mark, _cmd)) in leaf_info.iter().enumerate() {
647 if let Some(mark_name) = mark {
648 if i < pane_ids.len() {
649 let _ = self.set_mark(mark_name, &pane_ids[i]);
650 }
651 }
652 }
653
654 Ok((pane_ids, killed_backends))
655 }
656
657 pub fn select_layout(
659 &mut self,
660 session_name: &str,
661 window_id: WindowId,
662 layout_name: &str,
663 ) -> Result<()> {
664 let layout = parse_layout_name(layout_name)?;
665 let session = self
666 .sessions
667 .get_mut(session_name)
668 .ok_or_else(|| anyhow::anyhow!("session not found: {}", session_name))?;
669 let window = session
670 .find_window_mut(window_id)
671 .ok_or_else(|| anyhow::anyhow!("window not found: {}", window_id))?;
672 window.layout = layout;
673 Ok(())
674 }
675
676 pub(crate) fn create_pane(
677 &self,
678 shell: &str,
679 cols: u16,
680 rows: u16,
681 scrollback_limit: usize,
682 ) -> Result<Pane> {
683 let pane_id = self.allocate_pane_id();
684 let spawn_result = (self.pty_factory)(shell, cols, rows)?;
685
686 let screen = Arc::new(RwLock::new(Screen::new(cols, rows, scrollback_limit)));
687
688 let (update_tx, _) = broadcast::channel(64);
690 let (raw_tx, _) = broadcast::channel(64);
691 let (exit_tx, _) = broadcast::channel(4);
692
693 let manager_arc = self.self_arc.as_ref().and_then(|w| w.upgrade());
694
695 spawn_pane_tasks(
697 spawn_result.output_rx,
698 screen.clone(),
699 spawn_result.exit_rx,
700 update_tx.clone(),
701 raw_tx.clone(),
702 exit_tx.clone(),
703 pane_id.clone(),
704 manager_arc,
705 );
706
707 Ok(Pane {
708 id: pane_id,
709 pty: spawn_result.backend,
710 screen,
711 process: ProcessInfo {
712 pid: spawn_result.pid,
713 running: true,
714 exit_code: None,
715 },
716 size: (cols, rows),
717 title: shell.to_string(),
718 shell_command: shell.to_string(),
719 options: PaneOptions::default(),
720 update_tx,
721 raw_tx,
722 exit_tx,
723 })
724 }
725
726 pub fn new_session(
727 &mut self,
728 name: &str,
729 shell: Option<&str>,
730 ) -> Result<(SessionId, PaneId)> {
731 if self.sessions.contains_key(name) {
732 bail!("session already exists: {}", name);
733 }
734
735 let mut session = Session::new(name);
736 session.options = self.default_session_options();
737 let shell = shell.unwrap_or(&session.options.default_shell).to_string();
738 let scrollback_limit = session.options.scrollback_limit;
739 let pane = self.create_pane(&shell, 80, 24, scrollback_limit)?;
740 let pane_id = pane.id.clone();
741
742 let window_id = session.new_window(pane);
743 let session_id = session.name.clone();
744 self.pane_index.insert(pane_id.clone(), (session_id.clone(), window_id));
745 self.sessions.insert(session_id.clone(), session);
746
747 Ok((session_id, pane_id))
748 }
749
750 pub fn kill_session(&mut self, name: &str) -> Result<Vec<Box<dyn PtyBackend>>> {
751 let session = self
752 .sessions
753 .remove(name)
754 .ok_or_else(|| anyhow::anyhow!("session not found: {}", name))?;
755
756 let pane_ids: Vec<String> = session
757 .windows
758 .iter()
759 .flat_map(|w| w.panes.iter().map(|p| p.id.clone()))
760 .collect();
761
762 let mut backends = Vec::new();
763 for window in session.windows {
764 for pane in window.panes {
765 self.pane_index.remove(&pane.id);
766 backends.push(pane.pty);
767 }
768 }
769
770 self.marks.retain(|_, v| !pane_ids.contains(v));
771 Ok(backends)
772 }
773
774 pub fn list_sessions(&self) -> Vec<SessionInfo> {
775 let mut sessions: Vec<SessionInfo> = self.sessions.values().map(|s| s.to_info()).collect();
776 for session in &mut sessions {
777 for window in &mut session.windows {
778 self.inject_marks(&mut window.panes);
779 }
780 }
781 sessions
782 }
783
784 pub fn has_session(&self, name: &str) -> bool {
785 self.sessions.contains_key(name)
786 }
787
788 #[allow(dead_code)]
789 pub fn get_session(&self, name: &str) -> Option<&Session> {
790 self.sessions.get(name)
791 }
792
793 pub fn get_session_mut(&mut self, name: &str) -> Option<&mut Session> {
794 self.sessions.get_mut(name)
795 }
796
797 pub fn find_pane(&self, pane_id: &str) -> Option<(&Session, &Window, &Pane)> {
799 let (session_id, window_id) = self.pane_index.get(pane_id)?;
800 let session = self.sessions.get(session_id)?;
801 let window = session.find_window(*window_id)?;
802 let pane = window.panes.iter().find(|p| p.id == pane_id)?;
803 Some((session, window, pane))
804 }
805
806 pub fn find_pane_mut(&mut self, pane_id: &str) -> Option<&mut Pane> {
808 let (session_id, window_id) = self.pane_index.get(pane_id)?;
809 let session_id = session_id.clone();
810 let window_id = *window_id;
811 let session = self.sessions.get_mut(&session_id)?;
812 let window = session.find_window_mut(window_id)?;
813 window.panes.iter_mut().find(|p| p.id == pane_id)
814 }
815
816 pub fn get_prompt_pattern(&self, pane_id: &str) -> Option<String> {
818 self.find_pane(pane_id)
819 .map(|(session, _, _)| session.options.prompt_pattern.clone())
820 }
821
822 pub fn send_keys(&self, pane_id: &str, keys: &str) -> Result<()> {
824 let (_, _, pane) = self
825 .find_pane(pane_id)
826 .ok_or_else(|| anyhow::anyhow!("pane not found: {}", pane_id))?;
827 if !pane.process.running {
828 bail!("pane {} has exited", pane_id);
829 }
830 pane.pty.write(keys.as_bytes())
831 }
832
833 pub fn get_capture_info(
837 &self,
838 pane_id: &str,
839 ) -> Result<(Arc<RwLock<Screen>>, ProcessInfo)> {
840 let (_, _, pane) = self
841 .find_pane(pane_id)
842 .ok_or_else(|| anyhow::anyhow!("pane not found: {}", pane_id))?;
843 Ok((pane.screen.clone(), pane.process.clone()))
844 }
845
846 pub fn new_window(
848 &mut self,
849 session_name: &str,
850 shell: Option<&str>,
851 ) -> Result<(WindowId, PaneId)> {
852 let (default_shell, scrollback_limit) = self
853 .sessions
854 .get(session_name)
855 .map(|s| (s.options.default_shell.clone(), s.options.scrollback_limit))
856 .ok_or_else(|| anyhow::anyhow!("session not found: {}", session_name))?;
857
858 let shell_cmd = shell.unwrap_or(&default_shell).to_string();
859 let pane = self.create_pane(&shell_cmd, 80, 24, scrollback_limit)?;
860 let pane_id = pane.id.clone();
861
862 let session = self
863 .sessions
864 .get_mut(session_name)
865 .ok_or_else(|| anyhow::anyhow!("session not found: {}", session_name))?;
866 let window_id = session.new_window(pane);
867 self.pane_index.insert(pane_id.clone(), (session_name.to_string(), window_id));
868 Ok((window_id, pane_id))
869 }
870
871 pub fn kill_window(&mut self, session_name: &str, window_id: WindowId) -> Result<Vec<Box<dyn PtyBackend>>> {
874 let session = self
875 .sessions
876 .get_mut(session_name)
877 .ok_or_else(|| anyhow::anyhow!("session not found: {}", session_name))?;
878
879 let mut pane_ids = Vec::new();
881 if let Some(window) = session.find_window(window_id) {
882 for pane in &window.panes {
883 pane_ids.push(pane.id.clone());
884 self.pane_index.remove(&pane.id);
885 }
886 }
887
888 let (session_empty, backends) = session.kill_window(window_id)?;
889 if session_empty {
890 self.sessions.remove(session_name);
891 }
892
893 self.marks.retain(|_, v| !pane_ids.contains(v));
894 Ok(backends)
895 }
896
897 pub fn split_pane(
899 &mut self,
900 pane_id: &str,
901 horizontal: bool,
902 shell: Option<&str>,
903 ) -> Result<PaneId> {
904 let (session_name, window_id) = self.pane_index
905 .get(pane_id)
906 .ok_or_else(|| anyhow::anyhow!("pane not found: {}", pane_id))?;
907 let session_name = session_name.clone();
908 let window_id = *window_id;
909
910 let session = self.sessions.get(&session_name)
911 .ok_or_else(|| anyhow::anyhow!("session not found: {}", session_name))?;
912 let default_shell = session.options.default_shell.clone();
913 let scrollback_limit = session.options.scrollback_limit;
914
915 let shell_cmd = shell.unwrap_or(&default_shell).to_string();
916 let new_pane = self.create_pane(&shell_cmd, 80, 24, scrollback_limit)?;
917 let new_pane_id = new_pane.id.clone();
918
919 let session = self
920 .sessions
921 .get_mut(&session_name)
922 .ok_or_else(|| anyhow::anyhow!("session not found: {}", session_name))?;
923 let window = session
924 .find_window_mut(window_id)
925 .ok_or_else(|| anyhow::anyhow!("window not found: {}", window_id))?;
926 window.split_pane(new_pane, horizontal);
927 self.pane_index.insert(new_pane_id.clone(), (session_name, window_id));
928
929 Ok(new_pane_id)
930 }
931
932 pub fn kill_pane(&mut self, pane_id: &str) -> Result<Vec<Box<dyn PtyBackend>>> {
935 let (session_name, window_id) = self.pane_index
936 .remove(pane_id)
937 .ok_or_else(|| anyhow::anyhow!("pane not found: {}", pane_id))?;
938
939 let session = self
940 .sessions
941 .get_mut(&session_name)
942 .ok_or_else(|| anyhow::anyhow!("session not found: {}", session_name))?;
943 let window = session
944 .find_window_mut(window_id)
945 .ok_or_else(|| anyhow::anyhow!("window not found: {}", window_id))?;
946
947 let (window_empty, backend) = window.kill_pane(pane_id)?;
948 let mut backends = vec![backend];
949 if window_empty {
950 let (session_empty, mut window_backends) = session.kill_window(window_id)?;
951 backends.append(&mut window_backends);
952 if session_empty {
953 self.sessions.remove(&session_name);
954 }
955 }
956
957 self.marks.retain(|_, v| v.as_str() != pane_id);
958 Ok(backends)
959 }
960
961 pub fn list_panes(&self, session_name: &str) -> Result<Vec<PaneInfo>> {
963 let session = self
964 .sessions
965 .get(session_name)
966 .ok_or_else(|| anyhow::anyhow!("session not found: {}", session_name))?;
967 let mut panes = Vec::new();
968 for window in &session.windows {
969 for pane in &window.panes {
970 panes.push(pane.to_info());
971 }
972 }
973 self.inject_marks(&mut panes);
974 Ok(panes)
975 }
976
977 pub fn select_window(&mut self, session_name: &str, window_id: WindowId) -> Result<()> {
979 let session = self
980 .sessions
981 .get_mut(session_name)
982 .ok_or_else(|| anyhow::anyhow!("session \"{}\" not found", session_name))?;
983 let idx = session
984 .windows
985 .iter()
986 .position(|w| w.id == window_id)
987 .ok_or_else(|| {
988 anyhow::anyhow!(
989 "window {} not found in session \"{}\"",
990 window_id,
991 session_name
992 )
993 })?;
994 session.active_window = idx;
995 Ok(())
996 }
997
998 pub fn select_pane(&mut self, pane_id: &str) -> Result<()> {
1000 let (session_id, window_id) = self.pane_index
1001 .get(pane_id)
1002 .ok_or_else(|| anyhow::anyhow!("pane not found: {}", pane_id))?;
1003 let session_id = session_id.clone();
1004 let window_id = *window_id;
1005 let session = self.sessions.get_mut(&session_id)
1006 .ok_or_else(|| anyhow::anyhow!("session not found: {}", session_id))?;
1007 let window = session.find_window_mut(window_id)
1008 .ok_or_else(|| anyhow::anyhow!("window not found: {}", window_id))?;
1009 let idx = window.panes.iter().position(|p| p.id == pane_id)
1010 .ok_or_else(|| anyhow::anyhow!("pane not found: {}", pane_id))?;
1011 window.active_pane = idx;
1012 Ok(())
1013 }
1014
1015 pub fn swap_panes(&mut self, pane_a: &str, pane_b: &str) -> Result<()> {
1017 let (session_a, window_a) = self
1018 .pane_index
1019 .get(pane_a)
1020 .ok_or_else(|| anyhow::anyhow!("pane not found: {}", pane_a))?;
1021 let (session_b, window_b) = self
1022 .pane_index
1023 .get(pane_b)
1024 .ok_or_else(|| anyhow::anyhow!("pane not found: {}", pane_b))?;
1025
1026 if session_a != session_b || window_a != window_b {
1027 bail!("panes must be in the same window to swap");
1028 }
1029
1030 let session_name = session_a.clone();
1031 let window_id = *window_a;
1032
1033 if pane_a == pane_b {
1034 return Ok(());
1035 }
1036
1037 let session = self.sessions.get_mut(&session_name).unwrap();
1038 let window = session.find_window_mut(window_id).unwrap();
1039
1040 let idx_a = window.panes.iter().position(|p| p.id == pane_a).unwrap();
1041 let idx_b = window.panes.iter().position(|p| p.id == pane_b).unwrap();
1042
1043 window.panes.swap(idx_a, idx_b);
1044
1045 if window.active_pane == idx_a {
1046 window.active_pane = idx_b;
1047 } else if window.active_pane == idx_b {
1048 window.active_pane = idx_a;
1049 }
1050
1051 Ok(())
1052 }
1053
1054 pub fn move_pane(
1056 &mut self,
1057 pane_id: &str,
1058 target_session: &str,
1059 target_window_id: WindowId,
1060 ) -> Result<()> {
1061 let (src_session, src_window) = self
1063 .pane_index
1064 .get(pane_id)
1065 .ok_or_else(|| anyhow::anyhow!("pane not found: {}", pane_id))?;
1066 let src_session = src_session.clone();
1067 let src_window = *src_window;
1068
1069 let target_session_name = target_session.to_string();
1071 if !self.sessions.contains_key(&target_session_name) {
1072 bail!("session not found: {}", target_session);
1073 }
1074 {
1075 let session = self.sessions.get(&target_session_name).unwrap();
1076 if session.find_window(target_window_id).is_none() {
1077 bail!(
1078 "window not found: {}:{}",
1079 target_session,
1080 target_window_id
1081 );
1082 }
1083 }
1084
1085 if src_session == target_session_name && src_window == target_window_id {
1087 return Ok(());
1088 }
1089
1090 let pane = {
1092 let session = self.sessions.get_mut(&src_session).unwrap();
1093 let window = session.find_window_mut(src_window).unwrap();
1094 let idx = window.panes.iter().position(|p| p.id == pane_id).unwrap();
1095 let pane = window.panes.remove(idx);
1096 if window.panes.is_empty() {
1097 window.active_pane = 0;
1098 } else if window.active_pane >= window.panes.len() {
1099 window.active_pane = window.panes.len() - 1;
1100 }
1101 if window.panes.len() == 1 {
1102 window.layout = Layout::Single;
1103 } else if matches!(window.layout, Layout::Custom(_)) {
1104 window.layout = Layout::EvenHorizontal;
1105 }
1106 pane
1107 };
1108
1109 {
1111 let session = self.sessions.get_mut(&target_session_name).unwrap();
1112 let window = session.find_window_mut(target_window_id).unwrap();
1113 window.panes.push(pane);
1114 window.active_pane = window.panes.len() - 1;
1115 if matches!(window.layout, Layout::Custom(_))
1116 || (window.panes.len() == 2 && window.layout == Layout::Single)
1117 {
1118 window.layout = Layout::EvenHorizontal;
1119 }
1120 }
1121
1122 self.pane_index.insert(
1124 pane_id.to_string(),
1125 (target_session_name, target_window_id),
1126 );
1127
1128 {
1131 let session = self.sessions.get_mut(&src_session).unwrap();
1132 let window = session.find_window(src_window);
1133 if window.is_some_and(|w| w.panes.is_empty()) {
1134 let (session_empty, _) = session.kill_window(src_window)?;
1135 if session_empty {
1136 self.sessions.remove(&src_session);
1137 }
1138 }
1139 }
1140
1141 Ok(())
1142 }
1143
1144 pub fn list_windows(&self, session_name: &str) -> Result<Vec<WindowInfo>> {
1146 let session = self
1147 .sessions
1148 .get(session_name)
1149 .ok_or_else(|| anyhow::anyhow!("session not found: {}", session_name))?;
1150 Ok(session.windows.iter().map(|w| w.to_info()).collect())
1151 }
1152
1153 pub fn resize_pane_pty(
1156 &mut self,
1157 pane_id: &str,
1158 cols: u16,
1159 rows: u16,
1160 ) -> Result<Arc<RwLock<Screen>>> {
1161 if cols < MIN_PANE_COLS || rows < MIN_PANE_ROWS {
1162 bail!(
1163 "dimensions too small: {}x{} (minimum {}x{})",
1164 cols, rows, MIN_PANE_COLS, MIN_PANE_ROWS
1165 );
1166 }
1167 if cols > MAX_PANE_COLS || rows > MAX_PANE_ROWS {
1168 bail!(
1169 "dimensions too large: {}x{} (maximum {}x{})",
1170 cols, rows, MAX_PANE_COLS, MAX_PANE_ROWS
1171 );
1172 }
1173
1174 let pane = self
1175 .find_pane_mut(pane_id)
1176 .ok_or_else(|| anyhow::anyhow!("pane not found: {}", pane_id))?;
1177
1178 if !pane.process.running {
1179 bail!("pane {} has exited", pane_id);
1180 }
1181
1182 pane.pty.resize(cols, rows)?;
1183 pane.size = (cols, rows);
1184 Ok(pane.screen.clone())
1185 }
1186
1187 pub fn shutdown_all(&mut self) -> Vec<Box<dyn PtyBackend>> {
1189 self.pane_index.clear();
1190 self.marks.clear();
1191 let sessions = std::mem::take(&mut self.sessions);
1192 let mut backends = Vec::new();
1193 for (_, session) in sessions {
1194 for window in session.windows {
1195 for pane in window.panes {
1196 backends.push(pane.pty);
1197 }
1198 }
1199 }
1200 backends
1201 }
1202}
1203
1204#[cfg(test)]
1209pub mod testing {
1210 use super::*;
1211 use std::sync::atomic::{AtomicU32, Ordering};
1212
1213 pub struct MockPty {
1214 pid: u32,
1215 }
1216
1217 impl PtyBackend for MockPty {
1218 fn write(&self, _data: &[u8]) -> Result<()> {
1219 Ok(())
1220 }
1221 fn resize(&self, _cols: u16, _rows: u16) -> Result<()> {
1222 Ok(())
1223 }
1224 fn close(&mut self) -> Result<()> {
1225 Ok(())
1226 }
1227 fn pid(&self) -> u32 {
1228 self.pid
1229 }
1230 }
1231
1232 pub fn mock_factory() -> PtyFactory {
1233 let pid = AtomicU32::new(1000);
1234 Box::new(move |_cmd: &str, _cols: u16, _rows: u16| {
1235 let p = pid.fetch_add(1, Ordering::Relaxed);
1236 let (_, output_rx) = tokio::sync::mpsc::unbounded_channel();
1237 let (_, exit_rx) = tokio::sync::oneshot::channel();
1238 Ok(PtySpawnResult {
1239 backend: Box::new(MockPty { pid: p }),
1240 pid: p,
1241 output_rx,
1242 exit_rx,
1243 })
1244 })
1245 }
1246
1247 pub fn new_manager() -> SessionManager {
1248 SessionManager::new(mock_factory(), AimuxConfig::default())
1249 }
1250
1251 pub fn new_manager_arc() -> std::sync::Arc<tokio::sync::Mutex<SessionManager>> {
1252 std::sync::Arc::new(tokio::sync::Mutex::new(new_manager()))
1253 }
1254}
1255
1256#[cfg(test)]
1261mod tests {
1262 use super::testing::new_manager;
1263 use super::Layout;
1264
1265 #[tokio::test]
1266 async fn create_and_list_session() {
1267 let mut mgr = new_manager();
1268 let (name, pane_id) = mgr.new_session("work", None).unwrap();
1269 assert_eq!(name, "work");
1270 assert_eq!(pane_id, "%0");
1271
1272 let sessions = mgr.list_sessions();
1273 assert_eq!(sessions.len(), 1);
1274 assert_eq!(sessions[0].name, "work");
1275 assert_eq!(sessions[0].windows.len(), 1);
1276 assert_eq!(sessions[0].windows[0].panes.len(), 1);
1277 }
1278
1279 #[tokio::test]
1280 async fn duplicate_session_name_errors() {
1281 let mut mgr = new_manager();
1282 mgr.new_session("dup", None).unwrap();
1283 let err = mgr.new_session("dup", None).unwrap_err();
1284 assert!(err.to_string().contains("already exists"));
1285 }
1286
1287 #[tokio::test]
1288 async fn kill_session() {
1289 let mut mgr = new_manager();
1290 mgr.new_session("temp", None).unwrap();
1291 assert!(mgr.has_session("temp"));
1292
1293 mgr.kill_session("temp").unwrap();
1294 assert!(!mgr.has_session("temp"));
1295 assert!(mgr.list_sessions().is_empty());
1296 }
1297
1298 #[tokio::test]
1299 async fn has_session() {
1300 let mut mgr = new_manager();
1301 assert!(!mgr.has_session("nope"));
1302 mgr.new_session("yes", None).unwrap();
1303 assert!(mgr.has_session("yes"));
1304 }
1305
1306 #[tokio::test]
1307 async fn new_window_in_session() {
1308 let mut mgr = new_manager();
1309 mgr.new_session("s1", None).unwrap();
1310 let (win_id, pane_id) = mgr.new_window("s1", None).unwrap();
1311 assert_eq!(win_id, 1);
1312 assert_eq!(pane_id, "%1");
1313
1314 let windows = mgr.list_windows("s1").unwrap();
1315 assert_eq!(windows.len(), 2);
1316 }
1317
1318 #[tokio::test]
1319 async fn kill_window() {
1320 let mut mgr = new_manager();
1321 mgr.new_session("s1", None).unwrap();
1322 mgr.new_window("s1", None).unwrap();
1323
1324 mgr.kill_window("s1", 0).unwrap();
1325 let windows = mgr.list_windows("s1").unwrap();
1326 assert_eq!(windows.len(), 1);
1327 assert_eq!(windows[0].id, 1);
1328 }
1329
1330 #[tokio::test]
1331 async fn split_pane() {
1332 let mut mgr = new_manager();
1333 let (_, pane0) = mgr.new_session("s1", None).unwrap();
1334 let pane1 = mgr.split_pane(&pane0, true, None).unwrap();
1335 assert_eq!(pane1, "%1");
1336
1337 let panes = mgr.list_panes("s1").unwrap();
1338 assert_eq!(panes.len(), 2);
1339 }
1340
1341 #[tokio::test]
1342 async fn kill_pane() {
1343 let mut mgr = new_manager();
1344 let (_, pane0) = mgr.new_session("s1", None).unwrap();
1345 let pane1 = mgr.split_pane(&pane0, true, None).unwrap();
1346
1347 mgr.kill_pane(&pane1).unwrap();
1348 let panes = mgr.list_panes("s1").unwrap();
1349 assert_eq!(panes.len(), 1);
1350 assert_eq!(panes[0].id, pane0);
1351 }
1352
1353 #[tokio::test]
1354 async fn kill_cascade_pane_to_window_to_session() {
1355 let mut mgr = new_manager();
1356 let (_, pane0) = mgr.new_session("s1", None).unwrap();
1357
1358 mgr.kill_pane(&pane0).unwrap();
1359 assert!(!mgr.has_session("s1"));
1360 }
1361
1362 #[tokio::test]
1363 async fn kill_last_window_kills_session() {
1364 let mut mgr = new_manager();
1365 mgr.new_session("s1", None).unwrap();
1366
1367 mgr.kill_window("s1", 0).unwrap();
1368 assert!(!mgr.has_session("s1"));
1369 }
1370
1371 #[tokio::test]
1372 async fn pane_ids_globally_unique_and_sequential() {
1373 let mut mgr = new_manager();
1374 mgr.new_session("s1", None).unwrap(); mgr.new_session("s2", None).unwrap(); let pane2 = mgr.split_pane("%0", true, None).unwrap(); assert_eq!(pane2, "%2");
1378
1379 let (_, pane3) = mgr.new_window("s1", None).unwrap(); assert_eq!(pane3, "%3");
1381 }
1382
1383 #[tokio::test]
1384 async fn find_pane_across_sessions() {
1385 let mut mgr = new_manager();
1386 mgr.new_session("s1", None).unwrap(); mgr.new_session("s2", None).unwrap(); let result = mgr.find_pane("%1");
1390 assert!(result.is_some());
1391 let (session, _window, pane) = result.unwrap();
1392 assert_eq!(session.name, "s2");
1393 assert_eq!(pane.id, "%1");
1394
1395 assert!(mgr.find_pane("%99").is_none());
1396 }
1397
1398 #[tokio::test]
1399 async fn select_window() {
1400 let mut mgr = new_manager();
1401 mgr.new_session("s1", None).unwrap();
1402 mgr.new_window("s1", None).unwrap(); assert_eq!(mgr.get_session("s1").unwrap().active_window, 0);
1406
1407 mgr.select_window("s1", 1).unwrap();
1408 assert_eq!(mgr.get_session("s1").unwrap().active_window, 1);
1409
1410 let err = mgr.select_window("s1", 99).unwrap_err();
1412 assert!(err.to_string().contains("not found"));
1413
1414 let err = mgr.select_window("nope", 0).unwrap_err();
1416 assert!(err.to_string().contains("not found"));
1417 }
1418
1419 #[tokio::test]
1420 async fn select_pane() {
1421 let mut mgr = new_manager();
1422 let (_, pane0) = mgr.new_session("s1", None).unwrap();
1423 let pane1 = mgr.split_pane(&pane0, true, None).unwrap();
1424
1425 let (_, window, _) = mgr.find_pane(&pane0).unwrap();
1427 assert_eq!(window.active_pane, 0);
1428
1429 mgr.select_pane(&pane1).unwrap();
1430 let (_, window, _) = mgr.find_pane(&pane0).unwrap();
1431 assert_eq!(window.active_pane, 1);
1432
1433 let err = mgr.select_pane("%99").unwrap_err();
1435 assert!(err.to_string().contains("pane not found"));
1436 }
1437
1438 #[tokio::test]
1439 async fn send_keys_to_pane() {
1440 let mut mgr = new_manager();
1441 let (_, pane_id) = mgr.new_session("s1", None).unwrap();
1442 mgr.send_keys(&pane_id, "hello\r").unwrap();
1443 }
1444
1445 #[tokio::test]
1446 async fn send_keys_unknown_pane_errors() {
1447 let mgr = new_manager();
1448 let err = mgr.send_keys("%99", "hello").unwrap_err();
1449 assert!(err.to_string().contains("pane not found"));
1450 }
1451
1452 #[tokio::test]
1453 async fn capture_pane_returns_screen_content() {
1454 let mut mgr = new_manager();
1455 let (_, pane_id) = mgr.new_session("s1", None).unwrap();
1456
1457 {
1459 let (_, _, pane) = mgr.find_pane(&pane_id).unwrap();
1460 let mut screen = pane.screen.write().await;
1461 screen.feed(b"hello world");
1462 }
1463
1464 let (screen_arc, process) = mgr.get_capture_info(&pane_id).unwrap();
1465 let screen = screen_arc.read().await;
1466 assert_eq!(screen.capture_text()[0], "hello world");
1467 assert_eq!(screen.size(), (80, 24));
1468 assert!(process.running);
1469 }
1470
1471 #[tokio::test]
1472 async fn set_default_shell_option() {
1473 let mut mgr = new_manager();
1474 mgr.new_session("s1", None).unwrap();
1475
1476 let session = mgr.get_session_mut("s1").unwrap();
1478 session.options.default_shell = "cmd.exe".to_string();
1479
1480 let (_, pane_id) = mgr.new_window("s1", None).unwrap();
1482 let (_, _, pane) = mgr.find_pane(&pane_id).unwrap();
1483 assert_eq!(pane.title, "cmd.exe");
1485 }
1486
1487 #[tokio::test]
1488 async fn set_prompt_pattern_option() {
1489 let mut mgr = new_manager();
1490 mgr.new_session("s1", None).unwrap();
1491
1492 let session = mgr.get_session_mut("s1").unwrap();
1494 session.options.prompt_pattern = r"PS.*>".to_string();
1495
1496 let pattern = mgr.get_prompt_pattern("%0");
1498 assert_eq!(pattern, Some(r"PS.*>".to_string()));
1499 }
1500
1501 #[tokio::test]
1502 async fn remain_on_exit_option() {
1503 let mut mgr = new_manager();
1504 mgr.new_session("s1", None).unwrap();
1505
1506 let pane = mgr.find_pane_mut("%0").unwrap();
1508 pane.options.remain_on_exit = true;
1509 assert!(pane.options.remain_on_exit);
1510
1511 let pane = mgr.find_pane_mut("%0").unwrap();
1513 pane.options.remain_on_exit = false;
1514 assert!(!pane.options.remain_on_exit);
1515 }
1516
1517 #[tokio::test]
1518 async fn send_keys_to_dead_pane_errors() {
1519 let mut mgr = new_manager();
1520 let (_, pane_id) = mgr.new_session("s1", None).unwrap();
1521
1522 let pane = mgr.find_pane_mut(&pane_id).unwrap();
1524 pane.process.running = false;
1525 pane.process.exit_code = Some(0);
1526
1527 let err = mgr.send_keys(&pane_id, "hello").unwrap_err();
1529 assert!(err.to_string().contains("has exited"));
1530 }
1531
1532 #[tokio::test]
1533 async fn capture_dead_pane_still_works() {
1534 let mut mgr = new_manager();
1535 let (_, pane_id) = mgr.new_session("s1", None).unwrap();
1536
1537 {
1539 let (_, _, pane) = mgr.find_pane(&pane_id).unwrap();
1540 let mut screen = pane.screen.write().await;
1541 screen.feed(b"last output");
1542 }
1543
1544 let pane = mgr.find_pane_mut(&pane_id).unwrap();
1546 pane.options.remain_on_exit = true;
1547 pane.process.running = false;
1548 pane.process.exit_code = Some(0);
1549
1550 let (screen_arc, process) = mgr.get_capture_info(&pane_id).unwrap();
1552 let screen = screen_arc.read().await;
1553 assert_eq!(screen.capture_text()[0], "last output");
1554 assert!(!process.running);
1555 assert_eq!(process.exit_code, Some(0));
1556 }
1557
1558 #[tokio::test]
1559 async fn resize_pane_pty_updates_size() {
1560 let mut mgr = new_manager();
1561 let (_, pane_id) = mgr.new_session("s1", None).unwrap();
1562
1563 let (_, _, pane) = mgr.find_pane(&pane_id).unwrap();
1565 assert_eq!(pane.size, (80, 24));
1566
1567 let screen_arc = mgr.resize_pane_pty(&pane_id, 120, 40).unwrap();
1568 let (_, _, pane) = mgr.find_pane(&pane_id).unwrap();
1570 assert_eq!(pane.size, (120, 40));
1571
1572 {
1574 let mut screen = screen_arc.write().await;
1575 screen.resize(120, 40);
1576 assert_eq!(screen.size(), (120, 40));
1577 }
1578 }
1579
1580 #[tokio::test]
1581 async fn resize_nonexistent_pane_errors() {
1582 let mut mgr = new_manager();
1583 let result = mgr.resize_pane_pty("%99", 80, 24);
1584 assert!(result.is_err());
1585 assert!(result.err().unwrap().to_string().contains("pane not found"));
1586 }
1587
1588 #[tokio::test]
1589 async fn resize_exited_pane_errors() {
1590 let mut mgr = new_manager();
1591 let (_, pane_id) = mgr.new_session("s1", None).unwrap();
1592
1593 let pane = mgr.find_pane_mut(&pane_id).unwrap();
1594 pane.process.running = false;
1595 pane.process.exit_code = Some(0);
1596
1597 let result = mgr.resize_pane_pty(&pane_id, 80, 24);
1598 assert!(result.is_err());
1599 assert!(result.err().unwrap().to_string().contains("has exited"));
1600 }
1601
1602 #[tokio::test]
1603 async fn resize_out_of_range_dimensions_errors() {
1604 let mut mgr = new_manager();
1605 let (_, pane_id) = mgr.new_session("s1", None).unwrap();
1606
1607 let result = mgr.resize_pane_pty(&pane_id, 0, 0);
1609 assert!(result.is_err());
1610 assert!(result.err().unwrap().to_string().contains("too small"));
1611
1612 let result = mgr.resize_pane_pty(&pane_id, 1, 1);
1613 assert!(result.is_err());
1614 assert!(result.err().unwrap().to_string().contains("too small"));
1615
1616 let result = mgr.resize_pane_pty(&pane_id, 1001, 24);
1618 assert!(result.is_err());
1619 assert!(result.err().unwrap().to_string().contains("too large"));
1620
1621 let result = mgr.resize_pane_pty(&pane_id, 80, 1001);
1622 assert!(result.is_err());
1623 assert!(result.err().unwrap().to_string().contains("too large"));
1624 }
1625
1626 #[tokio::test]
1627 async fn new_pane_inherits_session_scrollback_limit() {
1628 let mut mgr = new_manager();
1629 let (_, pane0) = mgr.new_session("s1", None).unwrap();
1630
1631 {
1633 let (_, _, pane) = mgr.find_pane(&pane0).unwrap();
1634 let screen = pane.screen.read().await;
1635 assert_eq!(screen.scrollback_limit(), 2000);
1636 }
1637
1638 let session = mgr.get_session_mut("s1").unwrap();
1640 session.options.scrollback_limit = 500;
1641
1642 let pane1 = mgr.split_pane(&pane0, true, None).unwrap();
1644 {
1645 let (_, _, pane) = mgr.find_pane(&pane1).unwrap();
1646 let screen = pane.screen.read().await;
1647 assert_eq!(screen.scrollback_limit(), 500);
1648 }
1649 }
1650
1651 #[tokio::test]
1652 async fn session_scrollback_limit_does_not_affect_existing_panes() {
1653 let mut mgr = new_manager();
1654 let (_, pane0) = mgr.new_session("s1", None).unwrap();
1655
1656 let session = mgr.get_session_mut("s1").unwrap();
1658 session.options.scrollback_limit = 100;
1659
1660 {
1662 let (_, _, pane) = mgr.find_pane(&pane0).unwrap();
1663 let screen = pane.screen.read().await;
1664 assert_eq!(screen.scrollback_limit(), 2000);
1665 }
1666 }
1667
1668 #[tokio::test]
1669 async fn pane_index_consistent_after_operations() {
1670 let mut mgr = new_manager();
1671 let (_, s1_p0) = mgr.new_session("s1", None).unwrap();
1672 let (_, s2_p0) = mgr.new_session("s2", None).unwrap();
1673 let s1_p1 = mgr.split_pane(&s1_p0, true, None).unwrap();
1674 let s2_p1 = mgr.split_pane(&s2_p0, true, None).unwrap();
1675
1676 assert!(mgr.find_pane(&s1_p0).is_some());
1678 assert!(mgr.find_pane(&s1_p1).is_some());
1679 assert!(mgr.find_pane(&s2_p0).is_some());
1680 assert!(mgr.find_pane(&s2_p1).is_some());
1681
1682 mgr.kill_pane(&s1_p1).unwrap();
1684 mgr.kill_pane(&s2_p0).unwrap();
1685
1686 assert!(mgr.find_pane(&s1_p1).is_none());
1688 assert!(mgr.find_pane(&s2_p0).is_none());
1689 assert!(mgr.find_pane(&s1_p0).is_some());
1690 assert!(mgr.find_pane(&s2_p1).is_some());
1691 }
1692
1693 #[tokio::test]
1694 async fn pane_index_after_window_kill() {
1695 let mut mgr = new_manager();
1696 let (_, p0) = mgr.new_session("s1", None).unwrap(); let (_, p1) = mgr.new_window("s1", None).unwrap(); assert!(mgr.find_pane(&p0).is_some());
1701 assert!(mgr.find_pane(&p1).is_some());
1702
1703 mgr.kill_window("s1", 0).unwrap();
1705
1706 assert!(mgr.find_pane(&p0).is_none());
1708 assert!(mgr.find_pane(&p1).is_some());
1709 }
1710
1711 #[tokio::test]
1712 async fn pane_index_after_session_kill() {
1713 let mut mgr = new_manager();
1714 let (_, s1_p0) = mgr.new_session("s1", None).unwrap();
1715 let s1_p1 = mgr.split_pane(&s1_p0, true, None).unwrap();
1716 let (_, s2_p0) = mgr.new_session("s2", None).unwrap();
1717
1718 assert!(mgr.find_pane(&s1_p0).is_some());
1720 assert!(mgr.find_pane(&s1_p1).is_some());
1721 assert!(mgr.find_pane(&s2_p0).is_some());
1722
1723 mgr.kill_session("s1").unwrap();
1725
1726 assert!(mgr.find_pane(&s1_p0).is_none());
1728 assert!(mgr.find_pane(&s1_p1).is_none());
1729 assert!(mgr.find_pane(&s2_p0).is_some());
1730 }
1731
1732 #[tokio::test]
1735 async fn set_mark_and_resolve() {
1736 let mut mgr = new_manager();
1737 let (_, pane_id) = mgr.new_session("s1", None).unwrap();
1738 mgr.set_mark("build", &pane_id).unwrap();
1739
1740 let resolved = mgr.resolve_pane_target("@build").unwrap();
1741 assert_eq!(resolved, pane_id);
1742 }
1743
1744 #[tokio::test]
1745 async fn resolve_passthrough() {
1746 let mgr = new_manager();
1747 let resolved = mgr.resolve_pane_target("%0").unwrap();
1748 assert_eq!(resolved, "%0");
1749 }
1750
1751 #[tokio::test]
1752 async fn resolve_unknown_mark_errors() {
1753 let mgr = new_manager();
1754 let err = mgr.resolve_pane_target("@nonexistent").unwrap_err();
1755 assert!(err.to_string().contains("mark not found"));
1756 }
1757
1758 #[tokio::test]
1759 async fn validate_mark_name_rules() {
1760 use super::validate_mark_name;
1761
1762 validate_mark_name("build").unwrap();
1764 validate_mark_name("my-pane").unwrap();
1765 validate_mark_name("test_1").unwrap();
1766 validate_mark_name("A").unwrap();
1767 validate_mark_name("a".repeat(32).as_str()).unwrap();
1768
1769 assert!(validate_mark_name("").is_err());
1771 assert!(validate_mark_name(&"a".repeat(33)).is_err());
1773 assert!(validate_mark_name("hello world").is_err());
1775 assert!(validate_mark_name("foo@bar").is_err());
1776 assert!(validate_mark_name("a.b").is_err());
1777 }
1778
1779 #[tokio::test]
1780 async fn reassign_mark() {
1781 let mut mgr = new_manager();
1782 let (_, p0) = mgr.new_session("s1", None).unwrap();
1783 let p1 = mgr.split_pane(&p0, true, None).unwrap();
1784
1785 mgr.set_mark("target", &p0).unwrap();
1786 assert_eq!(mgr.resolve_pane_target("@target").unwrap(), p0);
1787
1788 mgr.set_mark("target", &p1).unwrap();
1790 assert_eq!(mgr.resolve_pane_target("@target").unwrap(), p1);
1791 }
1792
1793 #[tokio::test]
1794 async fn multiple_marks_per_pane() {
1795 let mut mgr = new_manager();
1796 let (_, pane_id) = mgr.new_session("s1", None).unwrap();
1797
1798 mgr.set_mark("build", &pane_id).unwrap();
1799 mgr.set_mark("main", &pane_id).unwrap();
1800 mgr.set_mark("dev", &pane_id).unwrap();
1801
1802 assert_eq!(mgr.resolve_pane_target("@build").unwrap(), pane_id);
1803 assert_eq!(mgr.resolve_pane_target("@main").unwrap(), pane_id);
1804 assert_eq!(mgr.resolve_pane_target("@dev").unwrap(), pane_id);
1805
1806 let mut marks = mgr.marks_for_pane(&pane_id);
1807 marks.sort();
1808 assert_eq!(marks, vec!["build", "dev", "main"]);
1809 }
1810
1811 #[tokio::test]
1812 async fn kill_pane_cleans_marks() {
1813 let mut mgr = new_manager();
1814 let (_, p0) = mgr.new_session("s1", None).unwrap();
1815 let p1 = mgr.split_pane(&p0, true, None).unwrap();
1816
1817 mgr.set_mark("a", &p0).unwrap();
1818 mgr.set_mark("b", &p1).unwrap();
1819
1820 mgr.kill_pane(&p0).unwrap();
1821 assert!(mgr.resolve_pane_target("@a").is_err());
1822 assert_eq!(mgr.resolve_pane_target("@b").unwrap(), p1);
1824 }
1825
1826 #[tokio::test]
1827 async fn kill_session_cleans_marks() {
1828 let mut mgr = new_manager();
1829 let (_, s1_p0) = mgr.new_session("s1", None).unwrap();
1830 let s1_p1 = mgr.split_pane(&s1_p0, true, None).unwrap();
1831 let (_, s2_p0) = mgr.new_session("s2", None).unwrap();
1832
1833 mgr.set_mark("a", &s1_p0).unwrap();
1834 mgr.set_mark("b", &s1_p1).unwrap();
1835 mgr.set_mark("c", &s2_p0).unwrap();
1836
1837 mgr.kill_session("s1").unwrap();
1838 assert!(mgr.resolve_pane_target("@a").is_err());
1839 assert!(mgr.resolve_pane_target("@b").is_err());
1840 assert_eq!(mgr.resolve_pane_target("@c").unwrap(), s2_p0);
1841 }
1842
1843 #[tokio::test]
1844 async fn kill_window_cleans_marks() {
1845 let mut mgr = new_manager();
1846 let (_, p0) = mgr.new_session("s1", None).unwrap();
1847 let (_, p1) = mgr.new_window("s1", None).unwrap();
1848
1849 mgr.set_mark("win0", &p0).unwrap();
1850 mgr.set_mark("win1", &p1).unwrap();
1851
1852 mgr.kill_window("s1", 0).unwrap();
1853 assert!(mgr.resolve_pane_target("@win0").is_err());
1854 assert_eq!(mgr.resolve_pane_target("@win1").unwrap(), p1);
1855 }
1856
1857 #[tokio::test]
1858 async fn list_marks_returns_all() {
1859 let mut mgr = new_manager();
1860 let (_, p0) = mgr.new_session("s1", None).unwrap();
1861 let p1 = mgr.split_pane(&p0, true, None).unwrap();
1862
1863 mgr.set_mark("alpha", &p0).unwrap();
1864 mgr.set_mark("beta", &p1).unwrap();
1865
1866 let mut marks = mgr.list_marks();
1867 marks.sort_by(|a, b| a.0.cmp(&b.0));
1868 assert_eq!(marks.len(), 2);
1869 assert_eq!(marks[0], ("alpha".to_string(), p0));
1870 assert_eq!(marks[1], ("beta".to_string(), p1));
1871 }
1872
1873 #[tokio::test]
1874 async fn remove_mark() {
1875 let mut mgr = new_manager();
1876 let (_, pane_id) = mgr.new_session("s1", None).unwrap();
1877 mgr.set_mark("temp", &pane_id).unwrap();
1878 mgr.remove_mark("temp").unwrap();
1879 assert!(mgr.resolve_pane_target("@temp").is_err());
1880 }
1881
1882 #[tokio::test]
1883 async fn remove_nonexistent_mark_errors() {
1884 let mut mgr = new_manager();
1885 let err = mgr.remove_mark("nope").unwrap_err();
1886 assert!(err.to_string().contains("mark not found"));
1887 }
1888
1889 #[tokio::test]
1890 async fn set_mark_nonexistent_pane_errors() {
1891 let mut mgr = new_manager();
1892 mgr.new_session("s1", None).unwrap();
1893 let err = mgr.set_mark("x", "%99").unwrap_err();
1894 assert!(err.to_string().contains("pane not found"));
1895 }
1896
1897 #[tokio::test]
1898 async fn inject_marks_populates_pane_info() {
1899 let mut mgr = new_manager();
1900 let (_, p0) = mgr.new_session("s1", None).unwrap();
1901 let p1 = mgr.split_pane(&p0, true, None).unwrap();
1902
1903 mgr.set_mark("build", &p0).unwrap();
1904 mgr.set_mark("test", &p1).unwrap();
1905 mgr.set_mark("main", &p0).unwrap();
1906
1907 let panes = mgr.list_panes("s1").unwrap();
1908 let p0_info = panes.iter().find(|p| p.id == p0).unwrap();
1909 let p1_info = panes.iter().find(|p| p.id == p1).unwrap();
1910
1911 let mut p0_marks = p0_info.marks.clone();
1912 p0_marks.sort();
1913 assert_eq!(p0_marks, vec!["build", "main"]);
1914 assert_eq!(p1_info.marks, vec!["test"]);
1915 }
1916
1917 #[tokio::test]
1920 async fn swap_panes_basic() {
1921 let mut mgr = new_manager();
1922 let (_, p0) = mgr.new_session("s1", None).unwrap();
1923 let p1 = mgr.split_pane(&p0, true, None).unwrap();
1924
1925 let panes_before = mgr.list_panes("s1").unwrap();
1927 assert_eq!(panes_before[0].id, p0);
1928 assert_eq!(panes_before[1].id, p1);
1929
1930 mgr.swap_panes(&p0, &p1).unwrap();
1931
1932 let panes_after = mgr.list_panes("s1").unwrap();
1934 assert_eq!(panes_after[0].id, p1);
1935 assert_eq!(panes_after[1].id, p0);
1936 }
1937
1938 #[tokio::test]
1939 async fn swap_panes_active_tracking() {
1940 let mut mgr = new_manager();
1941 let (_, p0) = mgr.new_session("s1", None).unwrap();
1942 let p1 = mgr.split_pane(&p0, true, None).unwrap();
1943
1944 mgr.select_pane(&p0).unwrap();
1946
1947 mgr.swap_panes(&p0, &p1).unwrap();
1948
1949 let session = mgr.get_session("s1").unwrap();
1951 let window = &session.windows[0];
1952 assert_eq!(window.panes[window.active_pane].id, p0);
1953 }
1954
1955 #[tokio::test]
1956 async fn swap_panes_noop() {
1957 let mut mgr = new_manager();
1958 let (_, p0) = mgr.new_session("s1", None).unwrap();
1959 mgr.split_pane(&p0, true, None).unwrap();
1960
1961 mgr.swap_panes(&p0, &p0).unwrap();
1963
1964 let panes = mgr.list_panes("s1").unwrap();
1965 assert_eq!(panes[0].id, p0);
1966 }
1967
1968 #[tokio::test]
1969 async fn swap_panes_cross_window_error() {
1970 let mut mgr = new_manager();
1971 let (_, p0) = mgr.new_session("s1", None).unwrap();
1972 let (_, p1) = mgr.new_window("s1", None).unwrap();
1973
1974 let err = mgr.swap_panes(&p0, &p1).unwrap_err();
1975 assert!(err.to_string().contains("same window"));
1976 }
1977
1978 #[tokio::test]
1979 async fn swap_panes_not_found() {
1980 let mut mgr = new_manager();
1981 let (_, p0) = mgr.new_session("s1", None).unwrap();
1982
1983 let err = mgr.swap_panes(&p0, "%99").unwrap_err();
1984 assert!(err.to_string().contains("pane not found"));
1985
1986 let err = mgr.swap_panes("%99", &p0).unwrap_err();
1987 assert!(err.to_string().contains("pane not found"));
1988 }
1989
1990 #[tokio::test]
1991 async fn swap_panes_three_panes() {
1992 let mut mgr = new_manager();
1993 let (_, p0) = mgr.new_session("s1", None).unwrap();
1994 let p1 = mgr.split_pane(&p0, true, None).unwrap();
1995 let p2 = mgr.split_pane(&p1, true, None).unwrap();
1996
1997 mgr.swap_panes(&p0, &p2).unwrap();
1999
2000 let panes = mgr.list_panes("s1").unwrap();
2001 assert_eq!(panes[0].id, p2);
2002 assert_eq!(panes[1].id, p1);
2003 assert_eq!(panes[2].id, p0);
2004 }
2005
2006 #[tokio::test]
2007 async fn swap_panes_marks_survive() {
2008 let mut mgr = new_manager();
2009 let (_, p0) = mgr.new_session("s1", None).unwrap();
2010 let p1 = mgr.split_pane(&p0, true, None).unwrap();
2011
2012 mgr.set_mark("build", &p0).unwrap();
2013 mgr.set_mark("test", &p1).unwrap();
2014
2015 mgr.swap_panes(&p0, &p1).unwrap();
2016
2017 assert_eq!(mgr.resolve_pane_target("@build").unwrap(), p0);
2019 assert_eq!(mgr.resolve_pane_target("@test").unwrap(), p1);
2020 }
2021
2022 #[tokio::test]
2025 async fn move_pane_basic() {
2026 let mut mgr = new_manager();
2027 let (_, p0) = mgr.new_session("src", None).unwrap();
2028 let p1 = mgr.split_pane(&p0, true, None).unwrap();
2029 let (_, _p2) = mgr.new_session("dst", None).unwrap();
2030
2031 let dst_windows = mgr.list_windows("dst").unwrap();
2033 let dst_win = dst_windows[0].id;
2034
2035 mgr.move_pane(&p1, "dst", dst_win).unwrap();
2036
2037 let dst_panes = mgr.list_panes("dst").unwrap();
2039 assert!(dst_panes.iter().any(|p| p.id == p1));
2040
2041 let src_panes = mgr.list_panes("src").unwrap();
2043 assert!(!src_panes.iter().any(|p| p.id == p1));
2044 }
2045
2046 #[tokio::test]
2047 async fn move_pane_cascade_kills_window_and_session() {
2048 let mut mgr = new_manager();
2049 let (_, p0) = mgr.new_session("src", None).unwrap();
2051 let (_, _p1) = mgr.new_session("dst", None).unwrap();
2052
2053 let dst_windows = mgr.list_windows("dst").unwrap();
2054 let dst_win = dst_windows[0].id;
2055
2056 mgr.move_pane(&p0, "dst", dst_win).unwrap();
2058
2059 assert!(!mgr.has_session("src"));
2061 }
2062
2063 #[tokio::test]
2064 async fn move_pane_cascade_kills_window_not_session() {
2065 let mut mgr = new_manager();
2066 let (_, p0) = mgr.new_session("src", None).unwrap();
2067 let (_, _p_extra) = mgr.new_window("src", None).unwrap();
2069 let (_, _p1) = mgr.new_session("dst", None).unwrap();
2070
2071 let dst_windows = mgr.list_windows("dst").unwrap();
2072 let dst_win = dst_windows[0].id;
2073
2074 mgr.move_pane(&p0, "dst", dst_win).unwrap();
2076
2077 assert!(mgr.has_session("src"));
2079 let src_windows = mgr.list_windows("src").unwrap();
2080 assert_eq!(src_windows.len(), 1);
2081 assert!(!src_windows.iter().any(|w| w.id == 0));
2083 }
2084
2085 #[tokio::test]
2086 async fn move_pane_same_window_noop() {
2087 let mut mgr = new_manager();
2088 let (_, p0) = mgr.new_session("s1", None).unwrap();
2089 let _ = mgr.split_pane(&p0, true, None).unwrap();
2090
2091 let windows = mgr.list_windows("s1").unwrap();
2092 let win_id = windows[0].id;
2093
2094 mgr.move_pane(&p0, "s1", win_id).unwrap();
2096
2097 let panes = mgr.list_panes("s1").unwrap();
2099 assert_eq!(panes.len(), 2);
2100 }
2101
2102 #[tokio::test]
2103 async fn move_pane_active_pane_update() {
2104 let mut mgr = new_manager();
2105 let (_, p0) = mgr.new_session("src", None).unwrap();
2106 let p1 = mgr.split_pane(&p0, true, None).unwrap();
2107 let (_, _p2) = mgr.new_session("dst", None).unwrap();
2108
2109 let dst_windows = mgr.list_windows("dst").unwrap();
2110 let dst_win = dst_windows[0].id;
2111
2112 mgr.move_pane(&p1, "dst", dst_win).unwrap();
2114
2115 let dst_session = mgr.get_session("dst").unwrap();
2117 let dst_window = dst_session.find_window(dst_win).unwrap();
2118 assert_eq!(dst_window.panes[dst_window.active_pane].id, p1);
2119 }
2120
2121 #[tokio::test]
2122 async fn move_pane_layout_update() {
2123 let mut mgr = new_manager();
2124 let (_, p0) = mgr.new_session("src", None).unwrap();
2125 let p1 = mgr.split_pane(&p0, true, None).unwrap();
2126 let (_, _p2) = mgr.new_session("dst", None).unwrap();
2127
2128 let dst_windows = mgr.list_windows("dst").unwrap();
2129 let dst_win = dst_windows[0].id;
2130
2131 let src_session = mgr.get_session("src").unwrap();
2133 assert_eq!(src_session.windows[0].layout, Layout::EvenHorizontal);
2134 let dst_session = mgr.get_session("dst").unwrap();
2135 let dst_window = dst_session.find_window(dst_win).unwrap();
2136 assert_eq!(dst_window.layout, Layout::Single);
2137
2138 mgr.move_pane(&p1, "dst", dst_win).unwrap();
2139
2140 let src_session = mgr.get_session("src").unwrap();
2142 assert_eq!(src_session.windows[0].layout, Layout::Single);
2143 let dst_session = mgr.get_session("dst").unwrap();
2144 let dst_window = dst_session.find_window(dst_win).unwrap();
2145 assert_eq!(dst_window.layout, Layout::EvenHorizontal);
2146 }
2147
2148 #[tokio::test]
2149 async fn move_pane_index_updated() {
2150 let mut mgr = new_manager();
2151 let (_, p0) = mgr.new_session("src", None).unwrap();
2152 let p1 = mgr.split_pane(&p0, true, None).unwrap();
2153 let (_, _p2) = mgr.new_session("dst", None).unwrap();
2154
2155 let dst_windows = mgr.list_windows("dst").unwrap();
2156 let dst_win = dst_windows[0].id;
2157
2158 mgr.move_pane(&p1, "dst", dst_win).unwrap();
2159
2160 let (session, window, pane) = mgr.find_pane(&p1).unwrap();
2162 assert_eq!(session.name, "dst");
2163 assert_eq!(window.id, dst_win);
2164 assert_eq!(pane.id, p1);
2165 }
2166
2167 #[tokio::test]
2168 async fn move_pane_marks_survive() {
2169 let mut mgr = new_manager();
2170 let (_, p0) = mgr.new_session("src", None).unwrap();
2171 let p1 = mgr.split_pane(&p0, true, None).unwrap();
2172 let (_, _p2) = mgr.new_session("dst", None).unwrap();
2173
2174 mgr.set_mark("build", &p1).unwrap();
2175
2176 let dst_windows = mgr.list_windows("dst").unwrap();
2177 let dst_win = dst_windows[0].id;
2178
2179 mgr.move_pane(&p1, "dst", dst_win).unwrap();
2180
2181 assert_eq!(mgr.resolve_pane_target("@build").unwrap(), p1);
2183 }
2184
2185 #[tokio::test]
2186 async fn move_pane_not_found() {
2187 let mut mgr = new_manager();
2188 let (_, _p0) = mgr.new_session("s1", None).unwrap();
2189
2190 let err = mgr.move_pane("%99", "s1", 0).unwrap_err();
2191 assert!(err.to_string().contains("pane not found"));
2192 }
2193
2194 #[tokio::test]
2195 async fn move_pane_target_session_not_found() {
2196 let mut mgr = new_manager();
2197 let (_, p0) = mgr.new_session("s1", None).unwrap();
2198
2199 let err = mgr.move_pane(&p0, "nonexistent", 0).unwrap_err();
2200 assert!(err.to_string().contains("session not found"));
2201 }
2202
2203 #[tokio::test]
2204 async fn move_pane_target_window_not_found() {
2205 let mut mgr = new_manager();
2206 let (_, p0) = mgr.new_session("s1", None).unwrap();
2207 let (_, _p1) = mgr.new_session("dst", None).unwrap();
2208
2209 let err = mgr.move_pane(&p0, "dst", 99).unwrap_err();
2210 assert!(err.to_string().contains("window not found"));
2211 }
2212
2213 #[tokio::test]
2214 async fn server_status_counts() {
2215 let mut mgr = new_manager();
2216
2217 let status = mgr.server_status();
2219 assert_eq!(status.sessions, 0);
2220 assert_eq!(status.windows, 0);
2221 assert_eq!(status.panes, 0);
2222
2223 let (_, p0) = mgr.new_session("s1", None).unwrap();
2225 mgr.split_pane(&p0, true, None).unwrap();
2226 mgr.new_session("s2", None).unwrap();
2227 mgr.new_window("s2", None).unwrap();
2228
2229 let status = mgr.server_status();
2230 assert_eq!(status.sessions, 2);
2231 assert_eq!(status.windows, 3); assert_eq!(status.panes, 4); assert!(status.uptime_secs < 5);
2236 }
2237}