afrim/
lib.rs

1mod convert;
2pub mod frontend;
3
4pub use afrim_config::Config;
5use afrim_preprocessor::{utils, Command as EventCmd, Preprocessor};
6use afrim_translator::Translator;
7use anyhow::{Context, Result};
8use enigo::{Enigo, Key, KeyboardControllable};
9use frontend::{Command as GUICmd, Frontend};
10use rdev::{self, EventType, Key as E_Key};
11use std::{rc::Rc, sync::mpsc, thread};
12
13/// Starts the afrim.
14pub fn run(
15    config: Config,
16    mut frontend: impl Frontend + std::marker::Send + 'static,
17) -> Result<()> {
18    // State.
19    let mut is_ctrl_released = true;
20    let mut idle = false;
21
22    // Configuration of the afrim.
23    let memory = utils::build_map(
24        config
25            .extract_data()
26            .iter()
27            .map(|(key, value)| vec![key.as_str(), value.as_str()])
28            .collect(),
29    );
30    let (buffer_size, auto_commit, page_size) = config
31        .core
32        .as_ref()
33        .map(|core| {
34            (
35                core.buffer_size.unwrap_or(32),
36                core.auto_commit.unwrap_or(false),
37                core.page_size.unwrap_or(10),
38            )
39        })
40        .unwrap_or((32, false, 10));
41    let mut keyboard = Enigo::new();
42    let mut preprocessor = Preprocessor::new(Rc::new(memory), buffer_size);
43    #[cfg(not(feature = "rhai"))]
44    let translator = Translator::new(config.extract_translation(), auto_commit);
45    #[cfg(feature = "rhai")]
46    let mut translator = Translator::new(config.extract_translation(), auto_commit);
47    #[cfg(feature = "rhai")]
48    config
49        .extract_translators()
50        .context("Failed to load translators.")?
51        .into_iter()
52        .for_each(|(name, ast)| translator.register(name, ast));
53
54    // Configuration of the frontend.
55    let (frontend_tx1, frontend_rx1) = mpsc::channel();
56    let (frontend_tx2, frontend_rx2) = mpsc::channel();
57
58    frontend_tx1.send(GUICmd::PageSize(page_size))?;
59    let screen_size = rdev::display_size().unwrap();
60    frontend_tx1.send(GUICmd::ScreenSize(screen_size))?;
61
62    let frontend_thread = thread::spawn(move || {
63        frontend
64            .init(frontend_tx2, frontend_rx1)
65            .context("Failure to initialize the frontend.")
66            .unwrap();
67        frontend
68            .listen()
69            .context("The frontend raise an unexpected error.")
70            .unwrap();
71    });
72
73    // Configuration of the event listener.
74    let (event_tx, event_rx) = mpsc::channel();
75    thread::spawn(move || {
76        rdev::listen(move |event| {
77            event_tx
78                .send(event)
79                .unwrap_or_else(|e| eprintln!("Could not send event {:?}", e));
80        })
81        .expect("Could not listen");
82    });
83
84    // We process event.
85    for event in event_rx.iter() {
86        match event.event_type {
87            // Handling of idle state.
88            EventType::KeyPress(E_Key::Pause) => {
89                idle = true;
90                frontend_tx1.send(GUICmd::State(idle))?;
91            }
92            EventType::KeyRelease(E_Key::Pause) => {
93                idle = false;
94                frontend_tx1.send(GUICmd::State(idle))?;
95            }
96            EventType::KeyPress(E_Key::ControlLeft | E_Key::ControlRight) => {
97                is_ctrl_released = false;
98            }
99            EventType::KeyRelease(E_Key::ControlLeft | E_Key::ControlRight) if is_ctrl_released => {
100                idle = !idle;
101                frontend_tx1.send(GUICmd::State(idle))?;
102            }
103            EventType::KeyRelease(E_Key::ControlLeft | E_Key::ControlRight) => {
104                is_ctrl_released = true;
105            }
106            _ if idle => (),
107            // Handling of special functions.
108            EventType::KeyRelease(E_Key::ShiftRight) if !is_ctrl_released => {
109                frontend_tx1.send(GUICmd::SelectNextPredicate)?;
110            }
111            EventType::KeyRelease(E_Key::ShiftLeft) if !is_ctrl_released => {
112                frontend_tx1.send(GUICmd::SelectPreviousPredicate)?;
113            }
114            EventType::KeyRelease(E_Key::Space) if !is_ctrl_released => {
115                rdev::simulate(&EventType::KeyRelease(E_Key::ControlLeft))
116                    .expect("We couldn't cancel the special function key");
117
118                frontend_tx1.send(GUICmd::SelectedPredicate)?;
119                if let GUICmd::Predicate(predicate) = frontend_rx2.recv()? {
120                    preprocessor.commit(
121                        predicate
122                            .texts
123                            .first()
124                            .unwrap_or(&String::default())
125                            .to_owned(),
126                    );
127                    frontend_tx1.send(GUICmd::Clear)?;
128                }
129            }
130            _ if !is_ctrl_released => (),
131            // GUI events.
132            EventType::MouseMove { x, y } => {
133                frontend_tx1.send(GUICmd::Position((x, y)))?;
134            }
135            // Process events.
136            _ => {
137                let (changed, _committed) = preprocessor.process(convert::from_event(event));
138
139                if changed {
140                    let input = preprocessor.get_input();
141
142                    frontend_tx1.send(GUICmd::Clear)?;
143
144                    translator
145                        .translate(&input)
146                        .into_iter()
147                        .take(page_size * 2)
148                        .try_for_each(|predicate| -> Result<()> {
149                            if predicate.texts.is_empty() {
150                            } else if auto_commit && predicate.can_commit {
151                                preprocessor.commit(predicate.texts[0].to_owned());
152                            } else {
153                                frontend_tx1.send(GUICmd::Predicate(predicate))?;
154                            }
155
156                            Ok(())
157                        })?;
158
159                    frontend_tx1.send(GUICmd::InputText(input))?;
160                    frontend_tx1.send(GUICmd::Update)?;
161                }
162            }
163        }
164
165        // Process preprocessor instructions
166        while let Some(command) = preprocessor.pop_queue() {
167            match command {
168                EventCmd::CommitText(text) => {
169                    keyboard.key_sequence(&text);
170                }
171                EventCmd::CleanDelete => {
172                    keyboard.key_up(Key::Backspace);
173                }
174                EventCmd::Delete => {
175                    keyboard.key_click(Key::Backspace);
176                }
177                EventCmd::Pause => {
178                    rdev::simulate(&EventType::KeyPress(E_Key::Pause)).unwrap();
179                }
180                EventCmd::Resume => {
181                    rdev::simulate(&EventType::KeyRelease(E_Key::Pause)).unwrap();
182                }
183            };
184        }
185
186        // Consult the frontend to know if there have some requests.
187        frontend_tx1.send(GUICmd::NOP)?;
188        match frontend_rx2.recv()? {
189            GUICmd::End => break,
190            GUICmd::State(state) => {
191                idle = state;
192                frontend_tx1.send(GUICmd::State(idle))?;
193            }
194            _ => (),
195        }
196    }
197
198    // Wait the frontend to end properly.
199    frontend_thread.join().unwrap();
200
201    Ok(())
202}
203
204#[cfg(test)]
205mod tests {
206    use crate::{frontend::Console, run, Config};
207    use rdev::{self, Button, EventType::*, Key::*};
208    use rstk::{self, TkPackLayout};
209    use std::{thread, time::Duration};
210
211    macro_rules! input {
212        ( $( $key:expr )*, $delay:expr ) => (
213            $(
214                thread::sleep($delay);
215                rdev::simulate(&KeyPress($key)).unwrap();
216                rdev::simulate(&KeyRelease($key)).unwrap();
217            )*
218        );
219    }
220
221    macro_rules! output {
222        ( $textfield: expr, $expected: expr ) => {
223            thread::sleep(Duration::from_millis(500));
224
225            // A loop to be sure to got something stable
226            loop {
227                let a = $textfield.get_to_end((1, 0));
228                let b = $textfield.get_to_end((1, 0));
229
230                if (a == b) {
231                    let content = a.chars().filter(|c| *c != '\0').collect::<String>();
232                    let content = content.trim();
233
234                    assert_eq!(content, $expected);
235                    break;
236                }
237            }
238        };
239    }
240
241    fn start_sandbox(start_point: &str) -> rstk::TkText {
242        let root = rstk::trace_with("wish").unwrap();
243        root.title("Afrim Test Environment");
244
245        let input_field = rstk::make_text(&root);
246        input_field.width(50);
247        input_field.height(12);
248        input_field.pack().layout();
249        root.geometry(200, 200, 0, 0);
250        input_field.insert((1, 1), start_point);
251        rstk::tell_wish(
252            r#"
253            chan configure stdout -encoding utf-8;
254            wm protocol . WM_DELETE_WINDOW {destroy .};
255        "#,
256        );
257        thread::sleep(Duration::from_secs(1));
258        input_field
259    }
260
261    fn end_sandbox() {
262        rstk::end_wish();
263    }
264
265    fn start_simulation() {
266        let typing_speed_ms = Duration::from_millis(500);
267
268        // To detect excessive backspace
269        const LIMIT: &str = "bbb";
270
271        // Start the sandbox
272        let textfield = start_sandbox(LIMIT);
273
274        rdev::simulate(&MouseMove { x: 100.0, y: 100.0 }).unwrap();
275        thread::sleep(typing_speed_ms);
276        rdev::simulate(&ButtonPress(Button::Left)).unwrap();
277        thread::sleep(typing_speed_ms);
278        rdev::simulate(&ButtonRelease(Button::Left)).unwrap();
279        thread::sleep(typing_speed_ms);
280
281        input!(KeyU, typing_speed_ms);
282        #[cfg(not(feature = "inhibit"))]
283        input!(Backspace, typing_speed_ms);
284        input!(KeyU KeyU Backspace KeyU, typing_speed_ms);
285        input!(
286            KeyC Num8 KeyC KeyE KeyD
287            KeyU KeyU
288            KeyA KeyF Num3, typing_speed_ms);
289        input!(
290            KeyA KeyF KeyA KeyF
291            KeyA KeyF KeyF Num3, typing_speed_ms);
292        input!(KeyU KeyU Num3, typing_speed_ms);
293        #[cfg(feature = "inhibit")]
294        output!(textfield, format!("{LIMIT}çʉ̄ɑ̄ɑɑɑ̄ɑ̄ʉ̄"));
295        #[cfg(not(feature = "inhibit"))]
296        output!(textfield, format!("{LIMIT}uçʉ̄ɑ̄ɑɑɑ̄ɑ̄ʉ̄"));
297
298        // We verify that the undo (backspace) works as expected
299        #[cfg(not(feature = "inhibit"))]
300        (0..12).for_each(|_| {
301            input!(Backspace, typing_speed_ms);
302        });
303        #[cfg(feature = "inhibit")]
304        (0..13).for_each(|_| {
305            input!(Backspace, typing_speed_ms);
306        });
307        output!(textfield, LIMIT);
308
309        // We verify that the pause/resume works as expected
310        rdev::simulate(&KeyPress(ControlLeft)).unwrap();
311        rdev::simulate(&KeyPress(ControlRight)).unwrap();
312        rdev::simulate(&KeyRelease(ControlRight)).unwrap();
313        rdev::simulate(&KeyRelease(ControlLeft)).unwrap();
314        input!(KeyU KeyU, typing_speed_ms);
315
316        rdev::simulate(&KeyPress(ControlLeft)).unwrap();
317        rdev::simulate(&KeyPress(ControlRight)).unwrap();
318        rdev::simulate(&KeyRelease(ControlRight)).unwrap();
319        rdev::simulate(&KeyRelease(ControlLeft)).unwrap();
320        input!(KeyA KeyF, typing_speed_ms);
321        output!(textfield, format!("{LIMIT}uuɑ"));
322        input!(Escape, typing_speed_ms);
323
324        // We verify the auto capitalization works as expected
325        input!(CapsLock KeyA CapsLock KeyF, typing_speed_ms);
326        input!(CapsLock KeyA CapsLock KeyF KeyF, typing_speed_ms);
327        input!(KeyA KeyF KeyF, typing_speed_ms);
328        output!(textfield, format!("{LIMIT}uuɑαⱭⱭɑɑ"));
329        input!(Escape, typing_speed_ms);
330
331        // We verify that the translation work as expected
332        input!(KeyH KeyE KeyL KeyL KeyO, typing_speed_ms);
333        output!(textfield, format!("{LIMIT}uuɑαⱭⱭɑɑhi"));
334        #[cfg(not(feature = "rhai"))]
335        input!(Escape KeyH Escape KeyE KeyL KeyL KeyO, typing_speed_ms);
336        #[cfg(feature = "rhai")]
337        input!(Escape KeyH KeyI, typing_speed_ms);
338        output!(textfield, format!("{LIMIT}uuɑαⱭⱭɑɑhihello"));
339        input!(Escape, typing_speed_ms);
340
341        // We verify that the predicate selection work as expected
342        input!(KeyH KeyE, typing_speed_ms);
343        rdev::simulate(&KeyPress(ControlLeft)).unwrap();
344        input!(ShiftLeft, typing_speed_ms);
345        input!(ShiftRight, typing_speed_ms);
346        rdev::simulate(&KeyRelease(ControlLeft)).unwrap();
347
348        input!(KeyA, typing_speed_ms);
349        rdev::simulate(&KeyPress(ControlLeft)).unwrap();
350        input!(Space, typing_speed_ms);
351        rdev::simulate(&KeyRelease(ControlLeft)).unwrap();
352        output!(textfield, format!("{LIMIT}uuɑαⱭⱭɑɑhihellohealth"));
353        input!(Escape, typing_speed_ms);
354
355        // We verify that we don't have a conflict
356        // between the translator and the processor
357        input!(KeyV KeyU KeyU KeyE, typing_speed_ms);
358        output!(textfield, format!("{LIMIT}uuɑαⱭⱭɑɑhihellohealthvʉe"));
359
360        // Test the idle state from the frontend.
361        input!(Escape Num8 KeyS KeyT KeyQ KeyT KeyE Num8, typing_speed_ms);
362        input!(Escape, typing_speed_ms);
363        rdev::simulate(&KeyPress(ShiftLeft)).unwrap();
364        input!(Minus, typing_speed_ms);
365        rdev::simulate(&KeyRelease(ShiftLeft)).unwrap();
366        input!(KeyS KeyT KeyA KeyT KeyE, typing_speed_ms);
367        rdev::simulate(&KeyPress(ShiftLeft)).unwrap();
368        input!(Minus, typing_speed_ms);
369        rdev::simulate(&KeyRelease(ShiftLeft)).unwrap();
370
371        // End the test
372        input!(Escape Num8 KeyE KeyX KeyI KeyT Num8, typing_speed_ms);
373        input!(Escape, typing_speed_ms);
374        rdev::simulate(&KeyPress(ShiftLeft)).unwrap();
375        input!(Minus, typing_speed_ms);
376        rdev::simulate(&KeyRelease(ShiftLeft)).unwrap();
377        input!(KeyE KeyX KeyI KeyT, typing_speed_ms);
378        rdev::simulate(&KeyPress(ShiftLeft)).unwrap();
379        input!(Minus, typing_speed_ms);
380        rdev::simulate(&KeyRelease(ShiftLeft)).unwrap();
381
382        end_sandbox();
383    }
384
385    #[test]
386    fn test_afrim() {
387        use std::path::Path;
388
389        let simulation_thread = thread::spawn(start_simulation);
390
391        let test_config = Config::from_file(Path::new("./data/test.toml")).unwrap();
392        assert!(run(test_config, Console::default()).is_ok());
393
394        // Wait the simulation to end properly.
395        simulation_thread.join().unwrap();
396    }
397}