#![allow(clippy::mutable_key_type)]
use itertools::Itertools;
use mime::Mime;
use serde::Serialize;
use std::{
collections::{BTreeMap, HashMap, VecDeque},
fmt::Display,
io::Write,
str::FromStr,
};
use tabled::Tabled;
use tracing::{debug, info};
use crate::{
apps::{DesktopList, MimeApps, SystemApps},
cli::SelectorArgs,
common::{render_table, DesktopHandler, Handleable, Handler, UserPath},
config::config_file::ConfigFile,
error::{Error, Result},
};
#[derive(Default, Debug)]
pub struct Config {
mime_apps: MimeApps,
system_apps: SystemApps,
pub config: ConfigFile,
pub terminal_output: bool,
}
impl Config {
pub fn new(terminal_output: bool) -> Result<Self> {
let config = ConfigFile::load();
Ok(Self {
mime_apps: MimeApps::read()?,
system_apps: SystemApps::populate()?,
config: config?,
terminal_output,
})
}
#[mutants::skip] pub fn get_handler(&self, mime: &Mime) -> Result<DesktopHandler> {
match self.mime_apps.get_handler_from_user(mime, &self.config) {
Err(e) if matches!(e, Error::Cancelled) => Err(e),
h => h
.inspect(|_| {
info!("Match found for `{}` in mimeapps.list Default Associations", mime);
})
.or_else(|_|{
info!("No match for `{}` in mimeapps.list Default Associations", mime);
self.get_handler_from_added_associations(mime)}),
}
}
fn get_handler_from_added_associations(
&self,
mime: &Mime,
) -> Result<DesktopHandler> {
self.mime_apps
.added_associations
.get(mime)
.inspect(|_|
info!("Found matching entry for `{}` in mimeapps.list Added Associations", mime)
)
.map_or_else(
|| {
info!("No matching entries for `{}` in mimeapps.list Added Associations", mime);
self.system_apps.get_handler(mime)
},
|h| h.front().cloned(),
)
.ok_or_else(|| {
info!("No matching installed handlers found for `{}`", mime);
Error::NotFound(mime.to_string())
})
}
#[mutants::skip] pub fn launch_handler(&self, mime: &Mime, args: Vec<String>) -> Result<()> {
info!(
"Launching handler for `{}` with arguments: {:?}",
mime, args
);
self.get_handler(mime)?
.launch(self, args.into_iter().map(|a| a.to_string()).collect())?;
info!("Finished launching handler");
Ok(())
}
pub fn show_handler<W: Write>(
&self,
writer: &mut W,
mime: &Mime,
output_json: bool,
) -> Result<()> {
info!("Showing handler for `{}`", mime);
debug!("JSON output: {}", output_json);
let handler = self.get_handler(mime)?;
let output = if output_json {
let entry = handler.get_entry()?;
let cmd = entry.get_cmd(self, vec![])?;
(serde_json::json!( {
"handler": handler.to_string(),
"name": entry.name,
"cmd": cmd.0 + " " + &cmd.1.join(" "),
}))
.to_string()
} else {
handler.to_string()
};
writeln!(writer, "{output}")?;
info!("Finished showing handler");
Ok(())
}
pub fn set_handler(
&mut self,
mime: &Mime,
handler: &DesktopHandler,
) -> Result<()> {
info!("Setting `{}` as handler for `{}`", handler, mime);
self.mime_apps.set_handler(
mime,
handler,
self.config.expand_wildcards,
)?;
self.mime_apps.save()?;
info!("Finished setting handler");
Ok(())
}
pub fn add_handler(
&mut self,
mime: &Mime,
handler: &DesktopHandler,
) -> Result<()> {
info!("Adding {} to list of handlers for `{}`", handler, mime);
self.mime_apps.add_handler(
mime,
handler,
self.config.expand_wildcards,
)?;
self.mime_apps.save()?;
info!("Finished adding handler");
Ok(())
}
#[mutants::skip] pub fn open_paths(&self, paths: &[UserPath]) -> Result<()> {
fn format_paths<T: Display>(paths: &[T]) -> String {
format!(
"[{}]",
paths
.iter()
.format_with(", ", |str, f| f(&format!("\"{}\"", str)))
)
}
info!("Started opening paths: {}", format_paths(paths));
for (handler, paths) in
self.assign_files_to_handlers(paths)?.into_iter()
{
debug!("Opening {} using `{}`", format_paths(&paths), handler);
handler.open(self, paths)?;
}
info!("Finished opening paths");
Ok(())
}
fn assign_files_to_handlers(
&self,
paths: &[UserPath],
) -> Result<HashMap<Handler, Vec<String>>> {
let mut handlers: HashMap<Handler, Vec<String>> = HashMap::new();
for path in paths.iter() {
handlers
.entry(self.get_handler_from_path(path)?)
.or_default()
.push(path.to_string())
}
Ok(handlers)
}
fn get_handler_from_path(&self, path: &UserPath) -> Result<Handler> {
Ok(if let Ok(handler) = self.config.get_regex_handler(path) {
info!("Using regex handler for `{}`", path);
handler.into()
} else {
info!("No matching regex handlers found for `{}`", path);
self.get_handler(&path.get_mime()?)?.into()
})
}
pub fn terminal(&self) -> Result<String> {
self.get_handler(&Mime::from_str("x-scheme-handler/terminal")?)
.ok()
.and_then(|h| h.get_entry().ok())
.or_else(|| self.system_apps.terminal_emulator())
.map(|e| {
let mut exec = e.exec.to_owned();
if let Some(opts) = &self.config.term_exec_args {
exec.push(' ');
exec.push_str(opts)
}
exec
})
.ok_or(Error::NoTerminal)
}
pub fn print<W: Write>(
&self,
writer: &mut W,
detailed: bool,
output_json: bool,
) -> Result<()> {
info!("Printing associations");
debug!("JSON output: {}", output_json);
let mimeapps_table = MimeAppsTable::new(
&self.mime_apps,
&self.system_apps,
self.terminal_output,
);
if detailed {
if output_json {
writeln!(writer, "{}", serde_json::to_string(&mimeapps_table)?)?
} else {
writeln!(writer, "Default Apps")?;
writeln!(
writer,
"{}",
render_table(
&mimeapps_table.default_apps,
self.terminal_output
)
)?;
if !self.mime_apps.added_associations.is_empty() {
writeln!(writer, "Added associations")?;
writeln!(
writer,
"{}",
render_table(
&mimeapps_table.added_associations,
self.terminal_output
)
)?;
}
writeln!(writer, "System Apps")?;
writeln!(
writer,
"{}",
render_table(
&mimeapps_table.system_apps,
self.terminal_output
)
)?
}
} else if output_json {
writeln!(
writer,
"{}",
serde_json::to_string(&mimeapps_table.default_apps)?
)?
} else {
writeln!(
writer,
"{}",
render_table(
&mimeapps_table.default_apps,
self.terminal_output
)
)?
}
info!("Finished printing associations");
Ok(())
}
pub fn unset_handler(&mut self, mime: &Mime) -> Result<()> {
info!("Unsetting handler for `{}`", mime);
if self.mime_apps.unset_handler(mime).is_some() {
self.mime_apps.save()?
}
info!("Finished unsetting handler");
Ok(())
}
pub fn remove_handler(
&mut self,
mime: &Mime,
handler: &DesktopHandler,
) -> Result<()> {
info!(
"Removing `{}` from list of handlers for `{}`",
handler, mime
);
if self.mime_apps.remove_handler(mime, handler).is_some() {
self.mime_apps.save()?
}
info!("Finished removing handler");
Ok(())
}
pub fn override_selector(&mut self, selector_args: SelectorArgs) {
self.config.override_selector(selector_args);
}
}
#[derive(PartialEq, Eq, PartialOrd, Ord, Tabled, Serialize)]
struct MimeAppsEntry {
mime: String,
#[tabled(display_with("Self::display_handlers", self))]
handlers: Vec<String>,
#[tabled(skip)]
#[serde(skip_serializing)]
separator: String,
}
impl MimeAppsEntry {
fn new(
mime: &Mime,
handlers: &VecDeque<DesktopHandler>,
separator: &str,
) -> Self {
Self {
mime: mime.to_string(),
handlers: handlers
.iter()
.map(|x| x.to_string())
.collect::<Vec<String>>(),
separator: separator.to_string(),
}
}
fn display_handlers(&self) -> String {
self.handlers.join(&self.separator)
}
}
#[derive(Serialize)]
struct MimeAppsTable {
added_associations: Vec<MimeAppsEntry>,
default_apps: Vec<MimeAppsEntry>,
system_apps: Vec<MimeAppsEntry>,
}
impl MimeAppsTable {
fn new(
mimeapps: &MimeApps,
system_apps: &SystemApps,
terminal_output: bool,
) -> Self {
let separator = if terminal_output { ",\n" } else { ", " };
let to_entries =
|map: &BTreeMap<Mime, DesktopList>| -> Vec<MimeAppsEntry> {
let mut rows = map
.iter()
.map(|(mime, handlers)| {
MimeAppsEntry::new(mime, handlers, separator)
})
.collect::<Vec<_>>();
rows.sort_unstable();
rows
};
Self {
added_associations: to_entries(&mimeapps.added_associations),
default_apps: to_entries(&mimeapps.default_apps),
system_apps: to_entries(&system_apps.associations),
}
}
}
#[cfg(test)]
mod tests {
use std::io::Read;
use crate::testing;
use super::*;
use similar_asserts::assert_eq;
crate::logs_snapshot_test!(wildcard_mimes, {
let mut config = Config::default();
config.add_handler(
&Mime::from_str("video/*")?,
&DesktopHandler::assume_valid("mpv.desktop".into()),
)?;
config.add_handler(
&Mime::from_str("video/webm")?,
&DesktopHandler::assume_valid("brave.desktop".into()),
)?;
assert_eq!(
config
.get_handler(&Mime::from_str("video/mp4")?)?
.to_string(),
"mpv.desktop"
);
assert_eq!(
config
.get_handler(&Mime::from_str("video/asdf")?)?
.to_string(),
"mpv.desktop"
);
assert_eq!(
config
.get_handler(&Mime::from_str("video/webm")?)?
.to_string(),
"brave.desktop"
);
});
#[test]
fn complex_wildcard_mimes() -> Result<()> {
let mut config = Config::default();
config.add_handler(
&Mime::from_str("application/vnd.oasis.opendocument.*")?,
&DesktopHandler::assume_valid("startcenter.desktop".into()),
)?;
config.add_handler(
&Mime::from_str("application/vnd.openxmlformats-officedocument.*")?,
&DesktopHandler::assume_valid("startcenter.desktop".into()),
)?;
assert_eq!(
config
.get_handler(&Mime::from_str(
"application/vnd.oasis.opendocument.text"
)?,)?
.to_string(),
"startcenter.desktop"
);
assert_eq!(
config
.get_handler(
&Mime::from_str("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")?,
)?
.to_string(),
"startcenter.desktop"
);
Ok(())
}
fn print_handlers_test<W: Write>(
buffer: &mut W,
detailed: bool,
output_json: bool,
terminal_output: bool,
) -> Result<()> {
let mut config = Config::default();
config.add_handler(
&Mime::from_str("video/mp4")?,
&DesktopHandler::assume_valid("mpv.desktop".into()),
)?;
config.add_handler(
&Mime::from_str("video/asdf")?,
&DesktopHandler::assume_valid("mpv.desktop".into()),
)?;
config.add_handler(
&Mime::from_str("video/webm")?,
&DesktopHandler::assume_valid("brave.desktop".into()),
)?;
config.add_handler(
&mime::TEXT_PLAIN,
&DesktopHandler::assume_valid("helix.desktop".into()),
)?;
config.add_handler(
&mime::TEXT_PLAIN,
&DesktopHandler::assume_valid("nvim.desktop".into()),
)?;
config.add_handler(
&mime::TEXT_PLAIN,
&DesktopHandler::assume_valid("kakoune.desktop".into()),
)?;
config.add_handler(
&Mime::from_str("application/vnd.oasis.opendocument.*")?,
&DesktopHandler::assume_valid("startcenter.desktop".into()),
)?;
config.add_handler(
&Mime::from_str("application/vnd.openxmlformats-officedocument.*")?,
&DesktopHandler::assume_valid("startcenter.desktop".into()),
)?;
config
.mime_apps
.added_associations
.entry(Mime::from_str("x-scheme-handler/terminal")?)
.or_default()
.push_back(DesktopHandler::assume_valid(
"org.wezfurlong.wezterm.desktop".into(),
));
config.terminal_output = terminal_output;
config.print(buffer, detailed, output_json)?;
Ok(())
}
crate::logs_snapshot_test!(print_handlers_default, {
let mut buffer = Vec::new();
print_handlers_test(&mut buffer, false, false, true)?;
insta::assert_snapshot!(String::from_utf8(buffer)?);
});
crate::logs_snapshot_test!(print_handlers_piped, {
let mut buffer = Vec::new();
print_handlers_test(&mut buffer, false, false, false)?;
insta::assert_snapshot!(String::from_utf8(buffer)?);
});
crate::logs_snapshot_test!(print_handlers_detailed, {
let mut buffer = Vec::new();
print_handlers_test(&mut buffer, true, false, true)?;
insta::assert_snapshot!(String::from_utf8(buffer)?);
});
crate::logs_snapshot_test!(print_handlers_detailed_piped, {
let mut buffer = Vec::new();
print_handlers_test(&mut buffer, true, false, false)?;
insta::assert_snapshot!(String::from_utf8(buffer)?);
});
crate::logs_snapshot_test!(print_handlers_json, {
let mut buffer = Vec::new();
print_handlers_test(&mut buffer, false, true, true)?;
insta::assert_snapshot!(String::from_utf8(buffer)?);
let mut buffer = Vec::new();
print_handlers_test(&mut buffer, false, true, false)?;
insta::assert_snapshot!(String::from_utf8(buffer)?);
});
crate::logs_snapshot_test!(print_handlers_detailed_json, {
let mut buffer = Vec::new();
print_handlers_test(&mut buffer, true, true, false)?;
insta::assert_snapshot!(String::from_utf8(buffer)?);
let mut buffer = Vec::new();
print_handlers_test(&mut buffer, true, true, false)?;
insta::assert_snapshot!(String::from_utf8(buffer)?);
});
crate::logs_snapshot_test!(terminal_command_set, {
let mut config = Config::default();
config.add_handler(
&Mime::from_str("x-scheme-handler/terminal")?,
&DesktopHandler::from_str(
"tests/assets/org.wezfurlong.wezterm.desktop",
)?,
)?;
assert_eq!(config.terminal()?, "wezterm start --cwd . -e");
});
crate::logs_snapshot_test!(terminal_command_fallback, {
let mut config = Config::default();
config
.system_apps
.add_unassociated(DesktopHandler::from_str(
"tests/assets/org.wezfurlong.wezterm.desktop",
)?);
assert_eq!(config.terminal()?, "wezterm start --cwd . -e");
});
fn test_show_handler<W: Write>(
writer: &mut W,
output_json: bool,
terminal_output: bool,
) -> Result<()> {
let mut config = Config {
terminal_output,
..Default::default()
};
config.add_handler(
&mime::TEXT_PLAIN,
&DesktopHandler::from_str("tests/assets/Helix.desktop")?,
)?;
config.add_handler(
&Mime::from_str("x-scheme-handler/terminal")?,
&DesktopHandler::from_str(
"tests/assets/org.wezfurlong.wezterm.desktop",
)?,
)?;
config.show_handler(writer, &mime::TEXT_PLAIN, output_json)?;
Ok(())
}
crate::logs_snapshot_test!(show_handler, {
let mut buffer = Vec::new();
test_show_handler(&mut buffer, false, false)?;
insta::assert_snapshot!(String::from_utf8(buffer)?);
});
crate::logs_snapshot_test!(show_handler_json, {
let mut buffer = Vec::new();
test_show_handler(&mut buffer, true, false)?;
insta::assert_snapshot!(String::from_utf8(buffer)?);
});
crate::logs_snapshot_test!(show_handler_terminal, {
let mut buffer = Vec::new();
test_show_handler(&mut buffer, false, true)?;
insta::assert_snapshot!(String::from_utf8(buffer)?);
});
crate::logs_snapshot_test!(show_handler_json_terminal, {
let mut buffer = Vec::new();
test_show_handler(&mut buffer, true, true)?;
insta::assert_snapshot!(String::from_utf8(buffer)?);
});
fn test_add_handlers(config: &mut Config) -> Result<()> {
config.add_handler(
&mime::TEXT_PLAIN,
&DesktopHandler::assume_valid("Helix.desktop".into()),
)?;
assert_eq!(
config.get_handler(&mime::TEXT_PLAIN)?.to_string(),
"Helix.desktop"
);
config.add_handler(
&mime::TEXT_PLAIN,
&DesktopHandler::assume_valid("nvim.desktop".into()),
)?;
assert_eq!(
config.get_handler(&mime::TEXT_PLAIN)?.to_string(),
"Helix.desktop"
);
Ok(())
}
fn test_remove_handlers(config: &mut Config) -> Result<()> {
config.remove_handler(
&mime::TEXT_PLAIN,
&DesktopHandler::assume_valid("Helix.desktop".into()),
)?;
assert_eq!(
config.get_handler(&mime::TEXT_PLAIN)?.to_string(),
"nvim.desktop"
);
config.remove_handler(
&mime::TEXT_PLAIN,
&DesktopHandler::assume_valid("nvim.desktop".into()),
)?;
assert!(config.get_handler(&mime::TEXT_PLAIN).is_err());
Ok(())
}
fn test_set_handlers(config: &mut Config) -> Result<()> {
config.set_handler(
&mime::TEXT_PLAIN,
&DesktopHandler::assume_valid("Helix.desktop".into()),
)?;
assert_eq!(
config.get_handler(&mime::TEXT_PLAIN)?.to_string(),
"Helix.desktop"
);
config.set_handler(
&mime::TEXT_PLAIN,
&DesktopHandler::assume_valid("nvim.desktop".into()),
)?;
assert_eq!(
config.get_handler(&mime::TEXT_PLAIN)?.to_string(),
"nvim.desktop"
);
Ok(())
}
fn test_unset_handlers(config: &mut Config) -> Result<()> {
config.unset_handler(&mime::TEXT_PLAIN)?;
assert!(config.get_handler(&mime::TEXT_PLAIN).is_err());
Ok(())
}
crate::logs_snapshot_test!(add_and_remove_handlers, {
let mut config = Config::default();
test_add_handlers(&mut config)?;
test_remove_handlers(&mut config)?;
});
crate::logs_snapshot_test!(set_and_unset_handlers, {
let mut config = Config::default();
test_set_handlers(&mut config)?;
test_unset_handlers(&mut config)?;
});
crate::logs_snapshot_test!(add_and_unset_handlers, {
let mut config = Config::default();
test_add_handlers(&mut config)?;
test_unset_handlers(&mut config)?;
});
crate::logs_snapshot_test!(set_and_remove_handlers, {
let mut config = Config::default();
test_set_handlers(&mut config)?;
test_remove_handlers(&mut config)?;
});
crate::logs_snapshot_test!(override_selector, {
let mut config = Config::default();
assert_eq!(config.config.selector, "rofi -dmenu -i -p 'Open With: '");
assert_eq!(config.config.enable_selector, false);
config.override_selector(SelectorArgs {
selector: Some("fzf".to_string()),
enable_selector: Some(true),
});
assert_eq!(config.config.selector, "fzf");
assert_eq!(config.config.enable_selector, true);
config.override_selector(SelectorArgs {
selector: Some("fuzzel --dmenu --prompt='Open With: '".to_string()),
enable_selector: Some(false),
});
assert_eq!(
config.config.selector,
"fuzzel --dmenu --prompt='Open With: '"
);
assert_eq!(config.config.enable_selector, false);
});
crate::logs_snapshot_test!(dont_override_selector, {
let mut config = Config::default();
assert_eq!(config.config.selector, "rofi -dmenu -i -p 'Open With: '");
assert_eq!(config.config.enable_selector, false);
config.override_selector(SelectorArgs {
selector: None,
enable_selector: None,
});
assert_eq!(config.config.selector, "rofi -dmenu -i -p 'Open With: '");
assert_eq!(config.config.enable_selector, false);
config.override_selector(SelectorArgs {
selector: None,
enable_selector: Some(false),
});
assert_eq!(config.config.selector, "rofi -dmenu -i -p 'Open With: '");
assert_eq!(config.config.enable_selector, false);
config.config.enable_selector = true;
config.override_selector(SelectorArgs {
selector: None,
enable_selector: Some(true),
});
assert_eq!(config.config.selector, "rofi -dmenu -i -p 'Open With: '");
assert_eq!(config.config.enable_selector, true);
config.override_selector(SelectorArgs {
selector: None,
enable_selector: None,
});
assert_eq!(config.config.selector, "rofi -dmenu -i -p 'Open With: '");
assert_eq!(config.config.enable_selector, true);
});
crate::logs_snapshot_test!(properly_assign_files_to_handlers, {
let mut config = Config::default();
config.add_handler(
&Mime::from_str("image/png")?,
&DesktopHandler::assume_valid("swayimg.desktop".into()),
)?;
config.add_handler(
&Mime::from_str("application/pdf")?,
&DesktopHandler::assume_valid("mupdf.desktop".into()),
)?;
let mut expected_handlers = HashMap::new();
expected_handlers
.insert(Handler::new("swayimg.desktop"), vec!["a.png".to_owned()]);
expected_handlers
.insert(Handler::new("mupdf.desktop"), vec!["a.pdf".to_owned()]);
assert_eq!(
config.assign_files_to_handlers(&[
UserPath::from_str("a.png")?,
UserPath::from_str("a.pdf")?
])?,
expected_handlers
);
assert_eq!(
config.assign_files_to_handlers(&[
UserPath::from_str("a.pdf")?,
UserPath::from_str("a.png")?
])?,
expected_handlers
);
let mut expected_handlers = HashMap::new();
expected_handlers.insert(
Handler::new("swayimg.desktop"),
vec!["a.png".to_owned(), "b.png".to_owned()],
);
expected_handlers
.insert(Handler::new("mupdf.desktop"), vec!["a.pdf".to_owned()]);
assert_eq!(
config.assign_files_to_handlers(&[
UserPath::from_str("a.png")?,
UserPath::from_str("b.png")?,
UserPath::from_str("a.pdf")?
])?,
expected_handlers
);
assert_eq!(
config.assign_files_to_handlers(&[
UserPath::from_str("a.pdf")?,
UserPath::from_str("a.png")?,
UserPath::from_str("b.png")?
])?,
expected_handlers
);
});
}