mfform_lib/
app.rs

1use std::{
2    io::{self, Stdout},
3    sync::{Arc, RwLock},
4};
5
6use crossterm::{
7    event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
8    terminal::{self, disable_raw_mode, enable_raw_mode},
9    ExecutableCommand, QueueableCommand,
10};
11use log::debug;
12use log4rs::Handle;
13
14use crate::form::Form;
15
16/// Result of a Form execute
17#[derive(Debug, Copy, Clone, PartialEq)]
18pub enum EventResult {
19    /// User submitted form, data is in the form fields
20    Submit,
21    /// User aborted form, no garantee about contents of fields
22    Abort,
23    /// User requested a toggle of debug output
24    ToggleDebug,
25    /// No result yet, keep processing events
26    None,
27}
28
29#[derive(Debug, Copy, Clone, PartialEq)]
30pub enum EventHandlerResult {
31    Handled(EventResult),
32    NotHandled,
33}
34
35/// A base App object which 'owns' the terminal, manages logging etc.
36pub struct App {
37    stdout: Stdout,
38    _logger_handle: log4rs::Handle,
39    logging_enabled: Arc<RwLock<bool>>,
40}
41
42impl App {
43    /// Constructor for the App struct which takes in the output where to write
44    /// the output of the program
45    ///```
46    ///let app = with_writer().init()?;
47    ///```
48    pub fn with_writer(stdout: Stdout) -> Self {
49        let logging_enabled = Arc::new(RwLock::new(false));
50
51        let logger_handle = Self::configure_logging(logging_enabled.clone());
52
53        Self {
54            stdout,
55            _logger_handle: logger_handle,
56            logging_enabled,
57        }
58    }
59
60    /// Initialize the terminal, place it into 'cooked' mode and install panic handler.
61    ///```
62    ///let app = with_writer().init()?;
63    ///```
64    pub fn init(&mut self) -> io::Result<()> {
65        self.init_terminal()?;
66        self.install_panic_handler();
67
68        Ok(())
69    }
70
71    fn install_panic_handler(&self) {
72        let original_hook = std::panic::take_hook();
73        std::panic::set_hook(Box::new(move |panic_info| {
74            let _ = Self::restore_terminal();
75            original_hook(panic_info);
76        }))
77    }
78
79    fn init_terminal(&mut self) -> io::Result<()> {
80        self.stdout.execute(terminal::EnterAlternateScreen)?;
81        self.stdout
82            .execute(terminal::Clear(terminal::ClearType::All))?;
83
84        enable_raw_mode()?;
85
86        Ok(())
87    }
88
89    fn restore_terminal() -> io::Result<()> {
90        disable_raw_mode()?;
91
92        io::stdout().execute(terminal::LeaveAlternateScreen)?;
93
94        Ok(())
95    }
96
97    /// Toggles debug output
98    pub fn toggle_log_output(&self) -> io::Result<()> {
99        if Self::is_logging_enabled(self.logging_enabled.clone()) {
100            self.disable_log_output()?;
101        } else {
102            self.enable_log_output()?;
103        }
104
105        Ok(())
106    }
107
108    fn enable_log_output(&self) -> io::Result<()> {
109        let mut logging_enabled = self
110            .logging_enabled
111            .write()
112            .expect("Unable to get write lock on log output toggle");
113        *logging_enabled = true;
114
115        // Drop write lock before using again in logging
116        drop(logging_enabled);
117
118        debug!("Log output enabled");
119
120        Ok(())
121    }
122    fn disable_log_output(&self) -> io::Result<()> {
123        let mut logging_enabled = self
124            .logging_enabled
125            .write()
126            .expect("Unable to get write lock on log output toggle");
127        *logging_enabled = false;
128
129        // Drop write lock before using again in logging
130        drop(logging_enabled);
131
132        std::io::stdout().queue(crossterm::terminal::Clear(
133            crossterm::terminal::ClearType::All,
134        ))?;
135
136        debug!("Log output disabled");
137
138        Ok(())
139    }
140
141    fn configure_logging(logging_enabled: Arc<RwLock<bool>>) -> Handle {
142        use log::LevelFilter;
143        use log4rs::append::file::FileAppender;
144        use log4rs::config::{Appender, Config, Root};
145        use log4rs::encode::pattern::PatternEncoder;
146
147        let log_buffer = crate::vec_appender::Appender::with_capacity(100);
148        let log_dialog = crate::dialog_appender::Appender::new((0, 26), 5, logging_enabled);
149
150        let debug_log = FileAppender::builder()
151            .encoder(Box::new(PatternEncoder::new("{l} - {m}{n}\n")))
152            .build("log/debug.log")
153            .unwrap();
154
155        let config = Config::builder()
156            .appender(Appender::builder().build("debug_log", Box::new(debug_log)))
157            .appender(Appender::builder().build("log_buffer", Box::new(log_buffer)))
158            .appender(Appender::builder().build("log_dialog", Box::new(log_dialog)))
159            .build(
160                Root::builder()
161                    .appender("log_dialog")
162                    .appender("debug_log")
163                    .build(LevelFilter::Trace),
164            )
165            .unwrap();
166
167        log4rs::init_config(config).unwrap()
168    }
169
170    fn is_logging_enabled(logging_enabled: Arc<RwLock<bool>>) -> bool {
171        logging_enabled.read().map(|e| *e).unwrap_or(false)
172    }
173
174    /// Process keyboard input, returning an EventResult indicating wheter the user
175    /// submittied, aobrted, toggled debug output or if further input is required.
176    pub fn keyboard_event(&mut self, form: &mut Form, ev: Event) -> io::Result<EventResult> {
177        match ev {
178            Event::Key(k) if k.code == KeyCode::Esc => {
179                return Ok(EventResult::Abort);
180            }
181            Event::Key(k) if k.code == KeyCode::Enter => {
182                return Ok(EventResult::Submit);
183            }
184            Event::Key(k) if k.code == KeyCode::Left => {
185                form.move_event(k.code);
186            }
187            Event::Key(k) if k.code == KeyCode::Right => {
188                form.move_event(k.code);
189            }
190            Event::Key(k) if k.code == KeyCode::Up => {
191                form.move_event(k.code);
192            }
193            Event::Key(k) if k.code == KeyCode::Down => {
194                form.move_event(k.code);
195            }
196            Event::Key(k)
197                if k.code == KeyCode::Char('d') && k.modifiers.contains(KeyModifiers::CONTROL) =>
198            {
199                return Ok(EventResult::ToggleDebug);
200            }
201            _ => (),
202        }
203
204        Ok(EventResult::None)
205    }
206
207    /// Executes a form to completion.  This is the event loop of a program under normal
208    /// conditions.  Uses the crossterm input events.
209    pub fn execute(&mut self, form: &mut Form) -> io::Result<EventResult> {
210        let mut output = EventResult::None;
211        loop {
212            form.display(&mut io::stdout())?;
213
214            let ev = event::read()?;
215
216            if let Event::Key(kev) = ev {
217                if kev == KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL) {
218                    return Ok(EventResult::Abort);
219                }
220            }
221
222            debug!("Key event: {:?}", ev);
223
224            let form_result = form.event_handler(&ev)?;
225
226            // If the event was handled, bubble up the result
227            let result = if let EventHandlerResult::Handled(form_result) = form_result {
228                form_result
229            } else {
230                self.keyboard_event(form, ev)?
231            };
232
233            debug!("Result: {:?}", result);
234
235            match result {
236                EventResult::Abort => break,
237                EventResult::Submit => {
238                    output = EventResult::Submit;
239                    break;
240                }
241                EventResult::ToggleDebug => {
242                    self.toggle_log_output()?;
243                }
244                EventResult::None => (),
245            };
246        }
247
248        Ok(output)
249    }
250}
251
252impl Drop for App {
253    fn drop(&mut self) {
254        let _ = Self::restore_terminal();
255    }
256}