fm/app/
application.rs

1use std::io::stdout;
2use std::panic;
3use std::process::exit;
4use std::sync::{mpsc, Arc};
5
6#[cfg(debug_assertions)]
7use std::backtrace;
8
9use anyhow::Result;
10use clap::Parser;
11use crossterm::{
12    cursor,
13    event::{DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture},
14    execute,
15    terminal::{disable_raw_mode, Clear, ClearType},
16};
17use parking_lot::Mutex;
18use ratatui::{init as init_term, DefaultTerminal};
19
20use crate::app::{Displayer, Refresher, Status};
21use crate::common::{clear_input_socket_files, clear_tmp_files, save_final_path, CONFIG_PATH};
22use crate::config::{load_config, set_configurable_static, Config, IS_LOGGING};
23use crate::event::{remove_socket, EventDispatcher, EventReader, FmEvents};
24use crate::io::{Args, FMLogger, Opener};
25use crate::log_info;
26
27/// Holds everything about the application itself.
28/// Dropping the instance of FM allows to write again to stdout.
29/// It should be ran like this : `crate::app::Fm::start().run().quit()`.
30///
31/// The application is split into several components:
32/// - a reader of FmEvents,
33/// - a dispatcher of said events,
34/// - the state of the application itself, which is mutated by the dispatcher,
35/// - a displayer which holds a non mutable reference to the state. The displayer can emits events to force state change if needs be.
36/// - a refresher which is used to force a refresh state + display if something happened externally or in some running thread.
37pub struct FM {
38    /// Poll the event sent to the terminal by the user or the OS
39    event_reader: EventReader,
40    /// Associate the event to a method, modifing the status.
41    event_dispatcher: EventDispatcher,
42    /// Current status of the application. Mostly the filetrees
43    status: Arc<Mutex<Status>>,
44    /// Refresher is used to force a refresh when a file has been modified externally.
45    /// It also has a [`std::mpsc::Sender`] to send a quit message and reset the cursor.
46    refresher: Refresher,
47    /// Used to handle every display on the screen.
48    /// It runs a single thread with an mpsc receiver to handle quit events.
49    /// Drawing is done 30 times per second.
50    displayer: Displayer,
51}
52
53impl FM {
54    /// Setup everything the application needs in its main loop :
55    /// a panic hook for graceful panic and displaying a traceback for debugging purpose,
56    /// an `EventReader`,
57    /// an `EventDispatcher`,
58    /// a `Status`,
59    /// a `Display`,
60    /// a `Refresher`.
61    /// It reads and drops the configuration from the config file.
62    /// If the config can't be parsed, it exits with error code 1.
63    ///
64    /// # Errors
65    ///
66    /// May fail if the [`ratatui::DefaultTerminal`] can't be started or crashes
67    pub fn start() -> Result<Self> {
68        Self::set_panic_hook();
69        let (mut config, start_folder) = Self::early_exit()?;
70        log_info!("start folder: {start_folder}");
71        let plugins = std::mem::take(&mut config.plugins);
72        set_configurable_static(&start_folder, plugins)?;
73        Self::build(config)
74    }
75
76    /// Set a panic hook for debugging the application.
77    /// In case of panic, we ensure to:
78    /// - erase temporary files
79    /// - restore the terminal as best as possible (show the cursor, disable the mouse capture)
80    /// - if in debug mode (target=debug), display a full traceback.
81    /// - if in release mode (target=release), display a sorry message.
82    fn set_panic_hook() {
83        panic::set_hook(Box::new(|traceback| {
84            clear_tmp_files();
85            let _ = disable_raw_mode();
86            let _ = execute!(
87                stdout(),
88                cursor::Show,
89                DisableMouseCapture,
90                DisableBracketedPaste
91            );
92
93            if cfg!(debug_assertions) {
94                if let Some(payload) = traceback.payload().downcast_ref::<&str>() {
95                    eprintln!("Traceback: {payload}",);
96                } else if let Some(payload) = traceback.payload().downcast_ref::<String>() {
97                    eprintln!("Traceback: {payload}",);
98                } else {
99                    eprintln!("Traceback:{traceback:?}");
100                }
101                if let Some(location) = traceback.location() {
102                    eprintln!("At {location}");
103                }
104                #[cfg(debug_assertions)]
105                eprintln!("{}", backtrace::Backtrace::capture());
106            } else {
107                eprintln!("fm exited unexpectedly.");
108            }
109        }));
110    }
111
112    /// Read config and args, leaving immediatly if the arguments say so.
113    /// It will return the fully set [`crate::fm::config::Config`] and the starting path
114    /// as a String.
115    fn early_exit() -> Result<(Config, String)> {
116        let args = Args::parse();
117        IS_LOGGING.get_or_init(|| args.log);
118        if args.log {
119            FMLogger::default().init()?;
120        }
121        log_info!("args {args:#?}");
122        let Ok(config) = load_config(CONFIG_PATH) else {
123            Self::exit_wrong_config()
124        };
125        Ok((config, args.path))
126    }
127
128    /// Exit the application and log a message.
129    /// Used when the config can't be read.
130    pub fn exit_wrong_config() -> ! {
131        eprintln!("Couldn't load the config file at {CONFIG_PATH}. See https://raw.githubusercontent.com/qkzk/fm/master/config_files/fm/config.yaml for an example.");
132        log_info!("Couldn't read the config file {CONFIG_PATH}");
133        exit(1)
134    }
135
136    /// Internal builder. Builds an Fm instance from the config.
137    /// We have to create the terminal before starting the display.
138    /// - status requires the terminal size to be initialized (can we display right tab etc. ?)
139    /// - displays has a cloned arc to status
140    ///
141    /// The terminal is intancied there and passed to the display.
142    fn build(config: Config) -> Result<Self> {
143        let (fm_sender, fm_receiver) = mpsc::channel::<FmEvents>();
144        let event_reader = EventReader::new(fm_receiver);
145        let fm_sender = Arc::new(fm_sender);
146        let term = Self::init_term()?;
147        let status = Status::arc_mutex_new(
148            term.size()?,
149            Opener::default(),
150            &config.binds,
151            fm_sender.clone(),
152        )?;
153        let event_dispatcher = EventDispatcher::new(config.binds);
154        let refresher = Refresher::new(fm_sender);
155        let displayer = Displayer::new(term, status.clone());
156
157        Ok(Self {
158            event_reader,
159            event_dispatcher,
160            status,
161            refresher,
162            displayer,
163        })
164    }
165
166    fn init_term() -> Result<DefaultTerminal> {
167        let term = init_term();
168        execute!(stdout(), EnableMouseCapture, EnableBracketedPaste)?;
169        Ok(term)
170    }
171
172    /// Update itself, changing its status.
173    /// It will dispatch every [`FmEvents`], updating [`Status`].
174    fn update(&mut self, event: FmEvents) -> Result<()> {
175        let mut status = self.status.lock();
176        self.event_dispatcher.dispatch(&mut status, event)?;
177
178        Ok(())
179    }
180
181    /// True iff the application must quit.
182    fn must_quit(&self) -> Result<bool> {
183        let status = self.status.lock();
184        Ok(status.must_quit())
185    }
186
187    /// Run the update status loop and returns itself after completion.
188    pub fn run(mut self) -> Result<Self> {
189        while !self.must_quit()? {
190            self.update(self.event_reader.poll_event())?;
191        }
192        Ok(self)
193    }
194
195    /// Clear before normal exit.
196    fn clear() -> Result<()> {
197        execute!(stdout(), Clear(ClearType::All))?;
198        Ok(())
199    }
200
201    /// Disable the mouse capture before normal exit.
202    fn disable_mouse_capture() -> Result<()> {
203        execute!(stdout(), DisableMouseCapture, DisableBracketedPaste)?;
204        Ok(())
205    }
206
207    /// Reset everything as best as possible, stop any long thread in a loop and exit.
208    ///
209    /// More specifically :
210    /// - Display the cursor,
211    /// - drop itself, which allow us to print normally afterward
212    /// - print the final path
213    ///
214    /// # Errors
215    ///
216    /// May fail if the terminal crashes
217    /// May also fail if the thread running in [`crate::app::Refresher`] crashed
218    pub fn quit(self) -> Result<()> {
219        let final_path = self.status.lock().current_tab_path_str().to_owned();
220
221        clear_tmp_files();
222
223        remove_socket(&self.event_reader.socket_path);
224        drop(self.event_reader);
225        drop(self.event_dispatcher);
226        self.displayer.quit();
227        self.refresher.quit();
228        let status = self.status.lock();
229        status.previewer.quit();
230        if status.internal_settings.clear_before_quit {
231            Self::clear()?;
232        }
233        drop(status);
234
235        drop(self.status);
236        clear_input_socket_files()?;
237        Self::disable_mouse_capture()?;
238        save_final_path(&final_path);
239        Ok(())
240    }
241}