use std::collections::HashMap;
use std::fmt;
use std::io::stdout;
use std::path::{Path, PathBuf};
use anyhow::{anyhow, Context, Result};
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture},
execute,
terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType},
};
use serde_yaml_ng::from_reader;
use serde_yaml_ng::Value;
use strum::IntoEnumIterator;
use strum_macros::{Display, EnumIter, EnumString};
use crate::common::{
is_in_path, tilde, OPENER_AUDIO, OPENER_DEFAULT, OPENER_IMAGE, OPENER_OFFICE, OPENER_PATH,
OPENER_READABLE, OPENER_TEXT, OPENER_VECT, OPENER_VIDEO,
};
use crate::io::{execute, execute_in_shell};
use crate::log_info;
use crate::modes::{
decompress_7z, decompress_gz, decompress_xz, decompress_zip, extract_extension, Quote,
};
#[derive(Clone, Hash, Eq, PartialEq, Debug, Display, Default, EnumString, EnumIter)]
pub enum Extension {
#[default]
Audio,
Bitmap,
Office,
Readable,
Text,
Vectorial,
Video,
Zip,
Sevenz,
Gz,
Xz,
Iso,
Default,
}
impl Extension {
pub fn matcher(ext: &str) -> Self {
match ext {
"avif" | "bmp" | "gif" | "png" | "jpg" | "jpeg" | "pgm" | "ppm" | "webp" | "tiff" => {
Self::Bitmap
}
"svg" => Self::Vectorial,
"flac" | "m4a" | "wav" | "mp3" | "ogg" | "opus" => Self::Audio,
"avi" | "mkv" | "av1" | "m4v" | "ts" | "webm" | "mov" | "wmv" => Self::Video,
"build" | "c" | "cmake" | "conf" | "cpp" | "css" | "csv" | "cu" | "ebuild" | "eex"
| "env" | "ex" | "exs" | "go" | "h" | "hpp" | "hs" | "html" | "ini" | "java" | "js"
| "json" | "kt" | "lua" | "lock" | "log" | "md" | "micro" | "ninja" | "py" | "rkt"
| "rs" | "scss" | "sh" | "srt" | "svelte" | "tex" | "toml" | "tsx" | "txt" | "vim"
| "xml" | "yaml" | "yml" => Self::Text,
"odt" | "odf" | "ods" | "odp" | "doc" | "docx" | "xls" | "xlsx" | "ppt" | "pptx" => {
Self::Office
}
"pdf" | "epub" => Self::Readable,
"zip" => Self::Zip,
"xz" => Self::Xz,
"7z" | "7za" => Self::Sevenz,
"lzip" | "lzma" | "rar" | "tgz" | "gz" | "bzip2" => Self::Gz,
"iso" => {
log_info!("extension kind iso");
Self::Iso
}
_ => Self::Default,
}
}
pub fn icon(&self) -> &'static str {
match self {
Self::Zip | Self::Xz | Self::Gz => "ó°—„ ",
Self::Readable => "ï€ ",
Self::Iso => " ",
Self::Text => "ï’‰ ",
Self::Audio => " ",
Self::Office => "󰈙 ",
Self::Bitmap => " ",
Self::Vectorial => "󰫨 ",
Self::Video => " ",
_ => "î©» ",
}
}
}
macro_rules! open_file_with {
($self:ident, $key:expr, $variant:ident, $yaml:ident) => {
if let Some(opener) = Kind::from_yaml(&$yaml[$key]) {
$self
.association
.entry(Extension::$variant)
.and_modify(|entry| *entry = opener);
}
};
}
#[derive(Clone)]
pub struct Association {
association: HashMap<Extension, Kind>,
}
impl Default for Association {
fn default() -> Self {
Self {
#[rustfmt::skip]
association: HashMap::from([
(Extension::Default, Kind::external(OPENER_DEFAULT)),
(Extension::Audio, Kind::external(OPENER_AUDIO)),
(Extension::Bitmap, Kind::external(OPENER_IMAGE)),
(Extension::Office, Kind::external(OPENER_OFFICE)),
(Extension::Readable, Kind::external(OPENER_READABLE)),
(Extension::Text, Kind::external(OPENER_TEXT)),
(Extension::Vectorial, Kind::external(OPENER_VECT)),
(Extension::Video, Kind::external(OPENER_VIDEO)),
(Extension::Sevenz, Kind::Internal(Internal::Sevenz)),
(Extension::Gz, Kind::Internal(Internal::Gz)),
(Extension::Xz, Kind::Internal(Internal::Xz)),
(Extension::Zip, Kind::Internal(Internal::Zip)),
(Extension::Iso, Kind::Internal(Internal::NotSupported)),
]),
}
}
}
impl Association {
fn with_config(mut self, path: &str) -> Self {
let Some(yaml) = Self::parse_yaml_file(path) else {
return self;
};
self.update(yaml);
self.validate();
log_info!("updated opener from {path}");
self
}
fn parse_yaml_file(path: &str) -> Option<Value> {
let Ok(file) = std::fs::File::open(std::path::Path::new(&tilde(path).to_string())) else {
eprintln!("Couldn't find opener file at {path}. Using default.");
log_info!("Unable to open {path}. Using default opener");
return None;
};
let Ok(yaml) = from_reader::<std::fs::File, Value>(file) else {
eprintln!("Couldn't read the opener config file at {path}.
See https://raw.githubusercontent.com/qkzk/fm/master/config_files/fm/opener.yaml for an example. Using default.");
log_info!("Unable to parse openers from {path}. Using default opener");
return None;
};
Some(yaml)
}
fn update(&mut self, yaml: Value) {
open_file_with!(self, "audio", Audio, yaml);
open_file_with!(self, "bitmap_image", Bitmap, yaml);
open_file_with!(self, "libreoffice", Office, yaml);
open_file_with!(self, "readable", Readable, yaml);
open_file_with!(self, "text", Text, yaml);
open_file_with!(self, "default", Default, yaml);
open_file_with!(self, "vectorial_image", Vectorial, yaml);
open_file_with!(self, "video", Video, yaml);
}
fn validate(&mut self) {
self.association.retain(|_, info| info.is_valid());
}
pub fn as_map_of_strings(&self) -> HashMap<String, String> {
let mut associations: HashMap<String, String> = self
.association
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
for s in Extension::iter() {
let s = s.to_string();
associations.entry(s).or_insert_with(|| "".to_owned());
}
associations
}
fn associate(&self, ext: &str) -> Option<&Kind> {
self.association
.get(&Extension::matcher(&ext.to_lowercase()))
}
}
#[derive(Clone, Hash, PartialEq, Eq, Debug, Default)]
pub enum Internal {
#[default]
Zip,
Xz,
Gz,
Sevenz,
NotSupported,
}
impl Internal {
fn open(&self, path: &Path) -> Result<()> {
match self {
Self::Sevenz => decompress_7z(path),
Self::Zip => decompress_zip(path),
Self::Xz => decompress_xz(path),
Self::Gz => decompress_gz(path),
Self::NotSupported => Err(anyhow!("Can't be opened directly")),
}
}
}
#[derive(Clone, Hash, PartialEq, Eq, Debug)]
pub struct External(String, bool);
impl External {
fn new(opener_pair: (&str, bool)) -> Self {
Self(opener_pair.0.to_owned(), opener_pair.1)
}
fn program(&self) -> &str {
self.0.as_str()
}
pub fn use_term(&self) -> bool {
self.1
}
fn open(&self, paths: &[&str]) -> Result<()> {
let mut args: Vec<&str> = vec![self.program()];
args.extend(paths);
Self::without_term(args)?;
Ok(())
}
fn open_in_window<'a>(&'a self, path: &'a str) -> Result<()> {
let arg = format!(
"{program} {path}",
program = self.program(),
path = path.quote()?
);
Self::open_command_in_window(&[&arg])
}
fn open_multiple_in_window(&self, paths: &[PathBuf]) -> Result<()> {
let arg = paths
.iter()
.filter_map(|p| p.to_str().and_then(|s| s.quote().ok()))
.collect::<Vec<_>>()
.join(" ");
Self::open_command_in_window(&[&format!("{program} {arg}", program = self.program())])
}
fn without_term(mut args: Vec<&str>) -> Result<std::process::Child> {
if args.is_empty() {
return Err(anyhow!("args shouldn't be empty"));
}
let executable = args.remove(0);
execute(executable, &args)
}
pub fn open_shell_in_window() -> Result<()> {
Self::open_command_in_window(&[])?;
Ok(())
}
pub fn open_command_in_window(args: &[&str]) -> Result<()> {
disable_raw_mode()?;
execute!(stdout(), DisableMouseCapture, Clear(ClearType::All))?;
execute_in_shell(args)?;
enable_raw_mode()?;
execute!(std::io::stdout(), EnableMouseCapture, Clear(ClearType::All))?;
Ok(())
}
}
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub enum Kind {
Internal(Internal),
External(External),
}
impl Default for Kind {
fn default() -> Self {
Self::external(OPENER_DEFAULT)
}
}
impl Kind {
fn external(opener_pair: (&str, bool)) -> Self {
Self::External(External::new(opener_pair))
}
fn from_yaml(yaml: &Value) -> Option<Self> {
Some(Self::external((
yaml.get("opener")?.as_str()?,
yaml.get("use_term")?.as_bool()?,
)))
}
fn is_external(&self) -> bool {
matches!(self, Self::External(_))
}
fn is_valid(&self) -> bool {
!self.is_external() || is_in_path(self.external_program().unwrap_or_default().0)
}
fn external_program(&self) -> Result<(&str, bool)> {
let Self::External(External(program, use_term)) = self else {
return Err(anyhow!("not an external opener"));
};
Ok((program, *use_term))
}
}
impl fmt::Display for Kind {
fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result {
let s = if let Self::External(External(program, _)) = &self {
program
} else {
"internal"
};
write!(f, "{s}")
}
}
#[derive(Clone)]
pub struct Opener {
pub association: Association,
}
impl Default for Opener {
fn default() -> Self {
Self {
association: Association::default().with_config(OPENER_PATH),
}
}
}
impl Opener {
pub fn kind(&self, path: &Path) -> Option<&Kind> {
if path.is_dir() {
return None;
}
self.association.associate(extract_extension(path))
}
pub fn extension_use_term(&self, extension: &str) -> bool {
if let Some(Kind::External(external)) = self.association.associate(extension) {
external.use_term()
} else {
false
}
}
pub fn use_term(&self, path: &Path) -> bool {
match self.kind(path) {
None => false,
Some(Kind::Internal(_)) => false,
Some(Kind::External(external)) => external.use_term(),
}
}
pub fn open_single(&self, path: &Path) -> Result<()> {
match self.kind(path) {
Some(Kind::External(external)) => {
external.open(&[path.to_str().context("couldn't")?])
}
Some(Kind::Internal(internal)) => internal.open(path),
None => Err(anyhow!("{p} can't be opened", p = path.display())),
}
}
pub fn open_multiple(&self, openers: HashMap<External, Vec<PathBuf>>) -> Result<()> {
for (external, grouped_paths) in openers.iter() {
let _ = external.open(&Self::collect_paths_as_str(grouped_paths));
}
Ok(())
}
pub fn regroup_per_opener(&self, paths: &[PathBuf]) -> HashMap<External, Vec<PathBuf>> {
let mut openers: HashMap<External, Vec<PathBuf>> = HashMap::new();
for path in paths {
let Some(Kind::External(pair)) = self.kind(path) else {
continue;
};
openers
.entry(External(pair.0.to_owned(), pair.1).to_owned())
.and_modify(|files| files.push((*path).to_owned()))
.or_insert(vec![(*path).to_owned()]);
}
openers
}
fn collect_paths_as_str(paths: &[PathBuf]) -> Vec<&str> {
paths
.iter()
.filter(|fp| !fp.is_dir())
.filter_map(|fp| fp.to_str())
.collect()
}
pub fn open_in_window(&self, path: &Path) {
let Some(Kind::External(external)) = self.kind(path) else {
return;
};
if !external.use_term() {
return;
};
let _ = external.open_in_window(path.to_string_lossy().as_ref());
}
pub fn open_multiple_in_window(&self, openers: HashMap<External, Vec<PathBuf>>) -> Result<()> {
let (external, paths) = openers.iter().next().unwrap();
external.open_multiple_in_window(paths)
}
}