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::bail;
12use anyhow::{anyhow, Context, Result};
13use copypasta::{ClipboardContext, ClipboardProvider};
14use sysinfo::Disk;
15use sysinfo::Disks;
16use unicode_segmentation::UnicodeSegmentation;
17
18use crate::common::{CONFIG_FOLDER, ZOXIDE};
19use crate::config::IS_LOGGING;
20use crate::event::build_input_socket_filepath;
21use crate::io::{execute_without_output, 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 log_line!("copied content to clipboard.");
163 let Ok(mut ctx) = ClipboardContext::new() else {
164 return;
165 };
166 let Ok(_) = ctx.set_contents(content) else {
167 return;
168 };
169 let _ = ctx.get_contents();
171}
172
173pub fn content_to_clipboard(path: &std::path::Path) {
175 let Some(extension) = path.extension() else {
176 return;
177 };
178 if !matches!(
179 Extension::matcher(&extension.to_string_lossy()),
180 Extension::Text
181 ) {
182 return;
183 }
184 let Ok(content) = read_to_string(path) else {
185 return;
186 };
187 set_clipboard(content);
188 log_line!("Copied {path} content to clipboard", path = path.display());
189}
190
191pub fn filename_to_clipboard(path: &std::path::Path) {
193 let Some(filename) = path.file_name() else {
194 return;
195 };
196 let filename = filename.to_string_lossy().to_string();
197 set_clipboard(filename)
198}
199
200pub fn filepath_to_clipboard(path: &std::path::Path) {
202 let path = path.to_string_lossy().to_string();
203 set_clipboard(path)
204}
205
206pub fn row_to_window_index(row: u16) -> usize {
209 row as usize - ContentWindow::HEADER_ROWS
210}
211
212pub fn string_to_path(path_string: &str) -> Result<std::path::PathBuf> {
215 let expanded_cow_path = tilde(path_string);
216 let expanded_target: &str = expanded_cow_path.borrow();
217 Ok(std::fs::canonicalize(expanded_target)?)
218}
219
220pub fn is_sudo_command(executable: &str) -> bool {
222 matches!(executable, "sudo")
223}
224
225pub fn open_in_current_neovim(path: &Path, nvim_server: &str) {
227 log_info!(
228 "open_in_current_neovim {nvim_server} {path}",
229 path = path.display()
230 );
231 match nvim_open(nvim_server, path) {
232 Ok(()) => log_line!("Opened {path} in neovim", path = path.display()),
233 Err(error) => log_line!(
234 "Couldn't open {path} in neovim. Error {error:?}",
235 path = path.display()
236 ),
237 }
238}
239
240pub fn random_name() -> String {
243 let mut rand_str = String::with_capacity(10);
244 rand_str.push_str("fm-");
245 crate::common::random_alpha_chars()
246 .take(7)
247 .for_each(|ch| rand_str.push(ch));
248 rand_str.push_str(".txt");
249 rand_str
250}
251
252pub fn clear_tmp_files() {
254 let Ok(read_dir) = std::fs::read_dir("/tmp") else {
255 return;
256 };
257 read_dir
258 .filter_map(|e| e.ok())
259 .filter(|e| e.file_name().to_string_lossy().starts_with("fm_thumbnail"))
260 .for_each(|e| std::fs::remove_file(e.path()).unwrap_or_default())
261}
262
263pub fn clear_input_socket_files() -> Result<()> {
264 let input_socket_filepath = build_input_socket_filepath();
265 if std::path::Path::new(&input_socket_filepath).exists() {
266 std::fs::remove_file(&input_socket_filepath)?;
267 }
268 Ok(())
269}
270
271pub fn is_dir_empty(path: &std::path::Path) -> Result<bool> {
276 Ok(path.read_dir()?.next().is_none())
277}
278
279pub fn path_to_string<P>(path: &P) -> String
281where
282 P: AsRef<std::path::Path>,
283{
284 path.as_ref().to_string_lossy().into_owned()
285}
286
287pub fn has_last_modification_happened_less_than<P>(path: P, seconds: u64) -> Result<bool>
291where
292 P: AsRef<std::path::Path>,
293{
294 let modified = path.as_ref().metadata()?.modified()?;
295 if let Ok(elapsed) = modified.elapsed() {
296 let need_refresh = elapsed < std::time::Duration::new(seconds, 0);
297 Ok(need_refresh)
298 } else {
299 let dt: chrono::DateTime<chrono::offset::Utc> = modified.into();
300 let fmt = dt.format("%Y/%m/%d %T");
301 log_info!(
302 "Error for {path} modified datetime {fmt} is in future",
303 path = path.as_ref().display(),
304 );
305 Ok(false)
306 }
307}
308
309pub fn rename_filename<P, Q>(old_path: P, new_name: Q) -> Result<std::path::PathBuf>
319where
320 P: AsRef<std::path::Path>,
321 Q: AsRef<std::path::Path>,
322{
323 let Some(old_parent) = old_path.as_ref().parent() else {
324 return Err(anyhow!(
325 "no parent for {old_path}",
326 old_path = old_path.as_ref().display()
327 ));
328 };
329 let new_path = old_parent.join(new_name);
330 if old_path.as_ref() == new_path {
331 log_info!(
332 "Path didn't change for {new_path}.",
333 new_path = new_path.display()
334 );
335 return Ok(new_path);
336 }
337 if new_path.exists() {
338 log_line!(
339 "File already exists {new_path}",
340 new_path = new_path.display()
341 );
342 bail!(
343 "File already exists {new_path}",
344 new_path = new_path.display()
345 );
346 }
347 let Some(new_parent) = new_path.parent() else {
348 bail!("no parent for {new_path}", new_path = new_path.display());
349 };
350
351 log_info!(
352 "renaming: {} -> {}",
353 old_path.as_ref().display(),
354 new_path.display()
355 );
356 log_line!(
357 "renaming: {} -> {}",
358 old_path.as_ref().display(),
359 new_path.display()
360 );
361
362 std::fs::create_dir_all(new_parent)?;
363 std::fs::rename(old_path, &new_path)?;
364 Ok(new_path)
365}
366
367pub fn rename_fullpath<P, Q>(old_path: P, new_path: Q) -> Result<()>
377where
378 P: AsRef<std::path::Path>,
379 Q: AsRef<std::path::Path>,
380{
381 let new_path = new_path.as_ref();
382 if new_path.exists() {
383 return Err(anyhow!(
384 "File already exists {new_path}",
385 new_path = new_path.display()
386 ));
387 }
388 let Some(new_parent) = new_path.parent() else {
389 return Err(anyhow!(
390 "no parent for {new_path}",
391 new_path = new_path.display()
392 ));
393 };
394
395 log_info!(
396 "renaming: {} -> {}",
397 old_path.as_ref().display(),
398 new_path.display()
399 );
400 log_line!(
401 "renaming: {} -> {}",
402 old_path.as_ref().display(),
403 new_path.display()
404 );
405
406 std::fs::create_dir_all(new_parent)?;
407 std::fs::rename(old_path, new_path)?;
408 Ok(())
409}
410
411pub trait UtfWidth {
421 fn utf_width(&self) -> usize;
424 fn utf_width_u16(&self) -> u16;
427}
428
429impl UtfWidth for String {
430 fn utf_width(&self) -> usize {
431 self.as_str().utf_width()
432 }
433
434 fn utf_width_u16(&self) -> u16 {
435 self.utf_width() as u16
436 }
437}
438
439impl UtfWidth for &str {
440 fn utf_width(&self) -> usize {
441 self.graphemes(true)
442 .map(|s| s.to_string())
443 .collect::<Vec<String>>()
444 .len()
445 }
446
447 fn utf_width_u16(&self) -> u16 {
448 self.utf_width() as u16
449 }
450}
451
452pub fn index_from_a(letter: char) -> Option<usize> {
463 (letter as usize).checked_sub('a' as usize)
464}
465
466pub fn path_to_config_folder() -> Result<PathBuf> {
468 Ok(std::path::PathBuf::from_str(tilde(CONFIG_FOLDER).borrow())?)
469}
470
471fn home_dir() -> Option<PathBuf> {
472 std::env::var_os("HOME")
473 .and_then(|h| if h.is_empty() { None } else { Some(h) })
474 .map(PathBuf::from)
475}
476
477pub fn tilde(input_str: &str) -> Cow<'_, str> {
480 if let Some(input_after_tilde) = input_str.strip_prefix('~') {
481 if input_after_tilde.is_empty() || input_after_tilde.starts_with('/') {
482 if let Some(hd) = home_dir() {
483 let result = format!("{}{}", hd.display(), input_after_tilde);
484 result.into()
485 } else {
486 input_str.into()
488 }
489 } else {
490 input_str.into()
492 }
493 } else {
494 input_str.into()
496 }
497}
498
499pub fn set_current_dir<P: AsRef<Path>>(path: P) -> Result<()> {
501 Ok(env::set_current_dir(path.as_ref())?)
502}
503
504pub fn update_zoxide<P: AsRef<Path>>(path: P) -> Result<()> {
512 let Some(is_logging) = IS_LOGGING.get() else {
513 return Ok(());
514 };
515 if *is_logging && is_in_path(ZOXIDE) {
516 execute_without_output(ZOXIDE, &["add", path.as_ref().to_string_lossy().as_ref()])?;
517 }
518 Ok(())
519}
520
521pub fn build_dest_path(source: &Path, dest: &Path) -> Option<PathBuf> {
523 let mut dest = dest.to_path_buf();
524 let filename = source.file_name()?;
525 dest.push(filename);
526 Some(dest)
527}