use std::borrow::Borrow;
use std::borrow::Cow;
use std::collections::HashSet;
use std::env;
use std::fs::{metadata, read_to_string, File};
use std::io::{BufRead, Write};
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use anyhow::bail;
use anyhow::{anyhow, Context, Result};
use copypasta::{ClipboardContext, ClipboardProvider};
use sysinfo::Disk;
use sysinfo::Disks;
use unicode_segmentation::UnicodeSegmentation;
use crate::common::{CONFIG_FOLDER, ZOXIDE};
use crate::config::IS_LOGGING;
use crate::event::build_input_socket_filepath;
use crate::io::execute_without_output;
use crate::io::Extension;
use crate::modes::{human_size, nvim_open, ContentWindow, Users};
use crate::{log_info, log_line};
pub trait MountPoint<'a> {
fn mount_point(&self, mount_points: &'a HashSet<&'a Path>) -> Option<&Self>;
}
impl<'a> MountPoint<'a> for Path {
fn mount_point(&self, mount_points: &'a HashSet<&'a Path>) -> Option<&Self> {
let mut current = self;
while !mount_points.contains(current) {
current = current.parent()?;
}
Some(current)
}
}
fn disk_used_by_path<'a>(disks: &'a Disks, path: &Path) -> Option<&'a Disk> {
let mut disks: Vec<&'a Disk> = disks.list().iter().collect();
disks.sort_by_key(|disk| usize::MAX - disk.mount_point().components().count());
disks
.iter()
.find(|&disk| path.starts_with(disk.mount_point()))
.map(|disk| &**disk)
}
fn disk_space_used(disk: Option<&Disk>) -> String {
match disk {
None => "".to_owned(),
Some(disk) => human_size(disk.available_space()),
}
}
pub fn disk_space(disks: &Disks, path: &Path) -> String {
if path.as_os_str().is_empty() {
return "".to_owned();
}
disk_space_used(disk_used_by_path(disks, path))
}
pub fn save_final_path(final_path: &str) {
log_info!("print on quit {final_path}");
println!("{final_path}");
let Ok(mut file) = File::create("/tmp/fm_output.txt") else {
log_info!("Couldn't save {final_path} to /tmp/fm_output.txt");
return;
};
writeln!(file, "{final_path}").expect("Failed to write to file");
}
pub fn read_lines<P>(
filename: P,
) -> std::io::Result<std::io::Lines<std::io::BufReader<std::fs::File>>>
where
P: AsRef<std::path::Path>,
{
let file = std::fs::File::open(filename)?;
Ok(std::io::BufReader::new(file).lines())
}
pub fn filename_from_path(path: &std::path::Path) -> Result<&str> {
path.file_name()
.unwrap_or_default()
.to_str()
.context("couldn't parse the filename")
}
pub fn current_uid() -> Result<u32> {
Ok(metadata("/proc/self").map(|metadata| metadata.uid())?)
}
pub fn current_username() -> Result<String> {
Users::only_users()
.get_user_by_uid(current_uid()?)
.context("Couldn't read my own name")
.cloned()
}
pub fn is_in_path<S>(program: S) -> bool
where
S: Into<String> + std::fmt::Display + AsRef<Path>,
{
let p = program.to_string();
let Some(program) = p.split_whitespace().next() else {
return false;
};
if Path::new(program).exists() {
return true;
}
if let Ok(path) = std::env::var("PATH") {
for p in path.split(':') {
let p_str = &format!("{p}/{program}");
if std::path::Path::new(p_str).exists() {
return true;
}
}
}
false
}
pub fn extract_lines(content: String) -> Vec<String> {
content.lines().map(|line| line.to_string()).collect()
}
pub fn get_clipboard() -> Option<String> {
let Ok(mut ctx) = ClipboardContext::new() else {
return None;
};
ctx.get_contents().ok()
}
pub fn set_clipboard(content: String) {
log_info!("copied to clipboard: {}", content);
let Ok(mut ctx) = ClipboardContext::new() else {
return;
};
let Ok(_) = ctx.set_contents(content) else {
return;
};
let _ = ctx.get_contents();
}
pub fn content_to_clipboard(path: &std::path::Path) {
let Some(extension) = path.extension() else {
return;
};
if !matches!(
Extension::matcher(&extension.to_string_lossy()),
Extension::Text
) {
return;
}
let Ok(content) = read_to_string(path) else {
return;
};
set_clipboard(content);
log_line!("Copied {path} content to clipboard", path = path.display());
}
pub fn filename_to_clipboard(path: &std::path::Path) {
let Some(filename) = path.file_name() else {
return;
};
let filename = filename.to_string_lossy().to_string();
set_clipboard(filename)
}
pub fn filepath_to_clipboard(path: &std::path::Path) {
let path = path.to_string_lossy().to_string();
set_clipboard(path)
}
pub fn row_to_window_index(row: u16) -> usize {
row as usize - ContentWindow::HEADER_ROWS
}
pub fn string_to_path(path_string: &str) -> Result<std::path::PathBuf> {
let expanded_cow_path = tilde(path_string);
let expanded_target: &str = expanded_cow_path.borrow();
Ok(std::fs::canonicalize(expanded_target)?)
}
pub fn is_sudo_command(executable: &str) -> bool {
matches!(executable, "sudo")
}
pub fn open_in_current_neovim(path: &Path, nvim_server: &str) {
log_info!(
"open_in_current_neovim {nvim_server} {path}",
path = path.display()
);
match nvim_open(nvim_server, path) {
Ok(()) => log_line!("Opened {path} in neovim", path = path.display()),
Err(error) => log_line!(
"Couldn't open {path} in neovim. Error {error:?}",
path = path.display()
),
}
}
pub fn random_name() -> String {
let mut rand_str = String::with_capacity(10);
rand_str.push_str("fm-");
crate::common::random_alpha_chars()
.take(7)
.for_each(|ch| rand_str.push(ch));
rand_str.push_str(".txt");
rand_str
}
pub fn clear_tmp_files() {
let Ok(read_dir) = std::fs::read_dir("/tmp") else {
return;
};
read_dir
.filter_map(|e| e.ok())
.filter(|e| e.file_name().to_string_lossy().starts_with("fm_thumbnail"))
.for_each(|e| std::fs::remove_file(e.path()).unwrap_or_default())
}
pub fn clear_input_socket_files() -> Result<()> {
let input_socket_filepath = build_input_socket_filepath();
if std::path::Path::new(&input_socket_filepath).exists() {
std::fs::remove_file(&input_socket_filepath)?;
}
Ok(())
}
pub fn is_dir_empty(path: &std::path::Path) -> Result<bool> {
Ok(path.read_dir()?.next().is_none())
}
pub fn path_to_string<P>(path: &P) -> String
where
P: AsRef<std::path::Path>,
{
path.as_ref().to_string_lossy().into_owned()
}
pub fn has_last_modification_happened_less_than<P>(path: P, seconds: u64) -> Result<bool>
where
P: AsRef<std::path::Path>,
{
let modified = path.as_ref().metadata()?.modified()?;
if let Ok(elapsed) = modified.elapsed() {
let need_refresh = elapsed < std::time::Duration::new(seconds, 0);
Ok(need_refresh)
} else {
let dt: chrono::DateTime<chrono::offset::Utc> = modified.into();
let fmt = dt.format("%Y/%m/%d %T");
log_info!(
"Error for {path} modified datetime {fmt} is in future",
path = path.as_ref().display(),
);
Ok(false)
}
}
pub fn rename_filename<P, Q>(old_path: P, new_name: Q) -> Result<std::path::PathBuf>
where
P: AsRef<std::path::Path>,
Q: AsRef<std::path::Path>,
{
let Some(old_parent) = old_path.as_ref().parent() else {
return Err(anyhow!(
"no parent for {old_path}",
old_path = old_path.as_ref().display()
));
};
let new_path = old_parent.join(new_name);
if old_path.as_ref() == new_path {
log_info!(
"Path didn't change for {new_path}.",
new_path = new_path.display()
);
return Ok(new_path);
}
if new_path.exists() {
log_line!(
"File already exists {new_path}",
new_path = new_path.display()
);
bail!(
"File already exists {new_path}",
new_path = new_path.display()
);
}
let Some(new_parent) = new_path.parent() else {
bail!("no parent for {new_path}", new_path = new_path.display());
};
log_info!(
"renaming: {} -> {}",
old_path.as_ref().display(),
new_path.display()
);
log_line!(
"renaming: {} -> {}",
old_path.as_ref().display(),
new_path.display()
);
std::fs::create_dir_all(new_parent)?;
std::fs::rename(old_path, &new_path)?;
Ok(new_path)
}
pub fn rename_fullpath<P, Q>(old_path: P, new_path: Q) -> Result<()>
where
P: AsRef<std::path::Path>,
Q: AsRef<std::path::Path>,
{
let new_path = new_path.as_ref();
if new_path.exists() {
return Err(anyhow!(
"File already exists {new_path}",
new_path = new_path.display()
));
}
let Some(new_parent) = new_path.parent() else {
return Err(anyhow!(
"no parent for {new_path}",
new_path = new_path.display()
));
};
log_info!(
"renaming: {} -> {}",
old_path.as_ref().display(),
new_path.display()
);
log_line!(
"renaming: {} -> {}",
old_path.as_ref().display(),
new_path.display()
);
std::fs::create_dir_all(new_parent)?;
std::fs::rename(old_path, new_path)?;
Ok(())
}
pub trait UtfWidth {
fn utf_width(&self) -> usize;
fn utf_width_u16(&self) -> u16;
}
impl UtfWidth for String {
fn utf_width(&self) -> usize {
self.as_str().utf_width()
}
fn utf_width_u16(&self) -> u16 {
self.utf_width() as u16
}
}
impl UtfWidth for &str {
fn utf_width(&self) -> usize {
self.graphemes(true)
.map(|s| s.to_string())
.collect::<Vec<String>>()
.len()
}
fn utf_width_u16(&self) -> u16 {
self.utf_width() as u16
}
}
pub fn index_from_a(letter: char) -> Option<usize> {
(letter as usize).checked_sub('a' as usize)
}
pub fn path_to_config_folder() -> Result<PathBuf> {
Ok(std::path::PathBuf::from_str(tilde(CONFIG_FOLDER).borrow())?)
}
fn home_dir() -> Option<PathBuf> {
std::env::var_os("HOME")
.and_then(|h| if h.is_empty() { None } else { Some(h) })
.map(PathBuf::from)
}
pub fn tilde(input_str: &str) -> Cow<'_, str> {
if let Some(input_after_tilde) = input_str.strip_prefix('~') {
if input_after_tilde.is_empty() || input_after_tilde.starts_with('/') {
if let Some(hd) = home_dir() {
let result = format!("{}{}", hd.display(), input_after_tilde);
result.into()
} else {
input_str.into()
}
} else {
input_str.into()
}
} else {
input_str.into()
}
}
pub fn set_current_dir<P: AsRef<Path>>(path: P) -> Result<()> {
Ok(env::set_current_dir(path.as_ref())?)
}
pub fn update_zoxide<P: AsRef<Path>>(path: P) -> Result<()> {
let Some(is_logging) = IS_LOGGING.get() else {
return Ok(());
};
if *is_logging && is_in_path(ZOXIDE) {
execute_without_output(ZOXIDE, &["add", path.as_ref().to_string_lossy().as_ref()])?;
}
Ok(())
}
pub fn build_dest_path(source: &Path, dest: &Path) -> Option<PathBuf> {
let mut dest = dest.to_path_buf();
let filename = source.file_name()?;
dest.push(filename);
Some(dest)
}