1use std::path::PathBuf;
7use std::process::Command;
8
9use rootcause::prelude::ResultExt;
10use rootcause::report;
11use serde::Deserialize;
12
13pub fn send_text_to_pane_cmd(text: &str, pane_id: i64) -> String {
15 format!("wezterm cli send-text {text} --pane-id '{pane_id}' --no-paste")
16}
17
18pub fn submit_pane_cmd(pane_id: i64) -> String {
20 format!(r#"printf "\r" | wezterm cli send-text --pane-id '{pane_id}' --no-paste"#)
21}
22
23pub fn activate_pane_cmd(pane_id: i64) -> String {
25 format!("wezterm cli activate-pane --pane-id '{pane_id}'")
26}
27
28pub fn get_current_pane_id() -> rootcause::Result<i64> {
35 let value = std::env::var("WEZTERM_PANE").context("error missing WEZTERM_PANE environment variable")?;
36 Ok(value
37 .parse()
38 .context("error parsing WEZTERM_PANE value as i64")
39 .attach_with(|| format!("value={value:?}"))?)
40}
41
42pub fn get_all_panes(envs: &[(&str, &str)]) -> rootcause::Result<Vec<WeztermPane>> {
51 let mut cmd = Command::new("wezterm");
52 cmd.args(["cli", "list", "--format", "json"]);
53 cmd.envs(envs.iter().copied());
54 Ok(
55 serde_json::from_slice(&cmd.output().context("error running wezterm cli list")?.stdout)
56 .context("error parsing wezterm cli list output")
57 .attach("format=JSON")?,
58 )
59}
60
61pub fn get_sibling_pane_with_titles(
67 panes: &[WeztermPane],
68 current_pane_id: i64,
69 pane_titles: &[&str],
70) -> rootcause::Result<WeztermPane> {
71 let current_pane_tab_id = panes
72 .iter()
73 .find(|w| w.pane_id == current_pane_id)
74 .ok_or_else(|| report!("error finding current pane"))
75 .attach_with(|| format!("pane_id={current_pane_id} panes={panes:#?}"))?
76 .tab_id;
77
78 Ok(panes
79 .iter()
80 .find(|w| w.tab_id == current_pane_tab_id && pane_titles.contains(&w.title.as_str()))
81 .ok_or_else(|| {
82 report!("error finding pane title in tab")
83 .attach(format!("pane_titles={pane_titles:#?} tab_id={current_pane_tab_id}"))
84 })?
85 .clone())
86}
87
88#[derive(Clone, Debug, Deserialize)]
90#[cfg_attr(any(test, feature = "fake"), derive(fake::Dummy))]
91pub struct WeztermPane {
92 pub cursor_shape: String,
94 pub cursor_visibility: String,
96 pub cursor_x: i64,
98 pub cursor_y: i64,
100 pub cwd: PathBuf,
102 pub is_active: bool,
104 pub is_zoomed: bool,
106 pub left_col: i64,
108 pub pane_id: i64,
110 pub size: WeztermPaneSize,
112 pub tab_id: i64,
114 pub tab_title: String,
116 pub title: String,
118 pub top_row: i64,
120 pub tty_name: String,
122 pub window_id: i64,
124 pub window_title: String,
126 pub workspace: String,
128}
129
130impl WeztermPane {
131 pub fn is_sibling_terminal_pane_of(&self, other: &Self) -> bool {
134 self.pane_id != other.pane_id && self.tab_id == other.tab_id && self.cwd.starts_with(&other.cwd)
135 }
136
137 pub fn absolute_cwd(&self) -> PathBuf {
139 let mut path_parts = self.cwd.components();
140 path_parts.next(); path_parts.next(); PathBuf::from("/").join(path_parts.collect::<PathBuf>())
143 }
144}
145
146#[derive(Clone, Debug, Deserialize)]
148#[cfg_attr(any(test, feature = "fake"), derive(fake::Dummy))]
149pub struct WeztermPaneSize {
150 pub cols: i64,
152 pub dpi: i64,
154 pub pixel_height: i64,
156 pub pixel_width: i64,
158 pub rows: i64,
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn send_text_to_pane_cmd_returns_the_expected_bash_cmd_string() {
168 assert_eq!(
169 send_text_to_pane_cmd("echo hi", 7),
170 "wezterm cli send-text echo hi --pane-id '7' --no-paste"
171 );
172 }
173
174 #[test]
175 fn submit_pane_cmd_returns_the_expected_bash_cmd_string() {
176 assert_eq!(
177 submit_pane_cmd(3),
178 "printf \"\\r\" | wezterm cli send-text --pane-id '3' --no-paste"
179 );
180 }
181
182 #[test]
183 fn activate_pane_cmd_returns_the_expected_bash_cmd_string() {
184 assert_eq!(activate_pane_cmd(9), "wezterm cli activate-pane --pane-id '9'");
185 }
186
187 #[test]
188 fn get_sibling_pane_with_titles_returns_the_expected_match_in_same_tab() {
189 let panes = vec![
190 pane_with(1, 10, "file:///host/home/user/project", "hx"),
191 pane_with(2, 10, "file:///host/home/user/project", "shell"),
192 pane_with(3, 11, "file:///host/home/user/other", "hx"),
193 ];
194 assert2::assert!(let Ok(sibling) = get_sibling_pane_with_titles(&panes, 2, &["hx"]));
195 assert_eq!(sibling.pane_id, 1);
196 }
197
198 #[test]
199 fn get_sibling_pane_with_titles_errors_when_no_title_match() {
200 let panes = vec![pane_with(1, 10, "file:///host/home/user/project", "shell")];
201 assert2::assert!(let Err(err) = get_sibling_pane_with_titles(&panes, 1, &["hx"]));
202 assert!(err.to_string().contains("error finding pane title in tab"));
203 }
204
205 #[test]
206 fn absolute_cwd_strips_file_uri_prefix() {
207 let pane = pane_with(1, 10, "file:///localhost/Users/alice/src", "hx");
208 let abs = pane.absolute_cwd();
209 assert!(abs.starts_with("/Users/alice/src"));
210 }
211
212 #[test]
213 fn is_sibling_terminal_pane_of_works_as_expected() {
214 let root = pane_with(1, 10, "file:///localhost/Users/alice/src", "hx");
215 let child = pane_with(2, 10, "file:///localhost/Users/alice/src/project", "shell");
216 let other_tab = pane_with(3, 11, "file:///localhost/Users/alice/src/project", "shell");
217 assert!(child.is_sibling_terminal_pane_of(&root));
218 assert!(!root.is_sibling_terminal_pane_of(&root));
219 assert!(!other_tab.is_sibling_terminal_pane_of(&root));
220 }
221
222 fn pane_with(id: i64, tab: i64, cwd: &str, title: &str) -> WeztermPane {
223 WeztermPane {
224 cursor_shape: "Block".into(),
225 cursor_visibility: "Visible".into(),
226 cursor_x: 0,
227 cursor_y: 0,
228 cwd: PathBuf::from(cwd),
229 is_active: false,
230 is_zoomed: false,
231 left_col: 0,
232 pane_id: id,
233 size: WeztermPaneSize {
234 cols: 80,
235 dpi: 96,
236 pixel_height: 800,
237 pixel_width: 600,
238 rows: 24,
239 },
240 tab_id: tab,
241 tab_title: "tab".into(),
242 title: title.into(),
243 top_row: 0,
244 tty_name: "tty".into(),
245 window_id: 1,
246 window_title: "win".into(),
247 workspace: "default".into(),
248 }
249 }
250}