1use std::borrow::Borrow;
2use std::borrow::Cow;
3use std::collections::HashSet;
4use std::env;
5use std::fs::{metadata, read_to_string, File};
6use std::io::{BufRead, Write};
7use std::os::unix::fs::MetadataExt;
8use std::path::{Path, PathBuf};
9use std::str::FromStr;
10
11use anyhow::{anyhow, Context, Result};
12use copypasta::{ClipboardContext, ClipboardProvider};
13use sysinfo::Disk;
14use sysinfo::Disks;
15use unicode_segmentation::UnicodeSegmentation;
16
17use crate::common::{CONFIG_FOLDER, ZOXIDE};
18use crate::config::IS_LOGGING;
19use crate::event::build_input_socket_filepath;
20use crate::io::execute_without_output;
21use crate::io::Extension;
22use crate::modes::{human_size, nvim_open, ContentWindow, Users};
23use crate::{log_info, log_line};
24
25pub trait MountPoint<'a> {
27 fn mount_point(&self, mount_points: &'a HashSet<&'a Path>) -> Option<&Self>;
29}
30
31impl<'a> MountPoint<'a> for Path {
32 fn mount_point(&self, mount_points: &'a HashSet<&'a Path>) -> Option<&Self> {
33 let mut current = self;
34 while !mount_points.contains(current) {
35 current = current.parent()?;
36 }
37 Some(current)
38 }
39}
40
41fn disk_used_by_path<'a>(disks: &'a Disks, path: &Path) -> Option<&'a Disk> {
47 let mut disks: Vec<&'a Disk> = disks.list().iter().collect();
48 disks.sort_by_key(|disk| usize::MAX - disk.mount_point().components().count());
49 disks
50 .iter()
51 .find(|&disk| path.starts_with(disk.mount_point()))
52 .map(|disk| &**disk)
53}
54
55fn disk_space_used(disk: Option<&Disk>) -> String {
56 match disk {
57 None => "".to_owned(),
58 Some(disk) => human_size(disk.available_space()),
59 }
60}
61
62pub fn disk_space(disks: &Disks, path: &Path) -> String {
67 if path.as_os_str().is_empty() {
68 return "".to_owned();
69 }
70 disk_space_used(disk_used_by_path(disks, path))
71}
72
73pub fn save_final_path(final_path: &str) {
77 log_info!("print on quit {final_path}");
78 println!("{final_path}");
79 let Ok(mut file) = File::create("/tmp/fm_output.txt") else {
80 log_info!("Couldn't save {final_path} to /tmp/fm_output.txt");
81 return;
82 };
83 writeln!(file, "{final_path}").expect("Failed to write to file");
84}
85
86pub fn read_lines<P>(
88 filename: P,
89) -> std::io::Result<std::io::Lines<std::io::BufReader<std::fs::File>>>
90where
91 P: AsRef<std::path::Path>,
92{
93 let file = std::fs::File::open(filename)?;
94 Ok(std::io::BufReader::new(file).lines())
95}
96
97pub fn filename_from_path(path: &std::path::Path) -> Result<&str> {
100 path.file_name()
101 .unwrap_or_default()
102 .to_str()
103 .context("couldn't parse the filename")
104}
105
106pub fn current_uid() -> Result<u32> {
110 Ok(metadata("/proc/self").map(|metadata| metadata.uid())?)
111}
112
113pub fn current_username() -> Result<String> {
116 Users::only_users()
117 .get_user_by_uid(current_uid()?)
118 .context("Couldn't read my own name")
119 .cloned()
120}
121
122pub fn is_in_path<S>(program: S) -> bool
125where
126 S: Into<String> + std::fmt::Display + AsRef<Path>,
127{
128 let p = program.to_string();
129 let Some(program) = p.split_whitespace().next() else {
130 return false;
131 };
132 if Path::new(program).exists() {
133 return true;
134 }
135 if let Ok(path) = std::env::var("PATH") {
136 for p in path.split(':') {
137 let p_str = &format!("{p}/{program}");
138 if std::path::Path::new(p_str).exists() {
139 return true;
140 }
141 }
142 }
143 false
144}
145
146pub fn extract_lines(content: String) -> Vec<String> {
148 content.lines().map(|line| line.to_string()).collect()
149}
150
151pub fn get_clipboard() -> Option<String> {
153 let Ok(mut ctx) = ClipboardContext::new() else {
154 return None;
155 };
156 ctx.get_contents().ok()
157}
158
159pub fn set_clipboard(content: String) {
161 log_info!("copied to clipboard: {}", content);
162 let Ok(mut ctx) = ClipboardContext::new() else {
163 return;
164 };
165 let Ok(_) = ctx.set_contents(content) else {
166 return;
167 };
168 let _ = ctx.get_contents();
170}
171
172pub fn content_to_clipboard(path: &std::path::Path) {
174 let Some(extension) = path.extension() else {
175 return;
176 };
177 if !matches!(
178 Extension::matcher(&extension.to_string_lossy()),
179 Extension::Text
180 ) {
181 return;
182 }
183 let Ok(content) = read_to_string(path) else {
184 return;
185 };
186 set_clipboard(content);
187 log_line!("Copied {path} content to clipboard", path = path.display());
188}
189
190pub fn filename_to_clipboard(path: &std::path::Path) {
192 let Some(filename) = path.file_name() else {
193 return;
194 };
195 let filename = filename.to_string_lossy().to_string();
196 set_clipboard(filename)
197}
198
199pub fn filepath_to_clipboard(path: &std::path::Path) {
201 let path = path.to_string_lossy().to_string();
202 set_clipboard(path)
203}
204
205pub fn row_to_window_index(row: u16) -> usize {
208 row as usize - ContentWindow::HEADER_ROWS
209}
210
211pub fn string_to_path(path_string: &str) -> Result<std::path::PathBuf> {
214 let expanded_cow_path = tilde(path_string);
215 let expanded_target: &str = expanded_cow_path.borrow();
216 Ok(std::fs::canonicalize(expanded_target)?)
217}
218
219pub fn is_sudo_command(executable: &str) -> bool {
221 matches!(executable, "sudo")
222}
223
224pub fn open_in_current_neovim(path: &Path, nvim_server: &str) {
226 log_info!(
227 "open_in_current_neovim {nvim_server} {path}",
228 path = path.display()
229 );
230 match nvim_open(nvim_server, path) {
231 Ok(()) => log_line!("Opened {path} in neovim", path = path.display()),
232 Err(error) => log_line!(
233 "Couldn't open {path} in neovim. Error {error:?}",
234 path = path.display()
235 ),
236 }
237}
238
239pub fn random_name() -> String {
242 let mut rand_str = String::with_capacity(10);
243 rand_str.push_str("fm-");
244 crate::common::random_alpha_chars()
245 .take(7)
246 .for_each(|ch| rand_str.push(ch));
247 rand_str.push_str(".txt");
248 rand_str
249}
250
251pub fn clear_tmp_files() {
253 let Ok(read_dir) = std::fs::read_dir("/tmp") else {
254 return;
255 };
256 read_dir
257 .filter_map(|e| e.ok())
258 .filter(|e| e.file_name().to_string_lossy().starts_with("fm_thumbnail"))
259 .for_each(|e| std::fs::remove_file(e.path()).unwrap_or_default())
260}
261
262pub fn clear_input_socket_files() -> Result<()> {
263 let input_socket_filepath = build_input_socket_filepath();
264 if std::path::Path::new(&input_socket_filepath).exists() {
265 std::fs::remove_file(&input_socket_filepath)?;
266 }
267 Ok(())
268}
269
270pub fn is_dir_empty(path: &std::path::Path) -> Result<bool> {
275 Ok(path.read_dir()?.next().is_none())
276}
277
278pub fn path_to_string<P>(path: &P) -> String
280where
281 P: AsRef<std::path::Path>,
282{
283 path.as_ref().to_string_lossy().into_owned()
284}
285
286pub fn has_last_modification_happened_less_than<P>(path: P, seconds: u64) -> Result<bool>
290where
291 P: AsRef<std::path::Path>,
292{
293 let modified = path.as_ref().metadata()?.modified()?;
294 if let Ok(elapsed) = modified.elapsed() {
295 let need_refresh = elapsed < std::time::Duration::new(seconds, 0);
296 Ok(need_refresh)
297 } else {
298 let dt: chrono::DateTime<chrono::offset::Utc> = modified.into();
299 let fmt = dt.format("%Y/%m/%d %T");
300 log_info!(
301 "Error for {path} modified datetime {fmt} is in future",
302 path = path.as_ref().display(),
303 );
304 Ok(false)
305 }
306}
307
308pub fn rename_filename<P, Q>(old_path: P, new_name: Q) -> Result<std::path::PathBuf>
318where
319 P: AsRef<std::path::Path>,
320 Q: AsRef<std::path::Path>,
321{
322 let Some(old_parent) = old_path.as_ref().parent() else {
323 return Err(anyhow!(
324 "no parent for {old_path}",
325 old_path = old_path.as_ref().display()
326 ));
327 };
328 let new_path = old_parent.join(new_name);
329 if new_path.exists() {
330 return Err(anyhow!(
331 "File already exists {new_path}",
332 new_path = new_path.display()
333 ));
334 }
335 let Some(new_parent) = new_path.parent() else {
336 return Err(anyhow!(
337 "no parent for {new_path}",
338 new_path = new_path.display()
339 ));
340 };
341
342 log_info!(
343 "renaming: {} -> {}",
344 old_path.as_ref().display(),
345 new_path.display()
346 );
347 log_line!(
348 "renaming: {} -> {}",
349 old_path.as_ref().display(),
350 new_path.display()
351 );
352
353 std::fs::create_dir_all(new_parent)?;
354 std::fs::rename(old_path, &new_path)?;
355 Ok(new_path)
356}
357
358pub fn rename_fullpath<P, Q>(old_path: P, new_path: Q) -> Result<()>
368where
369 P: AsRef<std::path::Path>,
370 Q: AsRef<std::path::Path>,
371{
372 let new_path = new_path.as_ref();
373 if new_path.exists() {
374 return Err(anyhow!(
375 "File already exists {new_path}",
376 new_path = new_path.display()
377 ));
378 }
379 let Some(new_parent) = new_path.parent() else {
380 return Err(anyhow!(
381 "no parent for {new_path}",
382 new_path = new_path.display()
383 ));
384 };
385
386 log_info!(
387 "renaming: {} -> {}",
388 old_path.as_ref().display(),
389 new_path.display()
390 );
391 log_line!(
392 "renaming: {} -> {}",
393 old_path.as_ref().display(),
394 new_path.display()
395 );
396
397 std::fs::create_dir_all(new_parent)?;
398 std::fs::rename(old_path, new_path)?;
399 Ok(())
400}
401
402pub trait UtfWidth {
412 fn utf_width(&self) -> usize;
415 fn utf_width_u16(&self) -> u16;
418}
419
420impl UtfWidth for String {
421 fn utf_width(&self) -> usize {
422 self.as_str().utf_width()
423 }
424
425 fn utf_width_u16(&self) -> u16 {
426 self.utf_width() as u16
427 }
428}
429
430impl UtfWidth for &str {
431 fn utf_width(&self) -> usize {
432 self.graphemes(true)
433 .map(|s| s.to_string())
434 .collect::<Vec<String>>()
435 .len()
436 }
437
438 fn utf_width_u16(&self) -> u16 {
439 self.utf_width() as u16
440 }
441}
442
443pub fn index_from_a(letter: char) -> Option<usize> {
454 (letter as usize).checked_sub('a' as usize)
455}
456
457pub fn path_to_config_folder() -> Result<PathBuf> {
459 Ok(std::path::PathBuf::from_str(tilde(CONFIG_FOLDER).borrow())?)
460}
461
462fn home_dir() -> Option<PathBuf> {
463 std::env::var_os("HOME")
464 .and_then(|h| if h.is_empty() { None } else { Some(h) })
465 .map(PathBuf::from)
466}
467
468pub fn tilde(input_str: &str) -> Cow<'_, str> {
471 if let Some(input_after_tilde) = input_str.strip_prefix('~') {
472 if input_after_tilde.is_empty() || input_after_tilde.starts_with('/') {
473 if let Some(hd) = home_dir() {
474 let result = format!("{}{}", hd.display(), input_after_tilde);
475 result.into()
476 } else {
477 input_str.into()
479 }
480 } else {
481 input_str.into()
483 }
484 } else {
485 input_str.into()
487 }
488}
489
490pub fn set_current_dir<P: AsRef<Path>>(path: P) -> Result<()> {
492 Ok(env::set_current_dir(path.as_ref())?)
493}
494
495pub fn update_zoxide<P: AsRef<Path>>(path: P) -> Result<()> {
503 let Some(is_logging) = IS_LOGGING.get() else {
504 return Ok(());
505 };
506 if *is_logging && is_in_path(ZOXIDE) {
507 execute_without_output(ZOXIDE, &["add", path.as_ref().to_string_lossy().as_ref()])?;
508 }
509 Ok(())
510}
511
512pub fn build_dest_path(source: &Path, dest: &Path) -> Option<PathBuf> {
514 let mut dest = dest.to_path_buf();
515 let filename = source.file_name()?;
516 dest.push(filename);
517 Some(dest)
518}