mirror of
https://github.com/wayvr-org/wayvr.git
synced 2026-10-28 13:02:53 +01:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5be6b162ed | ||
|
|
7061d6e5aa |
No files matched your search
Generated
+29
-7
@@ -1744,7 +1744,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2479,6 +2479,26 @@ version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306"
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cc00ea907cab49550b7da656f80ebb97be1b997d931fbcd28d39734e17ce592"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify-sys"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "input"
|
||||
version = "0.9.1"
|
||||
@@ -3215,7 +3235,7 @@ version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4125,7 +4145,7 @@ version = "0.14.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"heck 0.5.0",
|
||||
"itertools 0.14.0",
|
||||
"log",
|
||||
"multimap",
|
||||
@@ -4726,7 +4746,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.12.1",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5459,10 +5479,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.3.4",
|
||||
"getrandom 0.4.3",
|
||||
"once_cell",
|
||||
"rustix 1.1.4",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6422,7 +6442,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6860,6 +6880,8 @@ dependencies = [
|
||||
"identicons-svg",
|
||||
"idmap",
|
||||
"idmap-derive",
|
||||
"inotify",
|
||||
"libc",
|
||||
"log",
|
||||
"rodio",
|
||||
"serde",
|
||||
|
||||
+38
-19
@@ -180,43 +180,37 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
setup_signal_hooks()?;
|
||||
|
||||
let mut used_backend = None;
|
||||
auto_run(args, &mut used_backend);
|
||||
auto_run(&args, &mut used_backend);
|
||||
|
||||
if RESTART.load(Ordering::Relaxed) {
|
||||
log::warn!("Restarting...");
|
||||
let exe = std::env::current_exe()?;
|
||||
let mut args = vec!["--replace", "--show"];
|
||||
|
||||
match used_backend {
|
||||
Some(XrBackend::OpenXR) => args.push("--openxr"),
|
||||
Some(XrBackend::OpenVR) => args.push("--openvr"),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let _ = Command::new(exe).args(args).spawn();
|
||||
let _ = Command::new(exe)
|
||||
.args(restart_args(&args, used_backend))
|
||||
.spawn();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(unused_mut, clippy::similar_names)]
|
||||
fn auto_run(args: Args, used_backend: &mut Option<XrBackend>) {
|
||||
fn auto_run(args: &Args, out_backend: &mut Option<XrBackend>) {
|
||||
let mut tried_xr = false;
|
||||
let mut tried_vr = false;
|
||||
|
||||
loop {
|
||||
#[cfg(feature = "openxr")]
|
||||
if !args_get_openvr(&args) {
|
||||
if !args_get_openvr(args) {
|
||||
use crate::backend::{BackendError, openxr::openxr_run};
|
||||
tried_xr = true;
|
||||
match openxr_run(&args, init_run_params(args.no_audio)) {
|
||||
match openxr_run(args, init_run_params(args.no_audio)) {
|
||||
Ok(()) => {
|
||||
used_backend.replace(XrBackend::OpenXR);
|
||||
out_backend.replace(XrBackend::OpenXR);
|
||||
return;
|
||||
}
|
||||
Err(BackendError::NotSupported) => (),
|
||||
Err(e) => {
|
||||
used_backend.replace(XrBackend::OpenXR);
|
||||
out_backend.replace(XrBackend::OpenXR);
|
||||
log::error!("{e:?}");
|
||||
return;
|
||||
}
|
||||
@@ -224,17 +218,17 @@ fn auto_run(args: Args, used_backend: &mut Option<XrBackend>) {
|
||||
}
|
||||
|
||||
#[cfg(feature = "openvr")]
|
||||
if !args_get_openxr(&args) {
|
||||
if !args_get_openxr(args) {
|
||||
use crate::backend::{BackendError, openvr::openvr_run};
|
||||
tried_vr = true;
|
||||
match openvr_run(&args, init_run_params(args.no_audio)) {
|
||||
match openvr_run(args, init_run_params(args.no_audio)) {
|
||||
Ok(()) => {
|
||||
used_backend.replace(XrBackend::OpenVR);
|
||||
out_backend.replace(XrBackend::OpenVR);
|
||||
return;
|
||||
}
|
||||
Err(BackendError::NotSupported) => (),
|
||||
Err(e) => {
|
||||
used_backend.replace(XrBackend::OpenVR);
|
||||
out_backend.replace(XrBackend::OpenVR);
|
||||
log::error!("{e:?}");
|
||||
return;
|
||||
}
|
||||
@@ -303,6 +297,31 @@ const fn args_get_openxr(args: &Args) -> bool {
|
||||
ret
|
||||
}
|
||||
|
||||
fn restart_args(args: &Args, backend: Option<XrBackend>) -> Vec<&'static str> {
|
||||
let mut flags = vec!["--replace", "--show"];
|
||||
|
||||
match backend {
|
||||
Some(XrBackend::OpenXR) => flags.push("--openxr"),
|
||||
Some(XrBackend::OpenVR) => flags.push("--openvr"),
|
||||
None => {}
|
||||
}
|
||||
|
||||
if args.headless {
|
||||
flags.push("--headless");
|
||||
}
|
||||
if args.no_autostart {
|
||||
flags.push("--no-autostart");
|
||||
}
|
||||
if args.install {
|
||||
flags.push("--install");
|
||||
}
|
||||
if args.no_audio {
|
||||
flags.push("--no-audio");
|
||||
}
|
||||
|
||||
flags
|
||||
}
|
||||
|
||||
#[allow(clippy::single_match_else)]
|
||||
fn setup_signal_hooks() -> anyhow::Result<()> {
|
||||
let mut signals = Signals::new([SIGINT, SIGTERM, SIGUSR1])?;
|
||||
|
||||
@@ -9,6 +9,7 @@ use glam::{Affine3A, Vec3, Vec3A};
|
||||
use slotmap::{Key, SecondaryMap, SlotMap};
|
||||
use wgui::{i18n::Translation, log::LogErr};
|
||||
use wlx_common::{
|
||||
DesktopBackend,
|
||||
astr_containers::{AStrMap, AStrMapExt},
|
||||
config::SerializedWindowSet,
|
||||
overlays::{BackendAttrib, BackendAttribValue, ToastTopic},
|
||||
@@ -84,6 +85,8 @@ where
|
||||
};
|
||||
|
||||
if headless {
|
||||
app.screens.clear();
|
||||
app.feats.desktop_backend = DesktopBackend::Headless;
|
||||
log::info!("Running in headless mode; keyboard will be en-US");
|
||||
} else {
|
||||
// create one window set for each screen.
|
||||
@@ -92,12 +95,12 @@ where
|
||||
match create_screens(app) {
|
||||
Ok((data, backend)) => {
|
||||
app.feats.desktop_backend = backend;
|
||||
let last_idx = data.screens.len() - 1;
|
||||
let screen_count = data.screens.len();
|
||||
for (idx, (meta, mut config)) in data.screens.into_iter().enumerate() {
|
||||
config.show_on_spawn = true;
|
||||
me.add(OverlayWindowData::from_config(config), app);
|
||||
|
||||
if idx < last_idx {
|
||||
if idx + 1 < screen_count {
|
||||
me.sets.push(OverlayWindowSet::default());
|
||||
me.switch_to_set(app, Some(me.current_set.unwrap() + 1), false);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ chrono.workspace = true
|
||||
glam.workspace = true
|
||||
idmap = { workspace = true, features = ["serde"] }
|
||||
idmap-derive.workspace = true
|
||||
libc.workspace = true
|
||||
log.workspace = true
|
||||
serde = { workspace = true, features = ["rc"] }
|
||||
serde_json.workspace = true
|
||||
@@ -34,3 +35,4 @@ rodio = {
|
||||
}
|
||||
walkdir = "2.5.0"
|
||||
wgui = { path = "../wgui/" }
|
||||
inotify = { version = "0.11.5", default-features = false }
|
||||
+183
-117
@@ -3,14 +3,14 @@ use std::{
|
||||
ffi::OsStr,
|
||||
fmt::Debug,
|
||||
fs::exists,
|
||||
path::Path,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
thread::JoinHandle,
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use crate::{cache_dir, locale::Language};
|
||||
use crate::{cache_dir, desktop_watcher::DesktopWatcher, locale::Language};
|
||||
use anyhow::Context;
|
||||
use freedesktop_desktop_entry::DesktopEntry as FreedesktopDesktopEntry;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -57,14 +57,32 @@ const CATEGORY_TYPE_BLOCKLIST: [&str; 1] = ["ConsoleOnly"];
|
||||
struct DesktopFinderParams {
|
||||
size_preferences: Vec<&'static OsStr>,
|
||||
icon_folders: Vec<String>,
|
||||
app_folders: Vec<String>,
|
||||
app_folders: Vec<PathBuf>,
|
||||
langs: Vec<String>,
|
||||
}
|
||||
|
||||
struct CachedFileOwned {
|
||||
root_index: usize,
|
||||
entry: DesktopEntryOwned,
|
||||
}
|
||||
|
||||
struct CachedFile {
|
||||
root_index: usize,
|
||||
entry: Rc<DesktopEntry>,
|
||||
}
|
||||
|
||||
struct CacheBuild {
|
||||
files: HashMap<PathBuf, CachedFileOwned>,
|
||||
watcher: Option<DesktopWatcher>,
|
||||
}
|
||||
|
||||
pub struct DesktopFinder {
|
||||
params: Arc<DesktopFinderParams>,
|
||||
// include all desktop files, so that removing a higher-prio one will reveal the lower-prio underneath
|
||||
files: HashMap<PathBuf, CachedFile>,
|
||||
entry_cache: HashMap<String, Rc<DesktopEntry>>,
|
||||
bg_task: Option<JoinHandle<HashMap<String, DesktopEntryOwned>>>,
|
||||
watcher: Option<DesktopWatcher>,
|
||||
bg_task: Option<JoinHandle<CacheBuild>>,
|
||||
}
|
||||
|
||||
impl DesktopFinder {
|
||||
@@ -76,13 +94,8 @@ impl DesktopFinder {
|
||||
let mut icon_folders = vec![];
|
||||
|
||||
if let Some(data_home) = xdg.get_data_home() {
|
||||
app_folders.push(data_home.join("applications").to_string_lossy().to_string());
|
||||
app_folders.push(
|
||||
data_home
|
||||
.join("flatpak/exports/share/applications")
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
);
|
||||
app_folders.push(data_home.join("applications"));
|
||||
app_folders.push(data_home.join("flatpak/exports/share/applications"));
|
||||
icon_folders.push(data_home.join("icons").to_string_lossy().to_string());
|
||||
icon_folders.push(
|
||||
data_home
|
||||
@@ -97,7 +110,7 @@ impl DesktopFinder {
|
||||
|
||||
// /usr/share and such
|
||||
for data_dir in xdg.get_data_dirs() {
|
||||
app_folders.push(data_dir.join("applications").to_string_lossy().to_string());
|
||||
app_folders.push(data_dir.join("applications"));
|
||||
icon_folders.push(data_dir.join("icons").to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
@@ -122,126 +135,107 @@ impl DesktopFinder {
|
||||
size_preferences,
|
||||
langs,
|
||||
}),
|
||||
files: HashMap::new(),
|
||||
entry_cache: HashMap::new(),
|
||||
watcher: None,
|
||||
bg_task: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_cache(params: Arc<DesktopFinderParams>) -> HashMap<String, DesktopEntryOwned> {
|
||||
let start = Instant::now();
|
||||
fn read_single(params: &DesktopFinderParams, path: &Path) -> Option<DesktopEntryOwned> {
|
||||
let file_path = path.display();
|
||||
let file_name = path.file_name()?.to_string_lossy();
|
||||
let desktop = match FreedesktopDesktopEntry::from_path(path, Some(¶ms.langs)) {
|
||||
Ok(desktop) => desktop,
|
||||
Err(e) => {
|
||||
log::debug!("Failed to read .desktop file {}: {:?}, skipping", file_path, e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let mut known_files = HashSet::new();
|
||||
let mut entries = HashMap::<String, DesktopEntryOwned>::new();
|
||||
if desktop.groups.desktop_entry().is_none() {
|
||||
log::debug!("Failed to get [Desktop Entry] section for file {}, skipping", file_path);
|
||||
return None;
|
||||
}
|
||||
if desktop.only_show_in().is_some() || desktop.terminal() || desktop.no_display() || desktop.hidden() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let exec = desktop.exec()?;
|
||||
if CMD_BLOCKLIST.iter().any(|blocked| exec.contains(blocked)) {
|
||||
return None;
|
||||
}
|
||||
let exec_argv = desktop
|
||||
.parse_exec_with_uris(&[], ¶ms.langs)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|x| x.into_boxed_str())
|
||||
.collect::<Box<[_]>>();
|
||||
if exec_argv.is_empty() {
|
||||
log::debug!("Invalid Exec in desktop file {}, skipping", file_path);
|
||||
return None;
|
||||
}
|
||||
let app_name = desktop.name(¶ms.langs)?;
|
||||
let mut vec_categories = Vec::<Box<str>>::new();
|
||||
for cat in desktop.categories().unwrap_or_default() {
|
||||
if CATEGORY_TYPE_BLOCKLIST.contains(&cat) {
|
||||
return None;
|
||||
}
|
||||
if !cat.is_empty() {
|
||||
vec_categories.push(cat.into());
|
||||
}
|
||||
}
|
||||
|
||||
let icon_path = desktop
|
||||
.icon()
|
||||
.and_then(|icon_name| Self::find_icon(params, icon_name))
|
||||
.or_else(|| Self::create_icon(&file_name).ok());
|
||||
|
||||
Some(DesktopEntryOwned {
|
||||
app_id: desktop.id().into(),
|
||||
app_name: app_name.into(),
|
||||
exec_argv,
|
||||
icon_path: icon_path.map(|i| i.into_boxed_str()),
|
||||
categories: vec_categories.into_boxed_slice(),
|
||||
})
|
||||
}
|
||||
|
||||
fn build_cache_full(params: Arc<DesktopFinderParams>) -> CacheBuild {
|
||||
let start = Instant::now();
|
||||
// install watch before scan so edits during scan remain in inotify event queue
|
||||
let watcher = match DesktopWatcher::new(¶ms.app_folders) {
|
||||
Ok(watcher) => Some(watcher),
|
||||
Err(err) => {
|
||||
log::warn!("Could not watch .desktop files: {err}");
|
||||
None
|
||||
}
|
||||
};
|
||||
let mut files = HashMap::new();
|
||||
|
||||
let icons_folder = cache_dir::get_path("icons");
|
||||
if !std::fs::exists(&icons_folder).unwrap_or(false) {
|
||||
let _ = std::fs::create_dir(&icons_folder);
|
||||
}
|
||||
|
||||
for path in ¶ms.app_folders {
|
||||
log::debug!("Searching desktop entries in path {}", path);
|
||||
|
||||
'entries: for entry in WalkDir::new(path)
|
||||
for (root_index, root) in params.app_folders.iter().enumerate() {
|
||||
log::debug!("Searching desktop entries in path {}", root.display());
|
||||
for entry in WalkDir::new(root)
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| !e.file_type().is_dir())
|
||||
.filter_map(Result::ok)
|
||||
.filter(|entry| !entry.file_type().is_dir())
|
||||
{
|
||||
let Some(extension) = Path::new(entry.file_name()).extension() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if extension != "desktop" {
|
||||
continue; // ignore, go on
|
||||
}
|
||||
|
||||
let file_name = entry.file_name().to_string_lossy();
|
||||
let file_path = entry.path().display().to_string();
|
||||
|
||||
let desktop = match FreedesktopDesktopEntry::from_path(entry.path(), Some(¶ms.langs)) {
|
||||
Ok(desktop) => desktop,
|
||||
Err(e) => {
|
||||
log::debug!("Failed to read .desktop file {}: {:?}, skipping", file_path, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if desktop.groups.desktop_entry().is_none() {
|
||||
log::debug!("Failed to get [Desktop Entry] section for file {}, skipping", file_path);
|
||||
let path = entry.path();
|
||||
if path.extension() != Some(OsStr::new("desktop")) || files.contains_key(path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// prefer entries from earlier data directories
|
||||
let app_id = desktop.id();
|
||||
if known_files.contains(app_id) {
|
||||
continue;
|
||||
if let Some(parsed) = Self::read_single(¶ms, path) {
|
||||
files.insert(path.to_path_buf(), CachedFileOwned { root_index, entry: parsed });
|
||||
}
|
||||
|
||||
if desktop.only_show_in().is_some() || desktop.terminal() || desktop.no_display() || desktop.hidden() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(exec) = desktop.exec() else {
|
||||
log::debug!("Failed to get desktop entry Exec for file {}, skipping", file_path);
|
||||
continue;
|
||||
};
|
||||
|
||||
if CMD_BLOCKLIST.iter().any(|blocked| exec.contains(blocked)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let exec_argv = desktop
|
||||
.parse_exec_with_uris(&[], ¶ms.langs)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|x| x.into_boxed_str())
|
||||
.collect::<Box<[_]>>();
|
||||
|
||||
if exec_argv.is_empty() {
|
||||
log::debug!("Invalid Exec in desktop file {}, skipping", file_path);
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(app_name) = desktop.name(¶ms.langs) else {
|
||||
log::debug!(
|
||||
"Failed to get desktop entry application name for file {}, skipping",
|
||||
file_path
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
let icon_path = desktop
|
||||
.icon()
|
||||
.and_then(|icon_name| Self::find_icon(¶ms, icon_name))
|
||||
.or_else(|| Self::create_icon(&file_name).ok());
|
||||
|
||||
let mut vec_categories = Vec::<Box<str>>::new();
|
||||
for cat in desktop.categories().unwrap_or_default() {
|
||||
if CATEGORY_TYPE_BLOCKLIST.contains(&cat) {
|
||||
continue 'entries;
|
||||
}
|
||||
if !cat.is_empty() {
|
||||
vec_categories.push(cat.into());
|
||||
}
|
||||
}
|
||||
|
||||
known_files.insert(app_id.to_owned());
|
||||
|
||||
entries.insert(
|
||||
app_id.to_owned(),
|
||||
DesktopEntryOwned {
|
||||
app_id: app_id.into(),
|
||||
app_name: app_name.into(),
|
||||
exec_argv,
|
||||
icon_path: icon_path.map(|i| i.into_boxed_str()),
|
||||
categories: vec_categories.into_boxed_slice(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!("Desktop entry & icon scan took {:?}", start.elapsed());
|
||||
|
||||
entries
|
||||
CacheBuild { files, watcher }
|
||||
}
|
||||
|
||||
fn find_icon(params: &DesktopFinderParams, icon_name: &str) -> Option<String> {
|
||||
@@ -306,29 +300,101 @@ impl DesktopFinder {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(entries) = bg_task.join() else {
|
||||
let Ok(CacheBuild { files, watcher }) = bg_task.join() else {
|
||||
log::warn!("Desktop entry scan thread panicked");
|
||||
return;
|
||||
};
|
||||
|
||||
self.files = files
|
||||
.into_iter()
|
||||
.map(|(path, file)| {
|
||||
(path, CachedFile { root_index: file.root_index, entry: Rc::new(file.entry.into()) })
|
||||
})
|
||||
.collect();
|
||||
self.watcher = watcher;
|
||||
|
||||
let mut files: Vec<_> = self.files.iter().collect();
|
||||
files.sort_by(|(path_a, a), (path_b, b)| {
|
||||
a.root_index.cmp(&b.root_index).then_with(|| path_a.cmp(path_b))
|
||||
});
|
||||
self.entry_cache.clear();
|
||||
for (app_id, entry) in entries {
|
||||
self.entry_cache.insert(app_id, Rc::new(entry.into()));
|
||||
for (_, file) in files {
|
||||
self.entry_cache
|
||||
.entry(file.entry.app_id.to_string())
|
||||
.or_insert_with(|| Rc::clone(&file.entry));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_cached_entry(&self, app_id: &str) -> Option<&Rc<DesktopEntry>> {
|
||||
fn update_app_id(&mut self, app_id: &str) {
|
||||
let best = self
|
||||
.files
|
||||
.iter()
|
||||
.filter(|(_, file)| file.entry.app_id.as_ref() == app_id)
|
||||
.min_by(|(path_a, a), (path_b, b)| {
|
||||
a.root_index.cmp(&b.root_index).then_with(|| path_a.cmp(path_b))
|
||||
})
|
||||
.map(|(_, file)| Rc::clone(&file.entry));
|
||||
if let Some(best) = best {
|
||||
self.entry_cache.insert(app_id.to_owned(), best);
|
||||
} else {
|
||||
self.entry_cache.remove(app_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_file_changes(&mut self) {
|
||||
self.wait_for_entries();
|
||||
let Some(watcher) = self.watcher.as_mut() else {
|
||||
return;
|
||||
};
|
||||
let changes = watcher.collect_changes();
|
||||
if changes.overflowed {
|
||||
log::warn!("Desktop file inotify event queue overflowed; rebuilding cache");
|
||||
self.refresh();
|
||||
self.wait_for_entries();
|
||||
return;
|
||||
}
|
||||
|
||||
let mut affected_ids = HashSet::new();
|
||||
if !changes.removed_directories.is_empty() {
|
||||
self.files.retain(|path, file| {
|
||||
let removed = changes.removed_directories.iter().any(|directory| path.starts_with(directory));
|
||||
if removed {
|
||||
affected_ids.insert(file.entry.app_id.to_string());
|
||||
}
|
||||
!removed
|
||||
});
|
||||
}
|
||||
for path in changes.files {
|
||||
if let Some(previous) = self.files.remove(&path) {
|
||||
affected_ids.insert(previous.entry.app_id.to_string());
|
||||
}
|
||||
let root_index = self.params.app_folders.iter().position(|root| path.starts_with(root));
|
||||
if let Some(root_index) = root_index
|
||||
&& let Some(entry) = Self::read_single(&self.params, &path)
|
||||
{
|
||||
affected_ids.insert(entry.app_id.to_string());
|
||||
self.files.insert(path, CachedFile { root_index, entry: Rc::new(entry.into()) });
|
||||
}
|
||||
}
|
||||
for app_id in affected_ids {
|
||||
self.update_app_id(&app_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_cached_entry(&mut self, app_id: &str) -> Option<&Rc<DesktopEntry>> {
|
||||
self.sync_file_changes();
|
||||
self.entry_cache.get(app_id)
|
||||
}
|
||||
|
||||
pub fn find_entries(&mut self) -> HashMap<String, Rc<DesktopEntry>> {
|
||||
self.wait_for_entries();
|
||||
self.sync_file_changes();
|
||||
self.entry_cache.clone()
|
||||
}
|
||||
|
||||
pub fn refresh(&mut self) {
|
||||
let bg_task = std::thread::spawn({
|
||||
let params = self.params.clone();
|
||||
move || Self::build_cache(params)
|
||||
move || Self::build_cache_full(params)
|
||||
});
|
||||
self.bg_task = Some(bg_task);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
ffi::OsStr,
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use inotify::{EventMask, Inotify, WatchDescriptor, WatchMask};
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct DesktopChanges {
|
||||
pub files: HashSet<PathBuf>,
|
||||
pub removed_directories: HashSet<PathBuf>,
|
||||
pub overflowed: bool,
|
||||
}
|
||||
|
||||
pub(crate) struct DesktopWatcher {
|
||||
inotify: Inotify,
|
||||
roots: Vec<PathBuf>,
|
||||
by_wd: HashMap<WatchDescriptor, HashSet<PathBuf>>,
|
||||
by_path: HashMap<PathBuf, WatchDescriptor>,
|
||||
}
|
||||
|
||||
impl DesktopWatcher {
|
||||
pub fn new(roots: &[PathBuf]) -> io::Result<Self> {
|
||||
let mut watcher = Self {
|
||||
inotify: Inotify::init()?,
|
||||
roots: roots.to_vec(),
|
||||
by_wd: HashMap::new(),
|
||||
by_path: HashMap::new(),
|
||||
};
|
||||
|
||||
for root in roots {
|
||||
for ancestor in root.ancestors().skip(1) {
|
||||
if ancestor.is_dir() {
|
||||
watcher.watch_directory(ancestor);
|
||||
}
|
||||
}
|
||||
watcher.watch_tree(root, None);
|
||||
}
|
||||
|
||||
Ok(watcher)
|
||||
}
|
||||
|
||||
fn is_related_directory(&self, path: &Path) -> bool {
|
||||
self.roots
|
||||
.iter()
|
||||
.any(|root| path.starts_with(root) || root.starts_with(path))
|
||||
}
|
||||
|
||||
fn is_application_file(&self, path: &Path) -> bool {
|
||||
path.extension() == Some(OsStr::new("desktop"))
|
||||
&& self.roots.iter().any(|root| path.starts_with(root))
|
||||
}
|
||||
|
||||
fn watch_directory(&mut self, path: &Path) {
|
||||
if self.by_path.contains_key(path) || !path.is_dir() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mask = WatchMask::CREATE
|
||||
| WatchMask::DELETE
|
||||
| WatchMask::MOVED_FROM
|
||||
| WatchMask::MOVED_TO
|
||||
| WatchMask::MODIFY
|
||||
| WatchMask::CLOSE_WRITE
|
||||
| WatchMask::ATTRIB
|
||||
| WatchMask::DELETE_SELF
|
||||
| WatchMask::MOVE_SELF
|
||||
| WatchMask::ONLYDIR;
|
||||
|
||||
let wd = match self.inotify.watches().add(path, mask) {
|
||||
Ok(wd) => wd,
|
||||
Err(err) => {
|
||||
log::warn!(
|
||||
"Unable to watch desktop directory {}: {err}",
|
||||
path.display()
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
self.by_wd
|
||||
.entry(wd.clone())
|
||||
.or_default()
|
||||
.insert(path.to_path_buf());
|
||||
self.by_path.insert(path.to_path_buf(), wd);
|
||||
}
|
||||
|
||||
fn watch_tree(
|
||||
&mut self,
|
||||
directory: &Path,
|
||||
mut existing_files: Option<&mut HashSet<PathBuf>>,
|
||||
) {
|
||||
let mut pending = vec![directory.to_path_buf()];
|
||||
|
||||
while let Some(dir) = pending.pop() {
|
||||
if !self.is_related_directory(&dir) || !dir.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
self.watch_directory(&dir);
|
||||
|
||||
let children = match std::fs::read_dir(&dir) {
|
||||
Ok(children) => children,
|
||||
Err(err) => {
|
||||
log::debug!(
|
||||
"Cannot read desktop directory {}: {err}",
|
||||
dir.display()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
for child in children.flatten() {
|
||||
let path = child.path();
|
||||
|
||||
match child.file_type() {
|
||||
Ok(kind) if kind.is_dir() => {
|
||||
if self.is_related_directory(&path) {
|
||||
pending.push(path);
|
||||
}
|
||||
}
|
||||
Ok(_) => {
|
||||
if self.is_application_file(&path)
|
||||
&& let Some(files) = existing_files.as_deref_mut()
|
||||
{
|
||||
files.insert(path);
|
||||
}
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn unwatch_tree(&mut self, directory: &Path) {
|
||||
let descendants: Vec<_> = self
|
||||
.by_path
|
||||
.iter()
|
||||
.filter(|(path, _)| path.starts_with(directory))
|
||||
.map(|(path, wd)| (path.clone(), wd.clone()))
|
||||
.collect();
|
||||
|
||||
for (path, wd) in descendants {
|
||||
self.by_path.remove(&path);
|
||||
|
||||
if let Some(paths) = self.by_wd.get_mut(&wd) {
|
||||
paths.remove(&path);
|
||||
|
||||
if paths.is_empty() {
|
||||
self.by_wd.remove(&wd);
|
||||
let _ = self.inotify.watches().remove(wd);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn collect_changes(&mut self) -> DesktopChanges {
|
||||
let mut changes = DesktopChanges::default();
|
||||
let mut buffer = [0u8; 64 * 1024];
|
||||
|
||||
loop {
|
||||
let events = match self.inotify.read_events(&mut buffer) {
|
||||
Ok(events) => events,
|
||||
Err(err) if err.kind() == io::ErrorKind::WouldBlock => break,
|
||||
Err(err) if err.kind() == io::ErrorKind::Interrupted => continue,
|
||||
Err(err) => {
|
||||
log::warn!(
|
||||
"Unable to read desktop directory inotify events: {err}"
|
||||
);
|
||||
changes.overflowed = true;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
for event in events {
|
||||
if event.mask.contains(EventMask::Q_OVERFLOW) {
|
||||
changes.overflowed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if event.mask.contains(EventMask::IGNORED) {
|
||||
if let Some(paths) = self.by_wd.remove(&event.wd) {
|
||||
for path in paths {
|
||||
self.by_path.remove(&path);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(directories) = self.by_wd.get(&event.wd).cloned() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for directory in directories {
|
||||
let path = match event.name {
|
||||
Some(name) => directory.join(name),
|
||||
None => directory,
|
||||
};
|
||||
|
||||
if event.mask.intersects(
|
||||
EventMask::DELETE_SELF | EventMask::MOVE_SELF,
|
||||
) {
|
||||
changes.removed_directories.insert(path.clone());
|
||||
self.unwatch_tree(&path);
|
||||
} else if event.mask.contains(EventMask::ISDIR) {
|
||||
if event.mask.intersects(
|
||||
EventMask::DELETE | EventMask::MOVED_FROM,
|
||||
) {
|
||||
changes.removed_directories.insert(path.clone());
|
||||
self.unwatch_tree(&path);
|
||||
}
|
||||
|
||||
if event.mask.intersects(
|
||||
EventMask::CREATE | EventMask::MOVED_TO,
|
||||
) {
|
||||
self.watch_tree(&path, Some(&mut changes.files));
|
||||
}
|
||||
} else if self.is_application_file(&path) {
|
||||
changes.files.insert(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changes
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ pub mod config_io;
|
||||
pub mod dash_interface;
|
||||
pub mod dash_interface_emulated;
|
||||
pub mod data_dir;
|
||||
mod desktop_watcher;
|
||||
pub mod desktop_finder;
|
||||
pub mod locale;
|
||||
pub mod openxr_actions;
|
||||
|
||||
Reference in new issue
Block a user