1use std::path::PathBuf;
7use std::process::Command;
8
9use color_eyre::eyre::WrapErr;
10use color_eyre::eyre::eyre;
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() -> color_eyre::Result<i64> {
35 std::env::var("WEZTERM_PANE")
36 .wrap_err_with(|| eyre!("error missing WEZTERM_PANE environment variable"))
37 .and_then(|value| {
38 value
39 .parse()
40 .wrap_err_with(|| eyre!("error parsing WEZTERM_PANE value as i64 | value={value:?}"))
41 })
42}
43
44pub fn get_all_panes(envs: &[(&str, &str)]) -> color_eyre::Result<Vec<WeztermPane>> {
53 let mut cmd = Command::new("wezterm");
54 cmd.args(["cli", "list", "--format", "json"]);
55 cmd.envs(envs.iter().copied());
56 serde_json::from_slice(
57 &cmd.output()
58 .wrap_err_with(|| eyre!("error running wezterm cli list"))?
59 .stdout,
60 )
61 .wrap_err_with(|| eyre!("error parsing wezterm cli list output | format=JSON"))
62}
63
64pub fn get_sibling_pane_with_titles(
70 panes: &[WeztermPane],
71 current_pane_id: i64,
72 pane_titles: &[&str],
73) -> color_eyre::Result<WeztermPane> {
74 let current_pane_tab_id = panes
75 .iter()
76 .find(|w| w.pane_id == current_pane_id)
77 .ok_or_else(|| eyre!("error finding current pane | 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 eyre!("error finding pane title in tab | pane_titles={pane_titles:#?} tab_id={current_pane_tab_id}")
85 })?
86 .clone())
87}
88
89#[derive(Clone, Debug, Deserialize)]
91#[cfg_attr(any(test, feature = "fake"), derive(fake::Dummy))]
92pub struct WeztermPane {
93 pub cursor_shape: String,
95 pub cursor_visibility: String,
97 pub cursor_x: i64,
99 pub cursor_y: i64,
101 pub cwd: PathBuf,
103 pub is_active: bool,
105 pub is_zoomed: bool,
107 pub left_col: i64,
109 pub pane_id: i64,
111 pub size: WeztermPaneSize,
113 pub tab_id: i64,
115 pub tab_title: String,
117 pub title: String,
119 pub top_row: i64,
121 pub tty_name: String,
123 pub window_id: i64,
125 pub window_title: String,
127 pub workspace: String,
129}
130
131impl WeztermPane {
132 pub fn is_sibling_terminal_pane_of(&self, other: &Self) -> bool {
135 self.pane_id != other.pane_id && self.tab_id == other.tab_id && self.cwd.starts_with(&other.cwd)
136 }
137
138 pub fn absolute_cwd(&self) -> PathBuf {
140 let mut path_parts = self.cwd.components();
141 path_parts.next(); path_parts.next(); PathBuf::from("/").join(path_parts.collect::<PathBuf>())
144 }
145}
146
147#[derive(Clone, Debug, Deserialize)]
149#[cfg_attr(any(test, feature = "fake"), derive(fake::Dummy))]
150pub struct WeztermPaneSize {
151 pub cols: i64,
153 pub dpi: i64,
155 pub pixel_height: i64,
157 pub pixel_width: i64,
159 pub rows: i64,
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn send_text_to_pane_cmd_returns_the_expected_bash_cmd_string() {
169 assert_eq!(
170 send_text_to_pane_cmd("echo hi", 7),
171 "wezterm cli send-text echo hi --pane-id '7' --no-paste"
172 );
173 }
174
175 #[test]
176 fn submit_pane_cmd_returns_the_expected_bash_cmd_string() {
177 assert_eq!(
178 submit_pane_cmd(3),
179 "printf \"\\r\" | wezterm cli send-text --pane-id '3' --no-paste"
180 );
181 }
182
183 #[test]
184 fn activate_pane_cmd_returns_the_expected_bash_cmd_string() {
185 assert_eq!(activate_pane_cmd(9), "wezterm cli activate-pane --pane-id '9'");
186 }
187
188 #[test]
189 fn get_sibling_pane_with_titles_returns_the_expected_match_in_same_tab() {
190 let panes = vec![
191 pane_with(1, 10, "file:///host/home/user/project", "hx"),
192 pane_with(2, 10, "file:///host/home/user/project", "shell"),
193 pane_with(3, 11, "file:///host/home/user/other", "hx"),
194 ];
195 assert2::let_assert!(Ok(sibling) = get_sibling_pane_with_titles(&panes, 2, &["hx"]));
196 assert_eq!(sibling.pane_id, 1);
197 }
198
199 #[test]
200 fn get_sibling_pane_with_titles_errors_when_no_title_match() {
201 let panes = vec![pane_with(1, 10, "file:///host/home/user/project", "shell")];
202 assert2::let_assert!(Err(err) = get_sibling_pane_with_titles(&panes, 1, &["hx"]));
203 assert!(err.to_string().contains("error finding pane title in tab"));
204 }
205
206 #[test]
207 fn absolute_cwd_strips_file_uri_prefix() {
208 let pane = pane_with(1, 10, "file:///localhost/Users/alice/src", "hx");
209 let abs = pane.absolute_cwd();
210 assert!(abs.starts_with("/Users/alice/src"));
211 }
212
213 #[test]
214 fn is_sibling_terminal_pane_of_works_as_expected() {
215 let root = pane_with(1, 10, "file:///localhost/Users/alice/src", "hx");
216 let child = pane_with(2, 10, "file:///localhost/Users/alice/src/project", "shell");
217 let other_tab = pane_with(3, 11, "file:///localhost/Users/alice/src/project", "shell");
218 assert!(child.is_sibling_terminal_pane_of(&root));
219 assert!(!root.is_sibling_terminal_pane_of(&root));
220 assert!(!other_tab.is_sibling_terminal_pane_of(&root));
221 }
222
223 fn pane_with(id: i64, tab: i64, cwd: &str, title: &str) -> WeztermPane {
224 WeztermPane {
225 cursor_shape: "Block".into(),
226 cursor_visibility: "Visible".into(),
227 cursor_x: 0,
228 cursor_y: 0,
229 cwd: PathBuf::from(cwd),
230 is_active: false,
231 is_zoomed: false,
232 left_col: 0,
233 pane_id: id,
234 size: WeztermPaneSize {
235 cols: 80,
236 dpi: 96,
237 pixel_height: 800,
238 pixel_width: 600,
239 rows: 24,
240 },
241 tab_id: tab,
242 tab_title: "tab".into(),
243 title: title.into(),
244 top_row: 0,
245 tty_name: "tty".into(),
246 window_id: 1,
247 window_title: "win".into(),
248 workspace: "default".into(),
249 }
250 }
251}