1use crate::{Input, Locator, Pane, PaneSet, Result};
4
5#[derive(Debug, Clone)]
7pub struct PaneKeyboard {
8 pane: Pane,
9}
10
11impl PaneKeyboard {
12 pub(crate) const fn new(pane: Pane) -> Self {
13 Self { pane }
14 }
15
16 pub async fn type_text(&self, text: impl AsRef<str>) -> Result<()> {
18 self.pane.send_text(text.as_ref()).await
19 }
20
21 pub async fn press(&self, key: impl AsRef<str>) -> Result<()> {
26 self.pane.send_key(normalize_key_token(key.as_ref())).await
27 }
28}
29
30impl Pane {
31 #[must_use]
33 pub fn keyboard(&self) -> PaneKeyboard {
34 PaneKeyboard::new(self.clone())
35 }
36
37 #[must_use]
39 pub fn mouse(&self) -> PaneMouse {
40 PaneMouse::new(self.clone())
41 }
42}
43
44#[derive(Debug, Clone)]
46pub struct PaneSetKeyboard {
47 panes: PaneSet,
48}
49
50impl PaneSetKeyboard {
51 pub(crate) fn new(panes: PaneSet) -> Self {
52 Self { panes }
53 }
54
55 pub async fn type_text(&self, text: impl AsRef<str>) -> Result<()> {
57 self.panes.broadcast(Input::text(text.as_ref())).await?;
58 Ok(())
59 }
60
61 pub async fn press(&self, key: impl AsRef<str>) -> Result<()> {
63 let key = normalize_key_token(key.as_ref());
64 self.panes.broadcast(Input::key(&key)).await?;
65 Ok(())
66 }
67}
68
69impl PaneSet {
70 #[must_use]
72 pub fn keyboard(&self) -> PaneSetKeyboard {
73 PaneSetKeyboard::new(self.clone())
74 }
75}
76
77#[derive(Debug, Clone)]
84pub struct PaneMouse {
85 pane: Pane,
86}
87
88impl PaneMouse {
89 pub(crate) const fn new(pane: Pane) -> Self {
90 Self { pane }
91 }
92
93 pub async fn move_to(&self, row: u16, col: u16) -> Result<()> {
99 self.pane
100 .send_text(sgr_mouse_sequence(35, row, col, true))
101 .await
102 }
103
104 pub async fn click(&self, row: u16, col: u16) -> Result<()> {
110 self.pane
111 .send_text(sgr_mouse_sequence(0, row, col, true))
112 .await?;
113 self.pane
114 .send_text(sgr_mouse_sequence(0, row, col, false))
115 .await
116 }
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
121#[non_exhaustive]
122pub enum FillStrategy {
123 ControlU,
125 Backspace(usize),
127 None,
129}
130
131impl Locator {
132 pub async fn click(self) -> Result<()> {
134 let (_snapshot, item) = self.resolve_strict_with_wait().await?;
135 self.pane()
136 .mouse()
137 .click(item.text_match.start_row, item.text_match.start_col)
138 .await
139 }
140
141 pub async fn hover(self) -> Result<()> {
143 let (_snapshot, item) = self.resolve_strict_with_wait().await?;
144 self.pane()
145 .mouse()
146 .move_to(item.text_match.start_row, item.text_match.start_col)
147 .await
148 }
149
150 pub async fn fill(self, text: impl AsRef<str>) -> Result<()> {
157 self.fill_with(text, FillStrategy::ControlU).await
158 }
159
160 pub async fn fill_with(self, text: impl AsRef<str>, strategy: FillStrategy) -> Result<()> {
165 let pane = self.pane().clone();
166 let (_snapshot, _item) = self.resolve_strict_with_wait().await?;
167 let keyboard = pane.keyboard();
168 match strategy {
169 FillStrategy::ControlU => keyboard.press("C-u").await?,
170 FillStrategy::Backspace(count) => {
171 for _ in 0..count {
172 keyboard.press("Backspace").await?;
173 }
174 }
175 FillStrategy::None => {}
176 }
177 keyboard.type_text(text.as_ref()).await
178 }
179}
180
181fn normalize_key_token(key: &str) -> String {
182 let Some((modifiers, key_name)) = key.rsplit_once('+') else {
183 return key.to_owned();
184 };
185 if key_name.is_empty() {
186 return key.to_owned();
187 }
188
189 let mut normalized = Vec::new();
190 for modifier in modifiers.split('+') {
191 match modifier.to_ascii_lowercase().as_str() {
192 "control" | "ctrl" => normalized.push("C"),
193 "alt" | "meta" | "option" => normalized.push("M"),
194 "shift" => normalized.push("S"),
195 _ => return key.to_owned(),
196 }
197 }
198 if normalized.is_empty() {
199 return key.to_owned();
200 }
201
202 let has_shift = normalized.contains(&"S");
203 let control_only = normalized.len() == 1 && normalized[0] == "C";
204 let key_name = if control_only || (normalized.contains(&"C") && !has_shift) {
205 key_name.to_ascii_lowercase()
206 } else {
207 key_name.to_owned()
208 };
209 format!("{}-{key_name}", normalized.join("-"))
210}
211
212#[cfg(test)]
213fn control_key(rest: &str) -> String {
214 let lowered = rest.to_ascii_lowercase();
215 format!("C-{lowered}")
216}
217
218fn sgr_mouse_sequence(button: u16, row: u16, col: u16, press: bool) -> String {
219 let suffix = if press { 'M' } else { 'm' };
220 let row = row.saturating_add(1);
221 let col = col.saturating_add(1);
222 format!("\x1b[<{button};{col};{row}{suffix}")
223}
224
225#[cfg(test)]
226mod tests {
227 use super::{control_key, normalize_key_token, sgr_mouse_sequence};
228
229 #[test]
230 fn keyboard_tokens_preserve_plain_keys_and_normalize_control_spellings() {
231 assert_eq!(normalize_key_token("Enter"), "Enter");
232 assert_eq!(normalize_key_token("Backspace"), "Backspace");
233 assert_eq!(normalize_key_token("Control+C"), "C-c");
234 assert_eq!(normalize_key_token("Control+["), "C-[");
235 assert_eq!(normalize_key_token("Ctrl+Z"), "C-z");
236 assert_eq!(normalize_key_token("ctrl+c"), "C-c");
237 assert_eq!(normalize_key_token("Alt+x"), "M-x");
238 assert_eq!(normalize_key_token("Meta+x"), "M-x");
239 assert_eq!(normalize_key_token("Option+x"), "M-x");
240 assert_eq!(normalize_key_token("Shift+Tab"), "S-Tab");
241 assert_eq!(normalize_key_token("Control+Shift+T"), "C-S-T");
242 assert_eq!(normalize_key_token("Hyper+X"), "Hyper+X");
243 }
244
245 #[test]
246 fn control_key_lowercases_only_the_control_suffix() {
247 assert_eq!(control_key("C"), "C-c");
248 assert_eq!(control_key("Break"), "C-break");
249 }
250
251 #[test]
252 fn mouse_sequences_use_zero_based_input_and_sgr_coordinates() {
253 assert_eq!(sgr_mouse_sequence(35, 0, 0, true), "\x1b[<35;1;1M");
254 assert_eq!(sgr_mouse_sequence(0, 2, 4, true), "\x1b[<0;5;3M");
255 assert_eq!(sgr_mouse_sequence(0, 2, 4, false), "\x1b[<0;5;3m");
256 }
257
258 #[test]
259 fn mouse_sequences_saturate_at_terminal_protocol_bounds() {
260 assert_eq!(
261 sgr_mouse_sequence(0, u16::MAX, u16::MAX, true),
262 "\x1b[<0;65535;65535M"
263 );
264 }
265}