use crate::common::{App, AppInfo, AppInfoContext, SearchPath};
use crate::utils::image::{RustImage, RustImageData};
use crate::AppTrait;
use anyhow::Result;
use ini::ini;
use serde_derive::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::io::{prelude::*, BufReader};
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
#[derive(Debug, PartialEq, Clone, Default, Eq, Hash, Serialize, Deserialize)]
pub struct AppIcon {
name: String,
path: PathBuf,
dimensions: Option<u16>,
}
pub fn brute_force_find_entry(
desktop_file_path: &Path,
entry_names: Vec<&str>,
) -> Result<Option<String>> {
let file = std::fs::File::open(desktop_file_path)?;
let reader = BufReader::new(file);
for line in reader.lines() {
match line {
Ok(line) => {
for entry_name in entry_names.iter() {
if line.starts_with(entry_name) {
let entry = line[entry_name.len() + 1..line.len()].trim();
return Ok(Some(entry.to_string()));
}
}
}
Err(_e) => {}
}
}
Ok(None)
}
pub fn brute_force_find_icon(desktop_file_path: &Path) -> Result<Option<String>> {
brute_force_find_entry(desktop_file_path, vec!["Icon", "icon"])
}
pub fn brute_force_find_exec(desktop_file_path: &Path) -> Result<Option<String>> {
brute_force_find_entry(desktop_file_path, vec!["Exec", "exec"])
}
fn clean_exec_path(exec: &str) -> String {
let cleaned: Vec<&str> = exec
.split_whitespace()
.take_while(|s| !s.starts_with('%')) .collect();
cleaned.join(" ")
}
pub fn parse_desktop_file(desktop_file_path: &Path) -> (App, bool) {
let mut app = App::default();
app.app_desktop_path = desktop_file_path.to_path_buf();
let desktop_file_path_str = desktop_file_path.to_str().unwrap();
let map = ini!(desktop_file_path_str);
let desktop_entry_exists = map.contains_key("desktop entry");
let mut display = true;
if desktop_entry_exists {
let desktop_entry = map["desktop entry"].clone();
if desktop_entry.contains_key("nodisplay") {
let nodisplay = desktop_entry["nodisplay"].clone();
match nodisplay {
Some(nodisplay) => {
if nodisplay == "true" {
display = false;
}
}
None => {}
}
}
let raw_exec = desktop_entry
.get("exec")
.cloned()
.or_else(|| brute_force_find_exec(&desktop_file_path).ok())
.flatten();
if let Some(exec) = raw_exec {
app.app_path_exe = Some(PathBuf::from(clean_exec_path(&exec)));
}
if desktop_entry.contains_key("icon") {
let icon = desktop_entry["icon"].clone();
app.icon_path = Some(PathBuf::from(icon.unwrap()));
} else {
match brute_force_find_icon(&desktop_file_path) {
Ok(icon) => {
app.icon_path = icon.map(|icon| PathBuf::from(icon));
}
Err(_) => {}
};
}
if desktop_entry.contains_key("name") {
let name = desktop_entry["name"].clone();
app.name = name.unwrap();
}
}
return (app, display);
}
pub fn get_default_search_paths() -> Vec<SearchPath> {
let mut search_paths = vec![];
let xdg_data_dirs = std::env::var("XDG_DATA_DIRS").unwrap_or("/usr/share".to_string());
let xdg_data_dirs: Vec<&str> = xdg_data_dirs.split(':').collect();
let home_dir = std::env::var("HOME").unwrap();
let home_path = PathBuf::from(home_dir);
let local_share_apps = home_path.join(".local/share/applications");
let mut default_search_paths = vec![
"/usr/share/applications",
"/usr/share/xsessions",
"/etc/xdg/autostart",
"/var/lib/snapd/desktop/applications",
local_share_apps.to_str().unwrap(),
];
for path in xdg_data_dirs {
default_search_paths.push(path);
}
for path in default_search_paths {
search_paths.push(SearchPath::new(PathBuf::from(path), 1));
}
search_paths
}
pub fn get_all_apps(extra_search_paths: &Vec<SearchPath>) -> Result<Vec<App>> {
let default_search_paths = get_default_search_paths();
let mut search_dirs: HashSet<SearchPath> = default_search_paths
.into_iter()
.filter(|dir| dir.path.exists())
.map(|dir| SearchPath::new(dir.path, dir.depth))
.collect();
for path in extra_search_paths {
search_dirs.insert(path.clone());
}
let icons_db = find_all_app_icons()?;
let mut apps: HashSet<App> = HashSet::new();
for dir in search_dirs {
if !dir.path.exists() {
continue;
}
for entry in WalkDir::new(dir.path.clone()).max_depth(dir.depth as usize) {
if entry.is_err() {
continue;
}
let entry = entry.unwrap();
let path = entry.path();
if path.extension().is_none() {
continue;
}
if path.extension().unwrap() == "desktop" && path.is_file() {
let (mut app, has_display) = parse_desktop_file(&path);
if !has_display {
continue;
}
if app.icon_path.is_some() {
let icon_path = app.icon_path.clone().unwrap();
if !icon_path.exists() {
if let Some(icons) = icons_db.get(icon_path.to_str().unwrap()) {
if let Some(icon) = icons.first() {
app.icon_path = Some(icon.path.clone());
}
} else {
app.icon_path = None;
}
}
}
apps.insert(app);
}
}
}
Ok(apps.iter().cloned().collect())
}
pub fn find_all_app_icons() -> Result<HashMap<String, Vec<AppIcon>>> {
let hicolor_path: PathBuf = PathBuf::from("/usr/share/icons");
let search_dirs = vec![hicolor_path];
let search_dirs: Vec<PathBuf> = search_dirs.into_iter().filter(|dir| dir.exists()).collect();
let mut set = HashSet::new();
for dir in search_dirs {
let dir = PathBuf::from(dir);
if !dir.exists() {
continue;
}
for entry in WalkDir::new(dir.clone()) {
if entry.is_err() {
continue;
}
let entry = entry.unwrap();
let path = entry.path();
match path.extension() {
Some(ext) => {
if ext == "png" {
let path_str = path.to_string_lossy().to_string();
let split: Vec<&str> = path_str.split("/").collect();
let dim_str = if split.len() < 6 {
None
} else {
split[5].split("x").last()
};
let dim = match dim_str {
Some(dim) => match dim.parse::<u16>() {
Ok(dim) => Some(dim),
Err(_) => None,
},
None => None,
};
set.insert(AppIcon {
name: path.file_name().unwrap().to_str().unwrap().to_string(),
path: path.to_path_buf(),
dimensions: dim, });
}
}
None => {
continue;
}
}
}
}
let mut map: HashMap<String, Vec<AppIcon>> = HashMap::new();
for icon in set {
let name = icon.name.clone();
let name = &name[0..name.len() - 4]; if map.contains_key(name) {
map.get_mut(name).unwrap().push(icon);
} else {
map.insert(name.to_string(), vec![icon]);
}
}
for (_, icons) in map.iter_mut() {
icons.sort_by(|a, b| {
if a.dimensions.is_none() && b.dimensions.is_none() {
return std::cmp::Ordering::Equal;
}
if a.dimensions.is_none() {
return std::cmp::Ordering::Greater;
}
if b.dimensions.is_none() {
return std::cmp::Ordering::Less;
}
b.dimensions.unwrap().cmp(&a.dimensions.unwrap())
});
}
Ok(map)
}
pub fn open_file_with(file_path: PathBuf, app: App) {
let exe_path = app.app_path_exe.unwrap();
let exec_path_str = exe_path.to_str().unwrap();
let file_path_str = file_path.to_str().unwrap();
let output = std::process::Command::new(exec_path_str)
.arg(file_path_str)
.output()
.expect("failed to execute process");
}
pub fn get_running_apps() -> Vec<App> {
todo!()
}
pub fn get_frontmost_application() -> Result<App> {
let output = std::process::Command::new("xprop")
.arg("-root")
.arg("_NET_ACTIVE_WINDOW")
.output()
.expect("failed to execute process");
let output = std::str::from_utf8(&output.stdout).unwrap();
let id = output.split_whitespace().last().unwrap();
let output = std::process::Command::new("xprop")
.arg("-id")
.arg(id)
.arg("WM_CLASS")
.output()
.expect("failed to execute process");
let output = std::str::from_utf8(&output.stdout).unwrap();
let app_name = output.split('"').nth(1).unwrap();
let apps = get_all_apps(&vec![])?;
for app in apps {
if app.name == app_name {
return Ok(app);
}
}
Err(anyhow::Error::msg("No matching app found".to_string()))
}
impl AppTrait for App {
fn load_icon(&self) -> Result<crate::utils::image::RustImageData> {
match &self.icon_path {
Some(icon_path) => {
let icon_path_str = icon_path
.to_str()
.ok_or_else(|| anyhow::anyhow!("Failed to convert icon path to string"))?;
let image = crate::utils::image::RustImageData::from_path(icon_path_str)
.map_err(|e| anyhow::anyhow!("Failed to get icon: {}", e))?;
Ok(image)
}
None => Err(anyhow::Error::msg("Icon path is None".to_string())),
}
}
fn from_path(path: &Path) -> Result<Self> {
let (app, _) = parse_desktop_file(path);
Ok(app)
}
}
pub fn load_icon(path: &Path) -> Result<RustImageData> {
if path.extension().unwrap() == "svg" {
return Err(anyhow::anyhow!("SVG files are not supported on Linux yet"));
}
let image = RustImageData::from_path(path.to_str().unwrap())
.map_err(|e| anyhow::anyhow!("Failed to get icon: {}", e))?;
Ok(image)
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use std::process::Command;
use std::{str, vec};
use super::*;
#[test]
fn test_clean_exec_path() {
assert_eq!(clean_exec_path("code %f").to_string(), "code");
assert_eq!(clean_exec_path("code %f %F").to_string(), "code");
assert_eq!(clean_exec_path("\"/home/hacker/.local/share/JetBrains/Toolbox/apps/intellij-idea-ultimate/bin/idea\" %u").to_string(), "\"/home/hacker/.local/share/JetBrains/Toolbox/apps/intellij-idea-ultimate/bin/idea\"");
}
#[test]
fn test_get_apps() {
let apps = get_all_apps(&vec![]).unwrap();
println!("Number of Apps: {}", apps.len());
assert!(apps.len() > 0);
for app in apps {
if app.name.to_lowercase().contains("code") {
println!("App: {:#?}", app);
}
}
}
#[test]
fn test_find_all_app_icons() {
let start = std::time::Instant::now();
let icons_icons = find_all_app_icons().unwrap();
let elapsed = start.elapsed();
assert!(icons_icons.len() > 0);
println!("Elapsed: {:?}", elapsed);
}
}