bar app icons & tooltips

This commit is contained in:
galister
2026-01-05 15:45:19 +09:00
parent b86525d65d
commit b56aa1a8de
30 changed files with 291 additions and 129 deletions

View File

@@ -22,3 +22,7 @@ rodio = { version = "0.21.1", default-features = false, features = [
"mp3",
"hound",
] }
glob = "0.3.3"
walkdir = "2.5.0"
rust-ini = "0.21.3"
identicons-svg = "0.1.0"

View File

@@ -9,33 +9,3 @@ pub enum LeftRight {
Left,
Right,
}
pub trait LogErr {
fn log_err(self) -> Self;
fn log_warn(self) -> Self;
}
impl<T, E> LogErr for Result<T, E>
where
E: Debug + Send + Sync + 'static,
{
fn log_warn(self) -> Result<T, E> {
match self {
Ok(ok) => Ok(ok),
Err(error) => {
log::warn!("{error:?}");
Err(error)
}
}
}
fn log_err(self) -> Result<T, E> {
match self {
Ok(ok) => Ok(ok),
Err(error) => {
log::error!("{error:?}");
Err(error)
}
}
}
}

View File

@@ -3,6 +3,8 @@ use wayvr_ipc::{
packet_server::{WvrProcess, WvrProcessHandle, WvrWindow, WvrWindowHandle},
};
use crate::desktop_finder::DesktopFinder;
pub trait DashInterface<T> {
fn window_list(&mut self, data: &mut T) -> anyhow::Result<Vec<WvrWindow>>;
fn window_set_visible(&mut self, data: &mut T, handle: WvrWindowHandle, visible: bool) -> anyhow::Result<()>;
@@ -12,6 +14,7 @@ pub trait DashInterface<T> {
fn process_list(&mut self, data: &mut T) -> anyhow::Result<Vec<WvrProcess>>;
fn process_terminate(&mut self, data: &mut T, handle: WvrProcessHandle) -> anyhow::Result<()>;
fn recenter_playspace(&mut self, data: &mut T) -> anyhow::Result<()>;
fn desktop_finder<'a>(&'a mut self, data: &'a mut T) -> &'a mut DesktopFinder;
}
pub type BoxDashInterface<T> = Box<dyn DashInterface<T>>;

View File

@@ -3,7 +3,7 @@ use wayvr_ipc::{
packet_server::{WvrProcess, WvrProcessHandle, WvrWindow, WvrWindowHandle},
};
use crate::{dash_interface::DashInterface, gen_id};
use crate::{dash_interface::DashInterface, desktop_finder::DesktopFinder, gen_id};
#[derive(Debug)]
pub struct EmuProcess {
@@ -54,6 +54,7 @@ gen_id!(EmuProcessVec, EmuProcess, EmuProcessCell, EmuProcessHandle);
pub struct DashInterfaceEmulated {
processes: EmuProcessVec,
windows: EmuWindowVec,
desktop_finder: DesktopFinder,
}
impl DashInterfaceEmulated {
@@ -69,7 +70,14 @@ impl DashInterfaceEmulated {
visible: true,
});
Self { processes, windows }
let mut desktop_finder = DesktopFinder::new();
desktop_finder.refresh();
Self {
processes,
windows,
desktop_finder,
}
}
}
@@ -157,4 +165,8 @@ impl DashInterface<()> for DashInterfaceEmulated {
// stub!
Ok(())
}
fn desktop_finder<'a>(&'a mut self, _: &'a mut ()) -> &'a mut DesktopFinder {
&mut self.desktop_finder
}
}

View File

