fm/app/
internal_settings.rs

1use std::collections::{HashMap, HashSet};
2use std::path::{Path, PathBuf};
3use std::sync::{mpsc::Sender, Arc};
4
5use anyhow::{anyhow, Result};
6use clap::Parser;
7use indicatif::InMemoryTerm;
8use ratatui::layout::Size;
9use sysinfo::Disks;
10
11use crate::common::{is_in_path, open_in_current_neovim, NVIM, SS};
12use crate::event::FmEvents;
13use crate::io::{execute_and_output, Args, Extension, External, Opener};
14use crate::modes::{copy_move, extract_extension, Content, Flagged};
15
16/// Internal settings of the status.
17///
18/// Every setting which couldn't be attached elsewhere and is needed by the whole application.
19/// It knows:
20/// - if the content should be completely refreshed,
21/// - if the application has to quit,
22/// - the address of the nvim_server to send files to and if the application was launched from neovim,
23/// - which opener should be used for kind of files,
24/// - the height & width of the application,
25/// - basic informations about disks being used,
26/// - a copy queue to display informations about files beeing copied.
27pub struct InternalSettings {
28    /// Do we have to clear the screen ?
29    pub force_clear: bool,
30    /// True if the user issued a quit event (`Key::Char('q')` by default).
31    /// It's used to exit the main loop before reseting the cursor.
32    pub must_quit: bool,
33    /// NVIM RPC server address
34    pub nvim_server: String,
35    /// The opener used by the application.
36    pub opener: Opener,
37    /// Termin size, width & height
38    pub size: Size,
39    /// Info about the running machine. Only used to detect disks
40    /// and their mount points.
41    pub disks: Disks,
42    /// true if the application was launched inside a neovim terminal emulator
43    pub inside_neovim: bool,
44    /// queue of pairs (sources, dest) to be copied.
45    /// it shouldn't be massive under normal usage so we can use a vector instead of an efficient queue data structure.
46    pub copy_file_queue: Vec<(Vec<PathBuf>, PathBuf)>,
47    /// internal progressbar used to display copy progress
48    pub in_mem_progress: Option<InMemoryTerm>,
49    /// true if the current terminal is disabled
50    is_disabled: bool,
51    /// true if the terminal should be cleared before exit. It's set to true when we reuse the window to start a new shell.
52    pub clear_before_quit: bool,
53}
54
55impl InternalSettings {
56    pub fn new(opener: Opener, size: Size, disks: Disks) -> Self {
57        let args = Args::parse();
58        let force_clear = false;
59        let must_quit = false;
60        let nvim_server = args.server.clone();
61        let inside_neovim = args.neovim;
62        let copy_file_queue = vec![];
63        let in_mem_progress = None;
64        let is_disabled = false;
65        let clear_before_quit = false;
66        Self {
67            force_clear,
68            must_quit,
69            nvim_server,
70            opener,
71            disks,
72            size,
73            inside_neovim,
74            copy_file_queue,
75            in_mem_progress,
76            is_disabled,
77            clear_before_quit,
78        }
79    }
80
81    /// Returns the size of the terminal (width, height)
82    pub fn term_size(&self) -> Size {
83        self.size
84    }
85
86    pub fn update_size(&mut self, width: u16, height: u16) {
87        self.size = Size::from((width, height))
88    }
89
90    /// Set a "force clear" flag to true, which will reset the display.
91    /// It's used when some command or whatever may pollute the terminal.
92    /// We ensure to clear it before displaying again.
93    pub fn force_clear(&mut self) {
94        self.force_clear = true;
95    }
96
97    pub fn reset_clear(&mut self) {
98        self.force_clear = false;
99    }
100
101    pub fn should_be_cleared(&self) -> bool {
102        self.force_clear
103    }
104
105    pub fn disks(&mut self) -> &Disks {
106        self.disks.refresh_list();
107        &self.disks
108    }
109
110    pub fn mount_points_vec(&mut self) -> Vec<&Path> {
111        self.disks().iter().map(|d| d.mount_point()).collect()
112    }
113
114    pub fn mount_points_set(&self) -> HashSet<&Path> {
115        self.disks
116            .list()
117            .iter()
118            .map(|disk| disk.mount_point())
119            .collect()
120    }
121
122    pub fn update_nvim_listen_address(&mut self) {
123        if let Ok(nvim_listen_address) = std::env::var("NVIM_LISTEN_ADDRESS") {
124            self.nvim_server = nvim_listen_address;
125        } else if let Ok(nvim_listen_address) = Self::parse_nvim_address_from_ss_output() {
126            self.nvim_server = nvim_listen_address;
127        }
128    }
129
130    fn parse_nvim_address_from_ss_output() -> Result<String> {
131        if !is_in_path(SS) {
132            return Err(anyhow!("{SS} isn't installed"));
133        }
134        if let Ok(output) = execute_and_output(SS, ["-l"]) {
135            let output = String::from_utf8(output.stdout).unwrap_or_default();
136            let content: String = output
137                .split(&['\n', '\t', ' '])
138                .find(|w| w.contains(NVIM))
139                .unwrap_or("")
140                .to_string();
141            if !content.is_empty() {
142                return Ok(content);
143            }
144        }
145        Err(anyhow!("Couldn't get nvim listen address from `ss` output"))
146    }
147
148    /// Remove the top of the copy queue.
149    pub fn copy_file_remove_head(&mut self) -> Result<()> {
150        if self.copy_file_queue.is_empty() {
151            Err(anyhow!("Copy File Pool is empty"))
152        } else {
153            self.copy_file_queue.remove(0);
154            Ok(())
155        }
156    }
157
158    pub fn copy_next_file_in_queue(
159        &mut self,
160        fm_sender: Arc<Sender<FmEvents>>,
161        width: u16,
162    ) -> Result<()> {
163        let (sources, dest) = self.copy_file_queue[0].clone();
164        let Size { width: _, height } = self.term_size();
165        let in_mem = copy_move(
166            crate::modes::CopyMove::Copy,
167            sources,
168            dest,
169            width,
170            height,
171            fm_sender,
172        )?;
173        self.store_copy_progress(in_mem);
174        Ok(())
175    }
176
177    /// Store copy progress bar.
178    /// When a copy progress bar is stored,
179    /// display manager is responsible for its display in the left tab.
180    pub fn store_copy_progress(&mut self, in_mem_progress_bar: InMemoryTerm) {
181        self.in_mem_progress = Some(in_mem_progress_bar);
182    }
183
184    /// Set copy progress bar to None.
185    pub fn unset_copy_progress(&mut self) {
186        self.in_mem_progress = None;
187    }
188
189    /// Disable the application display.
190    /// It's used to give to allow another program to be executed.
191    pub fn disable_display(&mut self) {
192        self.is_disabled = true;
193    }
194
195    /// Display the application after it gave its terminal to another program.
196    ///
197    /// Enable the display again,
198    /// clear the screen,
199    /// set a flag to clear before quitting application.
200    pub fn enable_display(&mut self) {
201        if !self.is_disabled() {
202            return;
203        }
204        self.is_disabled = false;
205        self.force_clear();
206        self.clear_before_quit = true;
207    }
208
209    pub fn is_disabled(&self) -> bool {
210        self.is_disabled
211    }
212
213    pub fn open_in_window(&mut self, args: &[&str]) -> Result<()> {
214        self.disable_display();
215        External::open_command_in_window(args)?;
216        self.enable_display();
217        Ok(())
218    }
219
220    fn should_this_file_be_opened_in_neovim(&self, path: &Path) -> bool {
221        matches!(Extension::matcher(extract_extension(path)), Extension::Text)
222    }
223
224    pub fn open_single_file(&mut self, path: &Path) -> Result<()> {
225        if self.inside_neovim && self.should_this_file_be_opened_in_neovim(path) {
226            self.update_nvim_listen_address();
227            open_in_current_neovim(path, &self.nvim_server);
228            Ok(())
229        } else if self.opener.use_term(path) {
230            self.open_single_in_window(path);
231            Ok(())
232        } else {
233            self.opener.open_single(path)
234        }
235    }
236
237    fn open_single_in_window(&mut self, path: &Path) {
238        self.disable_display();
239        self.opener.open_in_window(path);
240        self.enable_display();
241    }
242
243    pub fn open_flagged_files(&mut self, flagged: &Flagged) -> Result<()> {
244        if self.inside_neovim && flagged.should_all_be_opened_in_neovim() {
245            self.open_multiple_in_neovim(flagged.content());
246            Ok(())
247        } else {
248            self.open_multiple_outside(flagged.content())
249        }
250    }
251
252    fn open_multiple_outside(&mut self, paths: &[PathBuf]) -> Result<()> {
253        let openers = self.opener.regroup_per_opener(paths);
254        if Self::all_files_opened_in_terminal(&openers) {
255            self.open_multiple_files_in_window(openers)
256        } else {
257            self.opener.open_multiple(openers)
258        }
259    }
260
261    fn all_files_opened_in_terminal(openers: &HashMap<External, Vec<PathBuf>>) -> bool {
262        openers.len() == 1 && openers.keys().next().expect("Can't be empty").use_term()
263    }
264
265    fn open_multiple_files_in_window(
266        &mut self,
267        openers: HashMap<External, Vec<PathBuf>>,
268    ) -> Result<()> {
269        self.disable_display();
270        self.opener.open_multiple_in_window(openers)?;
271        self.enable_display();
272        Ok(())
273    }
274
275    fn open_multiple_in_neovim(&mut self, paths: &[PathBuf]) {
276        self.update_nvim_listen_address();
277        for path in paths {
278            open_in_current_neovim(path, &self.nvim_server);
279        }
280    }
281
282    /// Set the must quit flag to true.
283    /// The next update call will exit the application.
284    /// It doesn't exit the application itself.
285    pub fn quit(&mut self) {
286        self.must_quit = true
287    }
288
289    pub fn format_copy_progress(&self) -> Option<String> {
290        let Some(copy_progress) = &self.in_mem_progress else {
291            return None;
292        };
293        let progress_bar = copy_progress.contents();
294        let nb_copy_left = self.copy_file_queue.len();
295        if nb_copy_left <= 1 {
296            Some(progress_bar)
297        } else {
298            Some(format!(
299                "{progress_bar}     -     1 of {nb}",
300                nb = nb_copy_left
301            ))
302        }
303    }
304}