Skip to main content

fm/app/
internal_settings.rs

1use std::collections::{HashMap, HashSet};
2use std::path::{Path, PathBuf};
3use std::sync::{mpsc::Sender, Arc};
4
5use anyhow::{bail, Result};
6use clap::Parser;
7use indicatif::InMemoryTerm;
8use ratatui::buffer::Buffer;
9use ratatui::layout::Size;
10use sysinfo::Disks;
11
12use crate::common::{is_in_path, open_in_current_neovim, set_clipboard, NVIM, SS};
13use crate::config::Bindings;
14use crate::event::FmEvents;
15use crate::io::{
16    execute_and_output, read_rect_from_buffer, Args, Cursor, CursorDirection, Extension, External,
17    Opener,
18};
19use crate::modes::{copy_move, extract_extension, Content, Flagged};
20
21/// Internal settings of the status.
22///
23/// Every setting which couldn't be attached elsewhere and is needed by the whole application.
24/// It knows:
25/// - if the content should be completely refreshed,
26/// - if the application has to quit,
27/// - the address of the nvim_server to send files to and if the application was launched from neovim,
28/// - which opener should be used for kind of files,
29/// - the height & width of the application,
30/// - basic informations about disks being used,
31/// - a copy queue to display informations about files beeing copied.
32pub struct InternalSettings {
33    /// Do we have to clear the screen ?
34    pub force_clear: bool,
35    /// True if the user issued a quit event (`Key::Char('q')` by default).
36    /// It's used to exit the main loop before reseting the cursor.
37    pub must_quit: bool,
38    /// NVIM RPC server address
39    pub nvim_server: String,
40    /// The opener used by the application.
41    pub opener: Opener,
42    /// Termin size, width & height
43    pub size: Size,
44    /// Info about the running machine. Only used to detect disks
45    /// and their mount points.
46    pub disks: Disks,
47    /// true if the application was launched inside a neovim terminal emulator
48    pub inside_neovim: bool,
49    /// queue of pairs (sources, dest) to be copied.
50    /// it shouldn't be massive under normal usage so we can use a vector instead of an efficient queue data structure.
51    pub copy_file_queue: Vec<(Vec<PathBuf>, PathBuf)>,
52    /// internal progressbar used to display copy progress
53    pub in_mem_progress: Option<InMemoryTerm>,
54    /// true if the current terminal is disabled
55    is_disabled: bool,
56    /// true if the terminal should be cleared before exit. It's set to true when we reuse the window to start a new shell.
57    pub clear_before_quit: bool,
58    /// Cesor movement and selection. Responsible of recording what is selected by the user.
59    pub cursor: Cursor,
60    /// Last registered frame
61    pub last_buffer: Option<Buffer>,
62}
63
64impl InternalSettings {
65    /// Creates a new instance. Some parameters (`nvim_server` and `inside_neovim`) are read from args.
66    pub fn new(opener: Opener, size: Size, disks: Disks, binds: &Bindings) -> Self {
67        let args = Args::parse();
68        let force_clear = false;
69        let must_quit = false;
70        let nvim_server = args.server.clone();
71        let inside_neovim = args.neovim;
72        let copy_file_queue = vec![];
73        let in_mem_progress = None;
74        let is_disabled = false;
75        let clear_before_quit = false;
76        let cursor = Cursor::new(binds);
77        let last_buffer = None;
78        Self {
79            force_clear,
80            must_quit,
81            nvim_server,
82            opener,
83            disks,
84            size,
85            inside_neovim,
86            copy_file_queue,
87            in_mem_progress,
88            is_disabled,
89            clear_before_quit,
90            cursor,
91            last_buffer,
92        }
93    }
94
95    #[inline]
96    /// Returns the size of the terminal (width, height)
97    pub fn term_size(&self) -> Size {
98        self.size
99    }
100
101    /// Update the size from width & height.
102    pub fn update_size(&mut self, width: u16, height: u16) {
103        self.size = Size::from((width, height))
104    }
105
106    /// Set a "force clear" flag to true, which will reset the display.
107    /// It's used when some command or whatever may pollute the terminal.
108    /// We ensure to clear it before displaying again.
109    pub fn force_clear(&mut self) {
110        self.force_clear = true;
111    }
112
113    /// Reset the clear flag.
114    /// Prevent the display from being completely reset for the next frame.
115    pub fn reset_clear(&mut self) {
116        self.force_clear = false;
117    }
118
119    /// True iff some event required a complete refresh of the dispplay
120    pub fn should_be_cleared(&self) -> bool {
121        self.force_clear
122    }
123
124    /// Refresh the disks -- removing non listed ones -- and returns a reference
125    pub fn disks(&mut self) -> &Disks {
126        self.disks.refresh(true);
127        &self.disks
128    }
129
130    /// Returns a vector of mount points.
131    /// Disks are refreshed first.
132    pub fn mount_points_vec(&mut self) -> Vec<&Path> {
133        self.disks().iter().map(|d| d.mount_point()).collect()
134    }
135
136    /// Returns a set of mount points
137    pub fn mount_points_set(&self) -> HashSet<&Path> {
138        self.disks
139            .list()
140            .iter()
141            .map(|disk| disk.mount_point())
142            .collect()
143    }
144
145    /// Tries its best to update the neovim address.
146    /// 1. from the `$NVIM_LISTEN_ADDRESS` environment variable,
147    /// 2. from the opened socket read from ss.
148    ///
149    /// # Warning
150    /// If multiple neovim instances are opened at the same time, it will get the first one from the ss output.
151    pub fn update_nvim_listen_address(&mut self) {
152        if let Ok(nvim_listen_address) = std::env::var("NVIM_LISTEN_ADDRESS") {
153            self.nvim_server = nvim_listen_address;
154        } else if let Ok(nvim_listen_address) = Self::parse_nvim_address_from_ss_output() {
155            self.nvim_server = nvim_listen_address;
156        }
157    }
158
159    fn parse_nvim_address_from_ss_output() -> Result<String> {
160        if !is_in_path(SS) {
161            bail!("{SS} isn't installed");
162        }
163        if let Ok(output) = execute_and_output(SS, ["-l"]) {
164            let output = String::from_utf8(output.stdout).unwrap_or_default();
165            let content: String = output
166                .split(&['\n', '\t', ' '])
167                .find(|w| w.contains(NVIM))
168                .unwrap_or("")
169                .to_string();
170            if !content.is_empty() {
171                return Ok(content);
172            }
173        }
174        bail!("Couldn't get nvim listen address from `ss` output")
175    }
176
177    /// Remove the top of the copy queue.
178    pub fn copy_file_remove_head(&mut self) -> Result<()> {
179        if !self.copy_file_queue.is_empty() {
180            self.copy_file_queue.remove(0);
181        }
182        Ok(())
183    }
184
185    /// Start the copy of the next file in copy file queue and register the progress in
186    /// the mock terminal used to create the display.
187    pub fn copy_next_file_in_queue(
188        &mut self,
189        fm_sender: Arc<Sender<FmEvents>>,
190        width: u16,
191    ) -> Result<()> {
192        let (sources, dest) = self.copy_file_queue[0].clone();
193        let height = self.term_size().height;
194        let in_mem = copy_move(
195            crate::modes::CopyMove::Copy,
196            sources,
197            dest,
198            width,
199            height,
200            fm_sender,
201        )?;
202        self.store_copy_progress(in_mem);
203        Ok(())
204    }
205
206    /// Store copy progress bar.
207    /// When a copy progress bar is stored,
208    /// display manager is responsible for its display in the left tab.
209    pub fn store_copy_progress(&mut self, in_mem_progress_bar: InMemoryTerm) {
210        self.in_mem_progress = Some(in_mem_progress_bar);
211    }
212
213    /// Set copy progress bar to None.
214    pub fn unset_copy_progress(&mut self) {
215        self.in_mem_progress = None;
216    }
217
218    /// Disable the application display.
219    /// It's used to give to allow another program to be executed.
220    pub fn disable_display(&mut self) {
221        self.is_disabled = true;
222    }
223
224    /// Display the application after it gave its terminal to another program.
225    ///
226    /// Enable the display again,
227    /// clear the screen,
228    /// set a flag to clear before quitting application.
229    pub fn enable_display(&mut self) {
230        if !self.is_disabled() {
231            return;
232        }
233        self.is_disabled = false;
234        self.force_clear();
235        self.clear_before_quit = true;
236    }
237
238    /// True iff the terminal is disabled.
239    /// The state (`self.is_disabled`) is changed every time
240    /// a new shell is started replacing the normal window.
241    /// If true, the display shouldn't be drawn.
242    pub fn is_disabled(&self) -> bool {
243        self.is_disabled
244    }
245
246    /// Open a new command which output will replace the current display.
247    /// Current progress of the application is locked as long as the command doesn't finish.
248    /// Firstly the display is disabled, then the command is ran.
249    /// Once the command ends... the display is reenabled again.
250    pub fn open_in_window<P>(&mut self, args: &[&str], current_path: P) -> Result<()>
251    where
252        P: AsRef<Path>,
253    {
254        self.disable_display();
255        External::open_command_in_window(args, current_path)?;
256        self.enable_display();
257        Ok(())
258    }
259
260    fn should_this_file_be_opened_in_neovim(&self, path: &Path) -> bool {
261        matches!(Extension::matcher(extract_extension(path)), Extension::Text)
262    }
263
264    /// Open a single file:
265    /// In neovim if this file should be,
266    /// or in a new shell in current terminal,
267    /// or in a new window.
268    pub fn open_single_file<P>(&mut self, path: &Path, current_path: P) -> Result<()>
269    where
270        P: AsRef<Path>,
271    {
272        if self.inside_neovim && self.should_this_file_be_opened_in_neovim(path) {
273            self.update_nvim_listen_address();
274            open_in_current_neovim(path, &self.nvim_server);
275            Ok(())
276        } else if self.opener.use_term(path) {
277            self.open_single_in_window(path, current_path);
278            Ok(())
279        } else {
280            self.opener.open_single(path)
281        }
282    }
283
284    fn open_single_in_window<P>(&mut self, path: &Path, current_path: P)
285    where
286        P: AsRef<Path>,
287    {
288        self.disable_display();
289        self.opener.open_in_window(path, current_path);
290        self.enable_display();
291    }
292
293    /// Open all the flagged files.
294    /// We try to open all files in a single command if it's possible.
295    /// If all files should be opened in neovim, it will be.
296    /// Otherwise, they will be opened separetely.
297    pub fn open_flagged_files<P>(&mut self, flagged: &Flagged, current_path: P) -> Result<()>
298    where
299        P: AsRef<Path>,
300    {
301        if self.inside_neovim && flagged.should_all_be_opened_in_neovim() {
302            self.open_multiple_in_neovim(flagged.content());
303            Ok(())
304        } else {
305            self.open_multiple_outside(flagged.content(), current_path)
306        }
307    }
308
309    fn open_multiple_outside<P>(&mut self, paths: &[PathBuf], current_path: P) -> Result<()>
310    where
311        P: AsRef<Path>,
312    {
313        let openers = self.opener.regroup_per_opener(paths);
314        if Self::all_files_opened_in_terminal(&openers) {
315            self.open_multiple_files_in_window(openers, current_path)
316        } else {
317            self.opener.open_multiple(openers)
318        }
319    }
320
321    fn all_files_opened_in_terminal(openers: &HashMap<External, Vec<PathBuf>>) -> bool {
322        openers.len() == 1 && openers.keys().next().expect("Can't be empty").use_term()
323    }
324
325    fn open_multiple_files_in_window<P>(
326        &mut self,
327        openers: HashMap<External, Vec<PathBuf>>,
328        current_path: P,
329    ) -> Result<()>
330    where
331        P: AsRef<Path>,
332    {
333        self.disable_display();
334        self.opener.open_multiple_in_window(openers, current_path)?;
335        self.enable_display();
336        Ok(())
337    }
338
339    fn open_multiple_in_neovim(&mut self, paths: &[PathBuf]) {
340        self.update_nvim_listen_address();
341        for path in paths {
342            open_in_current_neovim(path, &self.nvim_server);
343        }
344    }
345
346    /// Set the must quit flag to true.
347    /// The next update call will exit the application.
348    /// It doesn't exit the application itself.
349    pub fn quit(&mut self) {
350        self.must_quit = true
351    }
352
353    /// Format the progress of the current operation in copy file queue.
354    /// If nothing is being copied, it returns `None`
355    pub fn format_copy_progress(&self) -> Option<String> {
356        let Some(copy_progress) = &self.in_mem_progress else {
357            return None;
358        };
359        let progress_bar = copy_progress.contents();
360        let nb_copy_left = self.copy_file_queue.len();
361        if nb_copy_left <= 1 {
362            Some(progress_bar)
363        } else {
364            Some(format!(
365                "{progress_bar}     -     1 of {nb}",
366                nb = nb_copy_left
367            ))
368        }
369    }
370
371    /// Move the cursor in given direction, up, down, left or right.
372    /// Cursor is clamped to the the screen and can't move outside.
373    /// Extends the selection if the cursor is active.
374    pub fn move_cursor(&mut self, direction: CursorDirection) {
375        self.cursor.move_cursor(direction, self.term_size());
376        if self.cursor.is_selecting() {
377            self.cursor.extend_selection();
378        }
379    }
380
381    /// Copy the rect buffer of text in the clipboard.
382    pub fn copy_buffer_rect(&self) {
383        let Some(buffer) = &self.last_buffer else {
384            crate::log_info!("Tried to read last buffer but had nothing.");
385            crate::log_line!("Couldn't copy the content...");
386            return;
387        };
388        let Some(rect) = &self.cursor.rect() else {
389            return;
390        };
391        let content = read_rect_from_buffer(rect, buffer);
392        set_clipboard(content);
393    }
394}