Skip to main content

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        let theme = std::mem::take(&mut config.theme);
73        set_configurable_static(&start_folder, plugins, theme)?;
74        Self::build(config)
75    }
76
77    /// Set a panic hook for debugging the application.
78    /// In case of panic, we ensure to:
79    /// - erase temporary files
80    /// - restore the terminal as best as possible (show the cursor, disable the mouse capture)
81    /// - if in debug mode (target=debug), display a full traceback.
82    /// - if in release mode (target=release), display a sorry message.
83    fn set_panic_hook() {
84        panic::set_hook(Box::new(|traceback| {
85            clear_tmp_files();
86            let _ = disable_raw_mode();
87            let _ = execute!(
88                stdout(),
89                cursor::Show,
90                DisableMouseCapture,
91                DisableBracketedPaste
92            );
93
94            if cfg!(debug_assertions) {
95                if let Some(payload) = traceback.payload().downcast_ref::<&str>() {
96                    eprintln!("Traceback: {payload}",);
97                } else if let Some(payload) = traceback.payload().downcast_ref::<String>() {
98                    eprintln!("Traceback: {payload}",);
99                } else {
100                    eprintln!("Traceback:{traceback:?}");
101                }
102                if let Some(location) = traceback.location() {
103                    eprintln!("At {location}");
104                }
105                #[cfg(debug_assertions)]
106                eprintln!("{}", backtrace::Backtrace::capture());
107            } else {
108                eprintln!("fm exited unexpectedly.");
109            }
110        }));
111    }
112
113    /// Read config and args, leaving immediatly if the arguments say so.
114    /// It will return the fully set [`crate::fm::config::Config`] and the starting path
115    /// as a String.
116    fn early_exit() -> Result<(Config, String)> {
117        let args = Args::parse();
118        IS_LOGGING.get_or_init(|| args.log);
119        if args.log {
120            FMLogger::default().init()?;
121        }
122        log_info!("args {args:#?}");
123        let Ok(config) = load_config(CONFIG_PATH) else {
124            Self::exit_wrong_config()
125        };
126        Ok((config, args.path))
127    }
128
129    /// Exit the application and log a message.
130    /// Used when the config can't be read.
131    pub fn exit_wrong_config() -> ! {
132        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.");
133        log_info!("Couldn't read the config file {CONFIG_PATH}");
134        exit(1)
135    }
136
137    /// Internal builder. Builds an Fm instance from the config.
138    /// We have to create the terminal before starting the display.
139    /// - status requires the terminal size to be initialized (can we display right tab etc. ?)
140    /// - displays has a cloned arc to status
141    ///
142    /// The terminal is intancied there and passed to the display.
143    fn build(config: Config) -> Result<Self> {
144        let (fm_sender, fm_receiver) = mpsc::channel::<FmEvents>();
145        let event_reader = EventReader::new(fm_receiver);
146        let fm_sender = Arc::new(fm_sender);
147        let term = Self::init_term()?;
148        let status = Status::arc_mutex_new(
149            term.size()?,
150            Opener::default(),
151            &config.binds,
152            fm_sender.clone(),
153        )?;
154        let event_dispatcher = EventDispatcher::new(config.binds);
155        let refresher = Refresher::new(fm_sender);
156        let displayer = Displayer::new(term, status.clone());
157
158        Ok(Self {
159            event_reader,
160            event_dispatcher,
161            status,
162            refresher,
163            displayer,
164        })
165    }
166
167    fn init_term() -> Result<DefaultTerminal> {
168        let term = init_term();
169        execute!(stdout(), EnableMouseCapture, EnableBracketedPaste)?;
170        Ok(term)
171    }
172
173    /// Update itself, changing its status.
174    /// It will dispatch every [`FmEvents`], updating [`Status`].
175    fn update(&mut self, event: FmEvents) -> Result<()> {
176        let mut status = self.status.lock();
177        self.event_dispatcher.dispatch(&mut status, event)?;
178
179        Ok(())
180    }
181
182    /// True iff the application must quit.
183    fn must_quit(&self) -> Result<bool> {
184        let status = self.status.lock();
185        Ok(status.must_quit())
186    }
187
188    /// Run the update status loop and returns itself after completion.
189    pub fn run(mut self) -> Result<Self> {
190        while !self.must_quit()? {
191            self.update(self.event_reader.poll_event())?;
192        }
193        Ok(self)
194    }
195
196    /// Clear before normal exit.
197    fn clear() -> Result<()> {
198        execute!(stdout(), Clear(ClearType::All))?;
199        Ok(())
200    }
201
202    /// Disable the mouse capture before normal exit.
203    fn disable_mouse_capture() -> Result<()> {
204        execute!(stdout(), DisableMouseCapture, DisableBracketedPaste)?;
205        Ok(())
206    }
207
208    /// Reset everything as best as possible, stop any long thread in a loop and exit.
209    ///
210    /// More specifically :
211    /// - Display the cursor,
212    /// - drop itself, which allow us to print normally afterward
213    /// - print the final path
214    ///
215    /// # Errors
216    ///
217    /// May fail if the terminal crashes
218    /// May also fail if the thread running in [`crate::app::Refresher`] crashed
219    pub fn quit(self) -> Result<()> {
220        let final_path = self.status.lock().current_tab_path_str().to_owned();
221
222        clear_tmp_files();
223
224        remove_socket(&self.event_reader.socket_path);
225        drop(self.event_reader);
226        drop(self.event_dispatcher);
227        self.displayer.quit();
228        self.refresher.quit();
229        let status = self.status.lock();
230        status.previewer.quit();
231        if status.internal_settings.clear_before_quit {
232            Self::clear()?;
233        }
234        drop(status);
235
236        drop(self.status);
237        clear_input_socket_files()?;
238        Self::disable_mouse_capture()?;
239        save_final_path(&final_path);
240        Ok(())
241    }
242}