atm_tmux/lib.rs
1//! ATM Tmux — Thin async wrapper over the tmux CLI.
2//!
3//! Provides the [`TmuxClient`] trait for tmux pane management, with a real
4//! implementation ([`RealTmuxClient`]) that shells out to `tmux` via
5//! `tokio::process::Command`, and a mock ([`MockTmuxClient`]) for testing.
6//!
7//! All code follows the panic-free policy: no `.unwrap()`, `.expect()`,
8//! `panic!()`, `unreachable!()`, `todo!()`, or direct indexing `[i]`.
9
10pub mod client;
11pub mod error;
12pub mod layout;
13pub mod mock;
14
15pub use client::RealTmuxClient;
16pub use error::TmuxError;
17pub use mock::MockTmuxClient;
18
19use async_trait::async_trait;
20use serde::{Deserialize, Serialize};
21
22/// Direction for placing a new pane relative to the target pane.
23///
24/// Maps directly to tmux `split-window` flags:
25/// - `Left` → `-h -b` (horizontal split, new pane before/left)
26/// - `Right` → `-h` (horizontal split, new pane after/right)
27/// - `Above` → `-v -b` (vertical split, new pane before/above)
28/// - `Below` → `-v` (vertical split, new pane after/below)
29///
30/// The `-b` (before) flag requires tmux 3.1+.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
32#[serde(rename_all = "snake_case")]
33pub enum PaneDirection {
34 Left,
35 Right,
36 Above,
37 Below,
38}
39
40/// Information about a single tmux pane.
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub struct PaneInfo {
43 /// Pane ID (e.g., "%5").
44 pub pane_id: String,
45 /// Session name this pane belongs to.
46 pub session_name: String,
47 /// Window index within the session.
48 pub window_index: u32,
49 /// PID of the shell process running in the pane.
50 pub pane_pid: u32,
51 /// Pane width in columns.
52 pub width: u16,
53 /// Pane height in rows.
54 pub height: u16,
55 /// Whether this pane is the currently active pane.
56 pub is_active: bool,
57}
58
59/// Async interface for tmux pane management.
60///
61/// The real implementation shells out to the `tmux` CLI. The mock records
62/// calls for test assertions.
63#[async_trait]
64pub trait TmuxClient: Send + Sync {
65 /// Splits a pane, returning the new pane ID (e.g., "%7").
66 ///
67 /// # Arguments
68 /// * `target` — Target pane to split (e.g., "%5").
69 /// * `size` — Size specification (e.g., "30%", "20").
70 /// * `direction` — Where to place the new pane relative to the target.
71 /// * `command` — Optional command to run in the new pane.
72 async fn split_window(
73 &self,
74 target: &str,
75 size: &str,
76 direction: PaneDirection,
77 command: Option<&str>,
78 ) -> Result<String, TmuxError>;
79
80 /// Returns the current working directory of a pane, or `None` if unavailable.
81 ///
82 /// Queries tmux's `#{pane_current_path}` format variable.
83 async fn get_pane_cwd(&self, pane: &str) -> Result<Option<String>, TmuxError>;
84
85 /// Creates a new window in the given session, returning the new pane ID.
86 async fn new_window(&self, session: &str, command: Option<&str>) -> Result<String, TmuxError>;
87
88 /// Kills (closes) a pane.
89 async fn kill_pane(&self, pane: &str) -> Result<(), TmuxError>;
90
91 /// Resizes a pane.
92 ///
93 /// At least one of `width` or `height` must be `Some`.
94 async fn resize_pane(
95 &self,
96 pane: &str,
97 width: Option<u16>,
98 height: Option<u16>,
99 ) -> Result<(), TmuxError>;
100
101 /// Sends keystrokes to a pane.
102 async fn send_keys(&self, pane: &str, keys: &str) -> Result<(), TmuxError>;
103
104 /// Lists all panes across all sessions.
105 async fn list_panes(&self) -> Result<Vec<PaneInfo>, TmuxError>;
106
107 /// Displays a popup overlay.
108 async fn display_popup(
109 &self,
110 width: &str,
111 height: &str,
112 command: &str,
113 ) -> Result<(), TmuxError>;
114
115 /// Selects (focuses) a pane.
116 async fn select_pane(&self, pane: &str) -> Result<(), TmuxError>;
117
118 /// Captures the visible content of a pane.
119 ///
120 /// Returns the text currently displayed in the pane, one string per line.
121 /// Trailing blank lines are trimmed.
122 async fn capture_pane(&self, pane: &str) -> Result<Vec<String>, TmuxError>;
123
124 /// Creates a new detached tmux session, returning the initial pane ID.
125 async fn new_session(&self, name: &str) -> Result<String, TmuxError>;
126}