Compare commits

...
Author SHA1 Message Date
galister 5be6b162ed fix headless mode dummy screen & restart propagation 2026-10-28 16:41:43 +09:00
galister 7061d6e5aa watch desktop entry changes 2026-10-28 16:25:42 +09:00
galister a65b33fed2 openvr: fix scrolling 2026-10-28 14:41:12 +09:00
galister 786903efd5 wvr_server: process matching tweaks 2026-10-28 14:37:24 +09:00
15 changed files with 552 additions and 260 deletions

No files matched your search

Generated
+29 -7
View File
@@ -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",
+15 -16
View File
@@ -21,7 +21,7 @@ use smithay::{
use wgui::log::LogErr;
use xkbcommon::xkb;
use crate::backend::wayvr::{ExternalProcessRequest, WayVRTask};
use crate::backend::wayvr::WayVRTask;
use super::{
ProcessWayVREnv,
@@ -32,6 +32,7 @@ use super::{
pub struct WayVRClient {
pub client: wayland_server::Client,
pub pid: u32,
pub process: process::ProcessHandle,
}
pub struct WayVRCompositor {
@@ -160,16 +161,17 @@ impl WayVRCompositor {
let process_env = get_wayvr_env_from_pid(credentials.pid)?;
// Find suitable auth key from the process list
for (_, process) in processes {
if let process::Process::Managed(process) = &process
for (handle, process) in processes.iter() {
if let process::Process::Managed(process) = process
&& let Some(auth_key) = &process_env.display_auth
{
// Find process with matching auth key
if process.auth_key.as_str() == auth_key {
// Add client
// Associate client now, instead of trying to match env later
self.add_client(WayVRClient {
client,
pid: credentials.pid as u32,
process: handle,
});
return Ok(());
}
@@ -178,18 +180,15 @@ impl WayVRCompositor {
// This is a new process which we didn't met before.
// Treat external processes exclusively (spawned by the user or external program)
log::warn!(
"External process ID {} connected to this Wayland server",
credentials.pid
);
self.state
.wayvr_tasks
.send(WayVRTask::NewExternalProcess(ExternalProcessRequest {
env: process_env,
client,
pid: credentials.pid as u32,
}));
let pid = credentials.pid as u32;
log::warn!("External process ID {pid} connected to this Wayland server");
let process =
processes.insert(process::Process::External(process::ExternalProcess { pid }));
self.add_client(WayVRClient {
client,
pid,
process,
});
Ok(())
}
+16 -35
View File
@@ -118,14 +118,6 @@ pub struct ProcessWayVREnv {
pub display_name: Option<String>, // Externally spawned process by a user script
}
#[derive(Clone)]
pub struct ExternalProcessRequest {
#[allow(dead_code)]
pub env: ProcessWayVREnv,
pub client: wayland_server::Client,
pub pid: u32,
}
#[derive(Clone)]
pub enum WayVRTask {
NewToplevel(ClientId, ToplevelSurface),
@@ -133,7 +125,6 @@ pub enum WayVRTask {
MinimizeRequest(ClientId, ToplevelSurface),
TitleChange(ClientId, ToplevelSurface),
VisibilityChange(window::WindowHandle, bool),
NewExternalProcess(ExternalProcessRequest),
ProcessTerminationRequest(process::ProcessHandle, KillSignal),
CloseWindowRequest(window::WindowHandle),
DndStarted {
@@ -173,10 +164,6 @@ pub enum PointerFocusTarget {
None,
}
pub enum TickTask {
NewExternalProcess(ExternalProcessRequest), // Call WayVRCompositor::add_client after receiving this message
}
const KEY_REPEAT_DELAY: i32 = 500;
const KEY_REPEAT_RATE: i32 = 50;
const WAYVR_SCREEN_RES: [i32; 2] = [2560, 1440];
@@ -325,26 +312,29 @@ impl WvrServerState {
#[allow(clippy::too_many_lines)]
#[allow(clippy::significant_drop_tightening)]
pub fn tick_events(app: &mut AppState) -> anyhow::Result<Vec<TickTask>> {
let mut tasks: Vec<TickTask> = Vec::new();
pub fn tick_events(app: &mut AppState) -> anyhow::Result<()> {
let Some(wvr_server) = app.wvr_server.as_mut() else {
return Ok(tasks);
return Ok(());
};
app.ipc_server.tick(&mut ipc_server::TickParams {
wvr_server,
input_state: &app.input_state,
tasks: &mut tasks,
signals: &app.wayvr_signals,
hid_wrapper: &mut app.hid_provider,
});
// Tick all child processes
// Keep launch records while wl client is attached
let attached_processes: HashSet<_> = wvr_server
.manager
.clients
.iter()
.map(|client| client.process)
.collect();
let mut to_remove: SmallVec<[process::ProcessHandle; 2]> = SmallVec::new();
for (handle, process) in &mut wvr_server.processes {
if !process.is_running() {
if !process.is_running() && !attached_processes.contains(&handle) {
to_remove.push(handle);
}
}
@@ -356,9 +346,6 @@ impl WvrServerState {
while let Some(task) = wvr_server.tasks.read() {
match task {
WayVRTask::NewExternalProcess(req) => {
tasks.push(TickTask::NewExternalProcess(req));
}
WayVRTask::NewToplevel(client_id, toplevel) => {
let toplevel = Rc::new(toplevel);
@@ -368,15 +355,14 @@ impl WvrServerState {
continue;
}
let Some(process_handle) =
process::find_by_pid(&wvr_server.processes, client.pid)
else {
let process_handle = client.process;
if !wvr_server.processes.contains_key(process_handle) {
log::error!(
"WayVR window creation failed: Unexpected process ID {}. It wasn't registered before.",
"WayVR window creation failed: Process for PID {} was removed after the client connected.",
client.pid
);
continue;
};
}
let output_bounds = wvr_server.manager.state.output_logical_size();
@@ -601,7 +587,7 @@ impl WvrServerState {
}
let Some(window_handle) = wvr_server.wm.find_window_handle(&toplevel)
else {
log::warn!("MinimizeRequest: Couldn't find matching window handle");
log::warn!("TitleChange: Couldn't find matching window handle");
continue;
};
if let Some(oid) = wvr_server.window_to_overlay.get(&window_handle) {
@@ -703,7 +689,7 @@ impl WvrServerState {
wvr_server.ticks += 1;
Ok(tasks)
Ok(())
}
pub fn config_changed(&mut self, config: &GeneralConfig) {
@@ -1307,11 +1293,6 @@ impl WvrServerState {
self.manager.state.set_clipboard_text(content);
}
pub fn add_external_process(&mut self, pid: u32) -> process::ProcessHandle {
self.processes
.insert(process::Process::External(process::ExternalProcess { pid }))
}
#[allow(clippy::too_many_arguments)]
pub fn spawn_process(
&mut self,
+13 -9
View File
@@ -87,12 +87,14 @@ impl Process {
impl Drop for WayVRProcess {
fn drop(&mut self) {
log::info!(
"Sending SIGTERM (graceful exit) to process {} ({})",
self.child.id(),
self.exec_path.as_str()
);
self.kill(libc::SIGTERM);
if self.is_running() {
log::info!(
"Sending SIGTERM (graceful exit) to process {} ({})",
self.child.id(),
self.exec_path.as_str()
);
self.kill(libc::SIGTERM);
}
}
}
@@ -127,9 +129,11 @@ impl WayVRProcess {
}
fn kill(&mut self, signal: i32) {
unsafe {
// Gracefully stop process
libc::kill(self.child.id() as i32, signal);
if self.is_running() {
unsafe {
// Gracefully stop process
libc::kill(self.child.id() as i32, signal);
}
}
}
+2 -23
View File
@@ -1,6 +1,6 @@
use std::sync::Arc;
use crate::backend::wayvr::{self, WayVRTask, WvrServerState};
use crate::backend::wayvr::{WayVRTask, WvrServerState};
use wayvr_ipc::packet_client::{HandsfreeMode, HandsfreeParams};
use wayvr_ipc::packet_server::PacketServer;
use wlx_common::config::HandsfreePointer;
@@ -15,24 +15,6 @@ use crate::{
windowing::{OverlaySelector, manager::OverlayWindowManager},
};
fn process_tick_tasks(
tick_tasks: Vec<backend::wayvr::TickTask>,
server_state: &mut WvrServerState,
) {
for tick_task in tick_tasks {
match tick_task {
backend::wayvr::TickTask::NewExternalProcess(request) => {
log::info!("Registering external process with PID {}", request.pid);
server_state.add_external_process(request.pid);
server_state.manager.add_client(wayvr::client::WayVRClient {
client: request.client,
pid: request.pid,
});
}
}
}
}
pub fn tick_events<O>(
app: &mut AppState,
overlays: &mut OverlayWindowManager<O>,
@@ -135,10 +117,7 @@ where
}
}
let tick_tasks = WvrServerState::tick_events(app)?;
if let Some(wayvr_server) = app.wvr_server.as_mut() {
process_tick_tasks(tick_tasks, wayvr_server);
}
WvrServerState::tick_events(app)?;
Ok(())
}
-2
View File
@@ -89,8 +89,6 @@ fn read_payload(conn: &mut local_socket::Stream, size: u32) -> anyhow::Result<Op
pub struct TickParams<'a> {
pub wvr_server: &'a mut WvrServerState,
#[allow(dead_code)]
pub tasks: &'a mut Vec<wayvr::TickTask>,
pub signals: &'a SyncEventQueue<WayVRSignal>,
pub input_state: &'a InputState,
pub hid_wrapper: &'a mut HidWrapper,
+38 -19
View File
@@ -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])?;
+6 -10
View File
@@ -223,26 +223,22 @@
},
{
"inputs" : {
"scroll" : {
"position" : {
"output" : "/actions/default/in/scroll"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "smooth"
},
"mode" : "joystick",
"parameters" : {},
"path" : "/user/hand/left/input/thumbstick"
},
{
"inputs" : {
"scroll" : {
"position" : {
"output" : "/actions/default/in/scroll"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "smooth"
},
"mode" : "joystick",
"parameters" : {},
"path" : "/user/hand/right/input/thumbstick"
},
{
+6 -10
View File
@@ -141,26 +141,22 @@
},
{
"inputs" : {
"scroll" : {
"position" : {
"output" : "/actions/default/in/scroll"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "smooth"
},
"mode" : "joystick",
"parameters" : {},
"path" : "/user/hand/left/input/thumbstick"
},
{
"inputs" : {
"scroll" : {
"position" : {
"output" : "/actions/default/in/scroll"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "smooth"
},
"mode" : "joystick",
"parameters" : {},
"path" : "/user/hand/right/input/thumbstick"
}
]
+6 -10
View File
@@ -107,26 +107,22 @@
},
{
"inputs" : {
"scroll" : {
"position" : {
"output" : "/actions/default/in/scroll"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "smooth"
},
"mode" : "joystick",
"parameters" : {},
"path" : "/user/hand/left/input/joystick"
},
{
"inputs" : {
"scroll" : {
"position" : {
"output" : "/actions/default/in/scroll"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "smooth"
},
"mode" : "joystick",
"parameters" : {},
"path" : "/user/hand/right/input/joystick"
}
]
+5 -2
View File
@@ -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);
}
+2
View File
@@ -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
View File
@@ -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(&params.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(&[], &params.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(&params.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(&params.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 &params.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(&params.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(&params, 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(&[], &params.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(&params.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(&params, 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);
}
+230
View File
@@ -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
}
}
+1
View File
@@ -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;