git_record/
cursive_utils.rs

1//! TODO: extract into own crate
2
3use std::panic::{self, AssertUnwindSafe, RefUnwindSafe, UnwindSafe};
4use std::sync::mpsc::{channel, Receiver, Sender, TryRecvError};
5
6use cursive::event::Event;
7use cursive::{CursiveRunnable, CursiveRunner};
8
9pub trait EventDrivenCursiveApp
10where
11    Self: UnwindSafe,
12{
13    type Message: Clone + std::fmt::Debug + UnwindSafe + 'static;
14    type Return;
15
16    fn get_init_message(&self) -> Self::Message;
17    fn get_key_bindings(&self) -> Vec<(Event, Self::Message)>;
18    fn handle_message(
19        &mut self,
20        siv: &mut CursiveRunner<CursiveRunnable>,
21        main_tx: Sender<Self::Message>,
22        message: Self::Message,
23    );
24    fn finish(self) -> Self::Return;
25}
26
27pub trait EventDrivenCursiveAppExt: EventDrivenCursiveApp {
28    fn run(self, siv: CursiveRunner<CursiveRunnable>) -> Self::Return;
29}
30
31impl<T: EventDrivenCursiveApp + UnwindSafe + RefUnwindSafe> EventDrivenCursiveAppExt for T {
32    fn run(mut self, mut siv: CursiveRunner<CursiveRunnable>) -> T::Return {
33        let (main_tx, main_rx): (Sender<T::Message>, Receiver<T::Message>) = channel();
34
35        self.get_key_bindings().iter().cloned().for_each(
36            |(event, message): (cursive::event::Event, T::Message)| {
37                siv.add_global_callback(event, {
38                    let main_tx = main_tx.clone();
39                    move |_siv| main_tx.send(message.clone()).unwrap()
40                });
41            },
42        );
43
44        main_tx.send(self.get_init_message()).unwrap();
45        while siv.is_running() {
46            let message = main_rx.try_recv();
47            if message.is_err() {
48                // For tests: only pump the Cursive event loop if we have no events
49                // of our own to process. Otherwise, the event loop queues up all of
50                // the messages before we can process them, which means that none of
51                // the screenshots are correct.
52                siv.step();
53            }
54
55            match message {
56                Err(TryRecvError::Disconnected) => break,
57
58                Err(TryRecvError::Empty) => {
59                    // If we haven't received a message yet, defer to `siv.step`
60                    // to process the next user input.
61                    continue;
62                }
63
64                Ok(message) => {
65                    let maybe_panic = panic::catch_unwind({
66                        let mut siv = AssertUnwindSafe(&mut siv);
67                        let mut self_ = AssertUnwindSafe(&mut self);
68                        let main_tx = AssertUnwindSafe(main_tx.clone());
69                        move || {
70                            self_.handle_message(*siv, main_tx.clone(), message);
71                        }
72                    });
73                    match maybe_panic {
74                        Ok(()) => {
75                            siv.refresh();
76                        }
77                        Err(panic) => {
78                            // Ensure we exit TUI mode before attempting to print panic details.
79                            drop(siv);
80                            if let Some(payload) = panic.downcast_ref::<String>() {
81                                panic!("panic occurred: {}", payload);
82                            } else if let Some(payload) = panic.downcast_ref::<&str>() {
83                                panic!("panic occurred: {}", payload);
84                            } else {
85                                panic!("panic occurred (message not available)",);
86                            }
87                        }
88                    }
89                }
90            };
91        }
92
93        self.finish()
94    }
95}
96
97/// Testing helpers for interactive interfaces.
98pub mod testing {
99    use std::borrow::Borrow;
100    use std::cell::RefCell;
101    use std::rc::Rc;
102
103    use cursive::backend::Backend;
104    use cursive::theme::Color;
105
106    /// Represents a "screenshot" of the terminal taken at a point in time.
107    pub type Screen = Vec<Vec<char>>;
108
109    /// The kind of events that can be
110    #[derive(Clone, Debug)]
111    pub enum CursiveTestingEvent {
112        /// A regular Cursive event.
113        Event(cursive::event::Event),
114
115        /// Take a screenshot at the current point in time and store it in the
116        /// provided screenshot cell.
117        TakeScreenshot(Rc<RefCell<Screen>>),
118    }
119
120    /// The testing backend. It feeds a predetermined list of events to the
121    /// Cursive event loop and stores a virtual terminal for Cursive to draw on.
122    #[derive(Debug)]
123    pub struct CursiveTestingBackend {
124        events: Vec<CursiveTestingEvent>,
125        event_index: usize,
126        just_emitted_event: bool,
127        screen: RefCell<Screen>,
128    }
129
130    impl CursiveTestingBackend {
131        /// Construct the testing backend with the provided set of events.
132        pub fn init(events: Vec<CursiveTestingEvent>) -> Box<dyn Backend> {
133            Box::new(CursiveTestingBackend {
134                events,
135                event_index: 0,
136                just_emitted_event: false,
137                screen: RefCell::new(vec![vec![' '; 120]; 24]),
138            })
139        }
140    }
141
142    impl Backend for CursiveTestingBackend {
143        fn poll_event(&mut self) -> Option<cursive::event::Event> {
144            // Cursive will poll all available events. We only want it to
145            // process events one at a time, so return `None` after each event.
146            if self.just_emitted_event {
147                self.just_emitted_event = false;
148                return None;
149            }
150
151            let event_index = self.event_index;
152            self.event_index += 1;
153            match self.events.get(event_index)?.to_owned() {
154                CursiveTestingEvent::TakeScreenshot(screen_target) => {
155                    let mut screen_target = (*screen_target).borrow_mut();
156                    *screen_target = self.screen.borrow().clone();
157                    self.poll_event()
158                }
159                CursiveTestingEvent::Event(event) => {
160                    self.just_emitted_event = true;
161                    Some(event)
162                }
163            }
164        }
165
166        fn refresh(&mut self) {}
167
168        fn has_colors(&self) -> bool {
169            false
170        }
171
172        fn screen_size(&self) -> cursive::Vec2 {
173            let screen = self.screen.borrow();
174            (screen[0].len(), screen.len()).into()
175        }
176
177        fn print_at(&self, pos: cursive::Vec2, text: &str) {
178            for (i, c) in text.chars().enumerate() {
179                let mut screen = self.screen.borrow_mut();
180                let screen_width = screen[0].len();
181                if pos.x + i < screen_width {
182                    screen[pos.y][pos.x + i] = c;
183                } else {
184                    // Indicate that the screen was overfull.
185                    screen[pos.y][screen_width - 1] = '$';
186                }
187            }
188        }
189
190        fn clear(&self, _color: Color) {
191            let mut screen = self.screen.borrow_mut();
192            for i in 0..screen.len() {
193                for j in 0..screen[i].len() {
194                    screen[i][j] = ' ';
195                }
196            }
197        }
198
199        fn set_color(&self, colors: cursive::theme::ColorPair) -> cursive::theme::ColorPair {
200            colors
201        }
202
203        fn set_effect(&self, _effect: cursive::theme::Effect) {}
204
205        fn unset_effect(&self, _effect: cursive::theme::Effect) {}
206
207        fn set_title(&mut self, _title: String) {}
208    }
209
210    /// Convert the screenshot into a string for assertions, such as for use
211    /// with `insta::assert_snapshot!`.
212    pub fn screen_to_string(screen: &Rc<RefCell<Screen>>) -> String {
213        let screen = Rc::borrow(screen);
214        let screen = RefCell::borrow(screen);
215        screen
216            .iter()
217            .map(|row| {
218                let line: String = row.iter().collect();
219                line.trim_end().to_owned() + "\n"
220            })
221            .collect::<String>()
222            .trim()
223            .to_owned()
224    }
225}