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;
22use crate::io::Extension;
23use crate::modes::{human_size, nvim_open, ContentWindow, Users};
24use crate::{log_info, log_line};
25
26pub trait MountPoint<'a> {
28 fn mount_point(&self, mount_points: &'a HashSet<&'a Path>) -> Option<&Self>;
30}
31
32impl<'a> MountPoint<'a> for Path {
33 fn mount_point(&self, mount_points: &'a HashSet<&'a Path>) -> Option<&Self> {
34 let mut current = self;
35 while !mount_points.contains(current) {
36 current = current.parent()?;
37 }
38 Some(current)
39 }
40}
41
42fn disk_used_by_path<'a>(disks: &'a Disks, path: &Path) -> Option<&'a Disk> {
48 let mut disks: Vec<&'a Disk> = disks.list().iter().collect();
49 disks.sort_by_key(|disk| usize::MAX - disk.mount_point().components().count());
50 disks
51 .iter()
52 .find(|&disk| path.starts_with(disk.mount_point()))
53 .map(|disk| &**disk)
54}
55
56fn disk_space_used(disk: Option<&Disk>) -> String {
57 match disk {
58 None => "".to_owned(),
59 Some(disk) => human_size(disk.available_space()),
60 }
61}
62
63pub fn disk_space(disks: &Disks, path: &Path) -> String {
68 if path.as_os_str().is_empty() {
69 return "".to_owned();
70 }
71 disk_space_used(disk_used_by_path(disks, path))
72}
73
74pub fn save_final_path(final_path: &str) {
78 log_info!("print on quit {final_path}");
79 println!("{final_path}");
80 let Ok(mut file) = File::create("/tmp/fm_output.txt") else {
81 log_info!("Couldn't save {final_path} to /tmp/fm_output.txt");
82 return;
83 };
84 writeln!(file, "{final_path}").expect("Failed to write to file");
85}
86
87pub fn read_lines<P>(
89 filename: P,
90) -> std::io::Result<std::io::Lines<std::io::BufReader<std::fs::File>>>
91where
92 P: AsRef<std::path::Path>,
93{
94 let file = std::fs::File::open(filename)?;
95 Ok(std::io::BufReader::new(file).lines())
96}
97
98pub fn filename_from_path(path: &std::path::Path) -> Result<&str> {
101 path.file_name()
102 .unwrap_or_default()
103 .to_str()
104 .context("couldn't parse the filename")
105}
106
107pub fn current_uid() -> Result<u32> {
111 Ok(metadata("/proc/self").map(|metadata| metadata.uid())?)
112}
113
114pub fn current_username() -> Result<String> {
117 Users::only_users()
118 .get_user_by_uid(current_uid()?)
119 .context("Couldn't read my own name")
120 .cloned()
121}
122
123pub fn is_in_path<S>(program: S) -> bool
126where
127 S: Into<String> + std::fmt::Display + AsRef<Path>,
128{
129 let p = program.to_string();
130 let Some(program) = p.split_whitespace().next() else {
131 return false;
132 };
133 if Path::new(program).exists() {
134 return true;
135 }
136 if let Ok(path) = std::env::var("PATH") {
137 for p in path.split(':') {
138 let p_str = &format!("{p}/{program}");
139 if std::path::Path::new(p_str).exists() {
140 return true;
141 }
142 }
143 }
144 false
145}
146
147pub fn extract_lines(content: String) -> Vec<String> {
149 content.lines().map(|line| line.to_string()).collect()
150}
151
152pub fn get_clipboard() -> Option<String> {
154 let Ok(mut ctx) = ClipboardContext::new() else {
155 return None;
156 };
157 ctx.get_contents().ok()
158}
159
160pub fn set_clipboard(content: String) {
162 log_info!("copied to clipboard: {}", content);
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}