1use crate::core::keyboard;
3use crate::core::mouse;
4use crate::core::{Event, Point};
5use crate::simulator;
6
7use std::fmt;
8
9#[derive(Debug, Clone, PartialEq)]
13pub enum Instruction {
14 Interact(Interaction),
16 Expect(Expectation),
18}
19
20impl Instruction {
21 pub fn parse(line: &str) -> Result<Self, ParseError> {
23 parser::run(line)
24 }
25}
26
27impl fmt::Display for Instruction {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 match self {
30 Instruction::Interact(interaction) => interaction.fmt(f),
31 Instruction::Expect(expectation) => expectation.fmt(f),
32 }
33 }
34}
35
36#[derive(Debug, Clone, PartialEq)]
38pub enum Interaction {
39 Mouse(Mouse),
41 Keyboard(Keyboard),
43}
44
45impl Interaction {
46 pub fn from_event(event: &Event) -> Option<Self> {
50 Some(match event {
51 Event::Mouse(mouse) => Self::Mouse(match mouse {
52 mouse::Event::CursorMoved { position } => Mouse::Move(Target::Point(*position)),
53 mouse::Event::ButtonPressed(button) => Mouse::Press {
54 button: *button,
55 target: None,
56 },
57 mouse::Event::ButtonReleased(button) => Mouse::Release {
58 button: *button,
59 target: None,
60 },
61 _ => None?,
62 }),
63 Event::Keyboard(keyboard) => Self::Keyboard(match keyboard {
64 keyboard::Event::KeyPressed { key, text, .. } => match key {
65 keyboard::Key::Named(named) => Keyboard::Press(match named {
66 keyboard::key::Named::Enter => Key::Enter,
67 keyboard::key::Named::Escape => Key::Escape,
68 keyboard::key::Named::Tab => Key::Tab,
69 keyboard::key::Named::Backspace => Key::Backspace,
70 keyboard::key::Named::Space => Key::Space,
71 keyboard::key::Named::ArrowUp => Key::ArrowUp,
72 keyboard::key::Named::ArrowDown => Key::ArrowDown,
73 keyboard::key::Named::ArrowLeft => Key::ArrowLeft,
74 keyboard::key::Named::ArrowRight => Key::ArrowRight,
75 keyboard::key::Named::Home => Key::Home,
76 keyboard::key::Named::End => Key::End,
77 keyboard::key::Named::PageUp => Key::PageUp,
78 keyboard::key::Named::PageDown => Key::PageDown,
79 _ => None?,
80 }),
81 _ => Keyboard::Typewrite(text.as_ref()?.to_string()),
82 },
83 keyboard::Event::KeyReleased { key, .. } => match key {
84 keyboard::Key::Named(named) => Keyboard::Release(match named {
85 keyboard::key::Named::Enter => Key::Enter,
86 keyboard::key::Named::Escape => Key::Escape,
87 keyboard::key::Named::Tab => Key::Tab,
88 keyboard::key::Named::Backspace => Key::Backspace,
89 keyboard::key::Named::Space => Key::Space,
90 keyboard::key::Named::ArrowUp => Key::ArrowUp,
91 keyboard::key::Named::ArrowDown => Key::ArrowDown,
92 keyboard::key::Named::ArrowLeft => Key::ArrowLeft,
93 keyboard::key::Named::ArrowRight => Key::ArrowRight,
94 keyboard::key::Named::Home => Key::Home,
95 keyboard::key::Named::End => Key::End,
96 keyboard::key::Named::PageUp => Key::PageUp,
97 keyboard::key::Named::PageDown => Key::PageDown,
98 _ => None?,
99 }),
100 _ => None?,
101 },
102 keyboard::Event::ModifiersChanged(_) => None?,
103 }),
104 _ => None?,
105 })
106 }
107
108 pub fn merge(self, next: Self) -> (Self, Option<Self>) {
120 match (self, next) {
121 (Self::Mouse(current), Self::Mouse(next)) => match (current, next) {
122 (Mouse::Move(_), Mouse::Move(to)) => (Self::Mouse(Mouse::Move(to)), None),
123 (
124 Mouse::Move(to),
125 Mouse::Press {
126 button,
127 target: None,
128 },
129 ) => (
130 Self::Mouse(Mouse::Press {
131 button,
132 target: Some(to),
133 }),
134 None,
135 ),
136 (
137 Mouse::Move(to),
138 Mouse::Release {
139 button,
140 target: None,
141 },
142 ) => (
143 Self::Mouse(Mouse::Release {
144 button,
145 target: Some(to),
146 }),
147 None,
148 ),
149 (
150 Mouse::Press {
151 button: press,
152 target: press_at,
153 },
154 Mouse::Release {
155 button: release,
156 target: release_at,
157 },
158 ) if press == release
159 && release_at
160 .as_ref()
161 .is_none_or(|release_at| Some(release_at) == press_at.as_ref()) =>
162 {
163 (
164 Self::Mouse(Mouse::Click {
165 button: press,
166 target: press_at,
167 }),
168 None,
169 )
170 }
171 (
172 Mouse::Press {
173 button,
174 target: Some(press_at),
175 },
176 Mouse::Move(move_at),
177 ) if press_at == move_at => (
178 Self::Mouse(Mouse::Press {
179 button,
180 target: Some(press_at),
181 }),
182 None,
183 ),
184 (
185 Mouse::Click {
186 button,
187 target: Some(click_at),
188 },
189 Mouse::Move(move_at),
190 ) if click_at == move_at => (
191 Self::Mouse(Mouse::Click {
192 button,
193 target: Some(click_at),
194 }),
195 None,
196 ),
197 (current, next) => (Self::Mouse(current), Some(Self::Mouse(next))),
198 },
199 (Self::Keyboard(current), Self::Keyboard(next)) => match (current, next) {
200 (Keyboard::Typewrite(current), Keyboard::Typewrite(next)) => (
201 Self::Keyboard(Keyboard::Typewrite(format!("{current}{next}"))),
202 None,
203 ),
204 (Keyboard::Press(current), Keyboard::Release(next)) if current == next => {
205 (Self::Keyboard(Keyboard::Type(current)), None)
206 }
207 (current, next) => (Self::Keyboard(current), Some(Self::Keyboard(next))),
208 },
209 (current, next) => (current, Some(next)),
210 }
211 }
212
213 pub fn events(&self, find_target: impl FnOnce(&Target) -> Option<Point>) -> Option<Vec<Event>> {
218 let mouse_move_ = |to| Event::Mouse(mouse::Event::CursorMoved { position: to });
219
220 let mouse_press = |button| Event::Mouse(mouse::Event::ButtonPressed(button));
221
222 let mouse_release = |button| Event::Mouse(mouse::Event::ButtonReleased(button));
223
224 let key_press = |key| simulator::press_key(key, None);
225
226 let key_release = |key| simulator::release_key(key);
227
228 Some(match self {
229 Interaction::Mouse(mouse) => match mouse {
230 Mouse::Move(to) => vec![mouse_move_(find_target(to)?)],
231 Mouse::Press {
232 button,
233 target: Some(at),
234 } => vec![mouse_move_(find_target(at)?), mouse_press(*button)],
235 Mouse::Press {
236 button,
237 target: None,
238 } => {
239 vec![mouse_press(*button)]
240 }
241 Mouse::Release {
242 button,
243 target: Some(at),
244 } => {
245 vec![mouse_move_(find_target(at)?), mouse_release(*button)]
246 }
247 Mouse::Release {
248 button,
249 target: None,
250 } => {
251 vec![mouse_release(*button)]
252 }
253 Mouse::Click {
254 button,
255 target: Some(at),
256 } => {
257 vec![
258 mouse_move_(find_target(at)?),
259 mouse_press(*button),
260 mouse_release(*button),
261 ]
262 }
263 Mouse::Click {
264 button,
265 target: None,
266 } => {
267 vec![mouse_press(*button), mouse_release(*button)]
268 }
269 },
270 Interaction::Keyboard(keyboard) => match keyboard {
271 Keyboard::Press(key) => vec![key_press(*key)],
272 Keyboard::Release(key) => vec![key_release(*key)],
273 Keyboard::Type(key) => vec![key_press(*key), key_release(*key)],
274 Keyboard::Typewrite(text) => simulator::typewrite(text).collect(),
275 },
276 })
277 }
278}
279
280impl fmt::Display for Interaction {
281 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
282 match self {
283 Interaction::Mouse(mouse) => mouse.fmt(f),
284 Interaction::Keyboard(keyboard) => keyboard.fmt(f),
285 }
286 }
287}
288
289#[derive(Debug, Clone, PartialEq)]
291pub enum Mouse {
292 Move(Target),
294 Press {
296 button: mouse::Button,
298 target: Option<Target>,
300 },
301 Release {
303 button: mouse::Button,
305 target: Option<Target>,
307 },
308 Click {
310 button: mouse::Button,
312 target: Option<Target>,
314 },
315}
316
317impl fmt::Display for Mouse {
318 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
319 match self {
320 Mouse::Move(target) => {
321 write!(f, "move {}", target)
322 }
323 Mouse::Press { button, target } => {
324 write!(f, "press {}", format::button_at(*button, target.as_ref()))
325 }
326 Mouse::Release { button, target } => {
327 write!(f, "release {}", format::button_at(*button, target.as_ref()))
328 }
329 Mouse::Click { button, target } => {
330 write!(f, "click {}", format::button_at(*button, target.as_ref()))
331 }
332 }
333 }
334}
335
336#[derive(Debug, Clone, PartialEq)]
338pub enum Target {
339 Id(String),
341 Text(String),
343 Point(Point),
345}
346
347impl fmt::Display for Target {
348 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
349 match self {
350 Self::Id(id) => f.write_str(&format::id(id)),
351 Self::Point(point) => f.write_str(&format::point(*point)),
352 Self::Text(text) => f.write_str(&format::string(text)),
353 }
354 }
355}
356
357#[derive(Debug, Clone, PartialEq)]
359pub enum Keyboard {
360 Press(Key),
362 Release(Key),
364 Type(Key),
366 Typewrite(String),
368}
369
370impl fmt::Display for Keyboard {
371 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
372 match self {
373 Keyboard::Press(key) => {
374 write!(f, "press {}", format::key(*key))
375 }
376 Keyboard::Release(key) => {
377 write!(f, "release {}", format::key(*key))
378 }
379 Keyboard::Type(key) => {
380 write!(f, "type {}", format::key(*key))
381 }
382 Keyboard::Typewrite(text) => {
383 write!(f, "type \"{text}\"")
384 }
385 }
386 }
387}
388
389#[derive(Debug, Clone, Copy, PartialEq, Eq)]
391#[allow(missing_docs)]
392pub enum Key {
393 Enter,
394 Escape,
395 Tab,
396 Backspace,
397 Space,
398 ArrowUp,
399 ArrowDown,
400 ArrowLeft,
401 ArrowRight,
402 Home,
403 End,
404 PageUp,
405 PageDown,
406}
407
408impl From<Key> for keyboard::Key {
409 fn from(key: Key) -> Self {
410 match key {
411 Key::Enter => Self::Named(keyboard::key::Named::Enter),
412 Key::Escape => Self::Named(keyboard::key::Named::Escape),
413 Key::Tab => Self::Named(keyboard::key::Named::Tab),
414 Key::Backspace => Self::Named(keyboard::key::Named::Backspace),
415 Key::Space => Self::Named(keyboard::key::Named::Space),
416 Key::ArrowUp => Self::Named(keyboard::key::Named::ArrowUp),
417 Key::ArrowDown => Self::Named(keyboard::key::Named::ArrowDown),
418 Key::ArrowLeft => Self::Named(keyboard::key::Named::ArrowLeft),
419 Key::ArrowRight => Self::Named(keyboard::key::Named::ArrowRight),
420 Key::Home => Self::Named(keyboard::key::Named::Home),
421 Key::End => Self::Named(keyboard::key::Named::End),
422 Key::PageUp => Self::Named(keyboard::key::Named::PageUp),
423 Key::PageDown => Self::Named(keyboard::key::Named::PageDown),
424 }
425 }
426}
427
428mod format {
429 use super::*;
430
431 pub fn button_at(button: mouse::Button, at: Option<&Target>) -> String {
432 let button = self::button(button);
433
434 if let Some(at) = at {
435 if button.is_empty() {
436 at.to_string()
437 } else {
438 format!("{} {}", button, at)
439 }
440 } else {
441 button.to_owned()
442 }
443 }
444
445 pub fn button(button: mouse::Button) -> &'static str {
446 match button {
447 mouse::Button::Left => "",
448 mouse::Button::Right => "right",
449 mouse::Button::Middle => "middle",
450 mouse::Button::Back => "back",
451 mouse::Button::Forward => "forward",
452 mouse::Button::Other(_) => "other",
453 }
454 }
455
456 pub fn point(point: Point) -> String {
457 format!("({:.2}, {:.2})", point.x, point.y)
458 }
459
460 pub fn key(key: Key) -> &'static str {
461 match key {
462 Key::Enter => "enter",
463 Key::Escape => "escape",
464 Key::Tab => "tab",
465 Key::Backspace => "backspace",
466 Key::Space => "space",
467 Key::ArrowUp => "up",
468 Key::ArrowDown => "down",
469 Key::ArrowLeft => "left",
470 Key::ArrowRight => "right",
471 Key::Home => "home",
472 Key::End => "end",
473 Key::PageUp => "pageup",
474 Key::PageDown => "pagedown",
475 }
476 }
477
478 pub fn string(text: &str) -> String {
479 format!("\"{}\"", text.escape_default())
480 }
481
482 pub fn id(id: &str) -> String {
483 format!("#{id}")
484 }
485}
486
487#[derive(Debug, Clone, PartialEq)]
492pub enum Expectation {
493 Text(String),
495 Focused(Target),
497}
498
499impl fmt::Display for Expectation {
500 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
501 match self {
502 Expectation::Text(text) => {
503 write!(f, "expect {}", format::string(text))
504 }
505 Expectation::Focused(target) => {
506 write!(f, "expect focused {target}")
507 }
508 }
509 }
510}
511
512pub use parser::Error as ParseError;
513
514mod parser {
515 use super::*;
516
517 use nom::branch::alt;
518 use nom::bytes::complete::tag;
519 use nom::bytes::{is_not, take_while_m_n};
520 use nom::character::complete::{alphanumeric1, char, multispace0, multispace1};
521 use nom::combinator::{map, map_opt, map_res, opt, recognize, success, value, verify};
522 use nom::error::ParseError;
523 use nom::multi::{fold, many1_count};
524 use nom::number::float;
525 use nom::sequence::{delimited, preceded, separated_pair};
526 use nom::{Finish, IResult, Parser};
527
528 #[derive(Debug, Clone, thiserror::Error)]
530 #[error("parse error: {0}")]
531 pub struct Error(nom::error::Error<String>);
532
533 pub fn run(input: &str) -> Result<Instruction, Error> {
534 match instruction.parse_complete(input).finish() {
535 Ok((_rest, instruction)) => Ok(instruction),
536 Err(error) => Err(Error(error.cloned())),
537 }
538 }
539
540 fn instruction(input: &str) -> IResult<&str, Instruction> {
541 alt((
542 map(interaction, Instruction::Interact),
543 map(expectation, Instruction::Expect),
544 ))
545 .parse(input)
546 }
547
548 fn interaction(input: &str) -> IResult<&str, Interaction> {
549 alt((
550 map(mouse, Interaction::Mouse),
551 map(keyboard, Interaction::Keyboard),
552 ))
553 .parse(input)
554 }
555
556 fn mouse(input: &str) -> IResult<&str, Mouse> {
557 let mouse_move = preceded(tag("move "), target).map(Mouse::Move);
558
559 alt((mouse_move, mouse_click, mouse_press, mouse_release)).parse(input)
560 }
561
562 fn mouse_click(input: &str) -> IResult<&str, Mouse> {
563 let (input, _) = tag("click ")(input)?;
564 let (input, (button, target)) = mouse_button_at(input)?;
565
566 Ok((input, Mouse::Click { button, target }))
567 }
568
569 fn mouse_press(input: &str) -> IResult<&str, Mouse> {
570 let (input, _) = tag("press ")(input)?;
571 let (input, (button, target)) = mouse_button_at(input)?;
572
573 Ok((input, Mouse::Press { button, target }))
574 }
575
576 fn mouse_release(input: &str) -> IResult<&str, Mouse> {
577 let (input, _) = tag("release ")(input)?;
578 let (input, (button, target)) = mouse_button_at(input)?;
579
580 Ok((input, Mouse::Release { button, target }))
581 }
582
583 fn mouse_button_at(input: &str) -> IResult<&str, (mouse::Button, Option<Target>)> {
584 let (input, button) = mouse_button(input)?;
585 let (input, at) = opt(target).parse(input)?;
586
587 Ok((input, (button, at)))
588 }
589
590 fn target(input: &str) -> IResult<&str, Target> {
591 alt((
592 id.map(String::from).map(Target::Id),
593 string.map(Target::Text),
594 point.map(Target::Point),
595 ))
596 .parse(input)
597 }
598
599 fn mouse_button(input: &str) -> IResult<&str, mouse::Button> {
600 alt((
601 tag("right").map(|_| mouse::Button::Right),
602 success(mouse::Button::Left),
603 ))
604 .parse(input)
605 }
606
607 fn keyboard(input: &str) -> IResult<&str, Keyboard> {
608 alt((
609 map(preceded(tag("type "), string), Keyboard::Typewrite),
610 map(preceded(tag("type "), key), Keyboard::Type),
611 ))
612 .parse(input)
613 }
614
615 fn expectation(input: &str) -> IResult<&str, Expectation> {
616 alt((
617 map(
618 preceded(tag("expect focused "), target),
619 Expectation::Focused,
620 ),
621 map(preceded(tag("expect "), string), Expectation::Text),
622 ))
623 .parse(input)
624 }
625
626 fn key(input: &str) -> IResult<&str, Key> {
627 alt((
628 map(tag("enter"), |_| Key::Enter),
629 map(tag("escape"), |_| Key::Escape),
630 map(tag("tab"), |_| Key::Tab),
631 map(tag("backspace"), |_| Key::Backspace),
632 map(tag("space"), |_| Key::Space),
633 map(tag("up"), |_| Key::ArrowUp),
634 map(tag("down"), |_| Key::ArrowDown),
635 map(tag("left"), |_| Key::ArrowLeft),
636 map(tag("right"), |_| Key::ArrowRight),
637 map(tag("home"), |_| Key::Home),
638 map(tag("end"), |_| Key::End),
639 map(tag("pageup"), |_| Key::PageUp),
640 map(tag("pagedown"), |_| Key::PageDown),
641 ))
642 .parse(input)
643 }
644
645 fn id(input: &str) -> IResult<&str, &str> {
646 preceded(
647 char('#'),
648 recognize(many1_count(alt((alphanumeric1, tag("_"), tag("-"))))),
649 )
650 .parse(input)
651 }
652
653 fn point(input: &str) -> IResult<&str, Point> {
654 let comma = whitespace(char(','));
655
656 map(
657 delimited(
658 char('('),
659 separated_pair(float(), comma, float()),
660 char(')'),
661 ),
662 |(x, y)| Point { x, y },
663 )
664 .parse(input)
665 }
666
667 pub fn whitespace<'a, O, E: ParseError<&'a str>, F>(
668 inner: F,
669 ) -> impl Parser<&'a str, Output = O, Error = E>
670 where
671 F: Parser<&'a str, Output = O, Error = E>,
672 {
673 delimited(multispace0, inner, multispace0)
674 }
675
676 fn string(input: &str) -> IResult<&str, String> {
699 #[derive(Debug, Clone, Copy)]
700 enum Fragment<'a> {
701 Literal(&'a str),
702 EscapedChar(char),
703 EscapedWS,
704 }
705
706 fn fragment(input: &str) -> IResult<&str, Fragment<'_>> {
707 alt((
708 map(string_literal, Fragment::Literal),
709 map(escaped_char, Fragment::EscapedChar),
710 value(Fragment::EscapedWS, escaped_whitespace),
711 ))
712 .parse(input)
713 }
714
715 fn string_literal<'a, E: ParseError<&'a str>>(
716 input: &'a str,
717 ) -> IResult<&'a str, &'a str, E> {
718 let not_quote_slash = is_not("\"\\");
719
720 verify(not_quote_slash, |s: &str| !s.is_empty()).parse(input)
721 }
722
723 fn unicode(input: &str) -> IResult<&str, char> {
724 let parse_hex = take_while_m_n(1, 6, |c: char| c.is_ascii_hexdigit());
725
726 let parse_delimited_hex =
727 preceded(char('u'), delimited(char('{'), parse_hex, char('}')));
728
729 let parse_u32 = map_res(parse_delimited_hex, move |hex| u32::from_str_radix(hex, 16));
730
731 map_opt(parse_u32, std::char::from_u32).parse(input)
732 }
733
734 fn escaped_char(input: &str) -> IResult<&str, char> {
735 preceded(
736 char('\\'),
737 alt((
738 unicode,
739 value('\n', char('n')),
740 value('\r', char('r')),
741 value('\t', char('t')),
742 value('\u{08}', char('b')),
743 value('\u{0C}', char('f')),
744 value('\\', char('\\')),
745 value('/', char('/')),
746 value('"', char('"')),
747 )),
748 )
749 .parse(input)
750 }
751
752 fn escaped_whitespace(input: &str) -> IResult<&str, &str> {
753 preceded(char('\\'), multispace1).parse(input)
754 }
755
756 let build_string = fold(0.., fragment, String::new, |mut string, fragment| {
757 match fragment {
758 Fragment::Literal(s) => string.push_str(s),
759 Fragment::EscapedChar(c) => string.push(c),
760 Fragment::EscapedWS => {}
761 }
762 string
763 });
764
765 delimited(char('"'), build_string, char('"')).parse(input)
766 }
767}