@@ -0,0 +1,322 @@
use std::{
collections::{HashMap, HashSet}, ffi::OsStr, fmt::Debug, fs::exists, path::Path, rc::Rc, sync::Arc, thread::JoinHandle,
time::Instant,
};
use ini::Ini;
use serde::{Deserialize, Serialize};
use walkdir::WalkDir;
use crate::cache_dir;
struct DesktopEntryOwned {
exec_path: String,
exec_args: String,
app_name: String,
icon_path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DesktopEntry {
pub exec_path: Rc<str>,
pub exec_args: Rc<str>,
pub app_name: Rc<str>,
pub icon_path: Option<Rc<str>>,
}
impl From<DesktopEntryOwned> for DesktopEntry {
fn from(value: DesktopEntryOwned) -> Self {
Self {
exec_path: value.exec_path.into(),
exec_args: value.exec_args.into(),
app_name: value.app_name.into(),
icon_path: value.icon_path.map(|x| x.into()),
}
}
}
const CMD_BLOCKLIST: [&str; 3] = [
"lsp-plugins", // LSP Plugins collection. They clutter the application list a lot
"vrmonitor",
"vrurlhandler",
];
const CATEGORY_TYPE_BLOCKLIST: [&str; 1] = ["ConsoleOnly"];
struct DesktopFinderParams {
size_preferences: Vec<&'static OsStr>,
icon_folders: Vec<String>,
app_folders: Vec<String>,
}
pub struct DesktopFinder {
params: Arc<DesktopFinderParams>,
entry_cache: HashMap<String,DesktopEntry>,
bg_task: Option<JoinHandle<HashMap<String,DesktopEntryOwned>>>,
}
impl DesktopFinder {
pub fn new() -> Self {
let xdg = xdg::BaseDirectories::new();
let mut app_folders = vec![];
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(),
);
icon_folders.push(data_home.join("icons").to_string_lossy().to_string());
icon_folders.push(
data_home
.join("flatpak/exports/share/icons")
.to_string_lossy()
.to_string(),
);
}
app_folders.push("/var/lib/flatpak/exports/share/applications".into());
icon_folders.push("/var/lib/flatpak/exports/share/icons".into());
// /usr/share and such
for data_dir in xdg.get_data_dirs() {
app_folders.push(data_dir.join("applications").to_string_lossy().to_string());
icon_folders.push(data_dir.join("icons").to_string_lossy().to_string());
}
let size_preferences: Vec<&'static OsStr> = ["scalable", "128x128", "96x96", "72x72", "64x64", "48x48", "32x32"]
.into_iter()
.map(OsStr::new)
.collect();
Self {
params: Arc::new(DesktopFinderParams {
app_folders,
icon_folders,
size_preferences,
}),
entry_cache: HashMap::new(),
bg_task: None,
}
}
fn build_cache(params: Arc<DesktopFinderParams>) -> HashMap<String, DesktopEntryOwned> {
let start = Instant::now();
let mut known_files = HashSet::new();
let mut entries = HashMap::<String, DesktopEntryOwned>::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)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| !e.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 Some(app_id) = Path::new(entry.file_name()).file_stem().map(|x| x.to_string_lossy().to_string()) else {
continue;
};
if known_files.contains(file_name.as_ref()) {
// as per xdg spec, user entries of the same filename will override system entries
continue;
}
let file_path = format!("{}/{}", path, file_name);
let ini = match Ini::load_from_file(&file_path) {
Ok(ini) => ini,
Err(e) => {
log::debug!("Failed to read INI for .desktop file {}: {:?}, skipping", file_path, e);
continue;
}
};
let Some(section) = ini.section(Some("Desktop Entry")) else {
log::debug!("Failed to get [Desktop Entry] section for file {}, skipping", file_path);
continue;
};
if section.contains_key("OnlyShowIn") {
continue; // probably XFCE, KDE, GNOME or other DE-specific stuff
}
if let Some(x) = section.get("Terminal")
&& x == "true"
{
continue;
}
if let Some(x) = section.get("NoDisplay")
&& x.eq_ignore_ascii_case("true")
{
continue; // This application is hidden
}
if let Some(x) = section.get("Hidden")
&& x.eq_ignore_ascii_case("true")
{
continue; // This application is hidden
}
let Some(exec) = section.get("Exec") else {
log::debug!("Failed to get desktop entry Exec for file {}, skipping", file_path);
continue;
};
for entry in &CMD_BLOCKLIST {
if exec.contains(entry) {
continue 'entries;
}
}
let (exec_path, exec_args) = match exec.split_once(" ") {
Some((left, right)) => (
left,
right
.split(" ")
.filter(|arg| !arg.starts_with('%')) // exclude arguments like "%f"
.map(String::from)
.collect(),
),
None => (exec, Vec::new()),
};
let Some(app_name) = section.get("Name") else {
log::debug!(
"Failed to get desktop entry application name for file {}, skipping",
file_path
);
continue;
};
let icon_path = section
.get("Icon")
.and_then(|icon_name| Self::find_icon(&params, &icon_name))
.or_else(|| Self::create_icon(&file_name).ok());
if let Some(categories) = section.get("Categories") {
for cat in categories.split(";") {
if CATEGORY_TYPE_BLOCKLIST.contains(&cat) {
continue 'entries;
}
}
}
known_files.insert(file_name.to_string());
entries.insert(app_id, DesktopEntryOwned {
app_name: String::from(app_name),
exec_path: String::from(exec_path),
exec_args: exec_args.join(" "),
icon_path,
});
}
}
log::debug!("Desktop entry & icon scan took {:?}", start.elapsed());
entries
}
fn find_icon(params: &DesktopFinderParams, icon_name: &str) -> Option<String> {
if icon_name.starts_with("/") && exists(icon_name).unwrap_or(false) {
return Some(icon_name.to_string());
}
for folder in &params.icon_folders {
let pattern = format!("{}/hicolor/*/apps/{}.*", folder, icon_name);
let mut entries: Vec<_> = glob::glob(&pattern)
.expect("Bad glob pattern!")
.filter_map(Result::ok)
.collect();
// sort by SIZE_PREFERENCES
entries.sort_by_key(|path| {
path
.components()
.rev()
.nth(2) // ← hicolor/<*SIZE*>/apps/filename.ext
.map(|c| c.as_os_str())
.and_then(|size| params.size_preferences.iter().position(|&p| p == size))
.unwrap_or(usize::MAX)
});
if let Some(first) = entries.into_iter().next() {
return Some(first.to_string_lossy().into());
}
}
None
}
pub fn create_icon(desktop_entry_name: &str) -> anyhow::Result<String> {
let relative_path = format!("icons/{}.svg", desktop_entry_name);
let file_path = cache_dir::get_path(&relative_path).to_string_lossy().to_string();
if std::fs::exists(&file_path).unwrap_or(false) {
return Ok(file_path);
}
let svg = identicons_svg::generate(identicons_svg::IdenticonOptions {
background: identicons_svg::Background {
r: 64,
color: "rgba(0.9,0.9,0.9,0.5)".into(),
},
..Default::default()
});
std::fs::write(&file_path, svg)?;
Ok(file_path)
}
fn wait_for_entries(&mut self) {
let Some(bg_task) = self.bg_task.take() else {
return;
};
let Ok(entries) = bg_task.join() else {
return;
};
self.entry_cache.clear();
for (app_id, entry) in entries {
self.entry_cache.insert(app_id, entry.into());
}
}
pub fn get_cached_entry(&self, app_id: &str) -> Option<&DesktopEntry> {
self.entry_cache.get(app_id)
}
pub fn find_entries(&mut self) -> HashMap<String, DesktopEntry> {
self.wait_for_entries();
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)
});
self.bg_task = Some(bg_task);
}
}

View File

@@ -5,6 +5,7 @@ pub mod common;
pub mod config;
pub mod dash_interface;
pub mod dash_interface_emulated;
pub mod desktop_finder;
mod handle;
pub mod overlays;
pub mod timestep;

View File

@@ -1,3 +1,5 @@
use std::sync::Arc;
use idmap_derive::IntegerId;
use serde::{Deserialize, Serialize};
@@ -21,12 +23,15 @@ pub enum ToastDisplayMethod {
pub enum BackendAttrib {
Stereo,
MouseTransform,
Icon,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum BackendAttribValue {
Stereo(StereoMode),
MouseTransform(MouseTransform),
#[serde(skip_serializing, skip_deserializing)]
Icon(Arc<str>),
}
#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize)]