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