bar app icons & tooltips
This commit is contained in:
@@ -19,11 +19,10 @@ use wlx_common::{audio, dash_interface::BoxDashInterface, timestep::Timestep};
|
||||
use crate::{
|
||||
assets, settings,
|
||||
tab::{
|
||||
Tab, TabType, apps::TabApps, games::TabGames, home::TabHome, monado::TabMonado, processes::TabProcesses,
|
||||
settings::TabSettings,
|
||||
apps::TabApps, games::TabGames, home::TabHome, monado::TabMonado, processes::TabProcesses, settings::TabSettings,
|
||||
Tab, TabType,
|
||||
},
|
||||
util::{
|
||||
desktop_finder::DesktopFinder,
|
||||
popup_manager::{MountPopupParams, PopupManager, PopupManagerParams},
|
||||
toast_manager::ToastManager,
|
||||
various::AsyncExecutor,
|
||||
@@ -64,8 +63,6 @@ pub struct Frontend<T> {
|
||||
|
||||
window_audio_settings: WguiWindow,
|
||||
view_audio_settings: Option<views::audio_settings::View>,
|
||||
|
||||
pub(crate) desktop_finder: DesktopFinder,
|
||||
}
|
||||
|
||||
pub struct FrontendUpdateParams<'a, T> {
|
||||
@@ -137,9 +134,6 @@ impl<T: 'static> Frontend<T> {
|
||||
|
||||
let timestep = Timestep::new(60.0);
|
||||
|
||||
let mut desktop_finder = DesktopFinder::new();
|
||||
desktop_finder.refresh();
|
||||
|
||||
let mut frontend = Self {
|
||||
layout,
|
||||
state,
|
||||
@@ -159,7 +153,6 @@ impl<T: 'static> Frontend<T> {
|
||||
window_audio_settings: WguiWindow::default(),
|
||||
view_audio_settings: None,
|
||||
executor: Rc::new(smol::LocalExecutor::new()),
|
||||
desktop_finder,
|
||||
};
|
||||
|
||||
// init some things first
|
||||
@@ -292,7 +285,7 @@ impl<T: 'static> Frontend<T> {
|
||||
|
||||
fn process_task(&mut self, data: &mut T, task: FrontendTask) -> anyhow::Result<()> {
|
||||
match task {
|
||||
FrontendTask::SetTab(tab_type) => self.set_tab(tab_type)?,
|
||||
FrontendTask::SetTab(tab_type) => self.set_tab(data, tab_type)?,
|
||||
FrontendTask::RefreshClock => self.update_time()?,
|
||||
FrontendTask::RefreshBackground => self.update_background()?,
|
||||
FrontendTask::MountPopup(params) => self.mount_popup(params)?,
|
||||
@@ -305,14 +298,14 @@ impl<T: 'static> Frontend<T> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_tab(&mut self, tab_type: TabType) -> anyhow::Result<()> {
|
||||
fn set_tab(&mut self, data: &mut T, tab_type: TabType) -> anyhow::Result<()> {
|
||||
log::info!("Setting tab to {tab_type:?}");
|
||||
let widget_content = self.state.fetch_widget(&self.layout.state, "content")?;
|
||||
self.layout.remove_children(widget_content.id);
|
||||
|
||||
let tab: Box<dyn Tab<T>> = match tab_type {
|
||||
TabType::Home => Box::new(TabHome::new(self, widget_content.id)?),
|
||||
TabType::Apps => Box::new(TabApps::new(self, widget_content.id)?),
|
||||
TabType::Apps => Box::new(TabApps::new(self, widget_content.id, data)?),
|
||||
TabType::Games => Box::new(TabGames::new(self, widget_content.id)?),
|
||||
TabType::Monado => Box::new(TabMonado::new(self, widget_content.id)?),
|
||||
TabType::Processes => Box::new(TabProcesses::new(self, widget_content.id)?),
|
||||
|
||||
@@ -14,14 +14,12 @@ use wgui::{
|
||||
parser::{Fetchable, ParseDocumentParams, ParserState},
|
||||
task::Tasks,
|
||||
};
|
||||
use wlx_common::desktop_finder::DesktopEntry;
|
||||
|
||||
use crate::{
|
||||
frontend::{Frontend, FrontendTask, FrontendTasks},
|
||||
tab::{Tab, TabType},
|
||||
util::{
|
||||
desktop_finder::DesktopEntry,
|
||||
popup_manager::{MountPopupParams, PopupHandle},
|
||||
},
|
||||
util::popup_manager::{MountPopupParams, PopupHandle},
|
||||
views::{self, app_launcher},
|
||||
};
|
||||
|
||||
@@ -127,16 +125,16 @@ fn doc_params(globals: WguiGlobals) -> ParseDocumentParams<'static> {
|
||||
}
|
||||
|
||||
impl<T> TabApps<T> {
|
||||
pub fn new(frontend: &mut Frontend<T>, parent_id: WidgetID) -> anyhow::Result<Self> {
|
||||
pub fn new(frontend: &mut Frontend<T>, parent_id: WidgetID, data: &mut T) -> anyhow::Result<Self> {
|
||||
let globals = frontend.layout.state.globals.clone();
|
||||
let tasks = Tasks::new();
|
||||
let state = Rc::new(RefCell::new(State { view_launcher: None }));
|
||||
|
||||
let mut entries = frontend.desktop_finder.find_entries();
|
||||
let mut entries = frontend.interface.desktop_finder(data).find_entries();
|
||||
let parser_state = wgui::parser::parse_from_assets(&doc_params(globals.clone()), &mut frontend.layout, parent_id)?;
|
||||
let app_list_parent = parser_state.fetch_widget(&frontend.layout.state, "app_list_parent")?;
|
||||
let app_list = AppList {
|
||||
entries_to_mount: entries.drain(..).collect(),
|
||||
entries_to_mount: entries.into_values().collect(),
|
||||
list_parent: app_list_parent,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,315 +0,0 @@
|
||||
use std::{
|
||||
collections::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 wlx_common::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: Vec<DesktopEntry>,
|
||||
bg_task: Option<JoinHandle<Vec<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: Vec::new(),
|
||||
bg_task: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_cache(params: Arc<DesktopFinderParams>) -> Vec<DesktopEntryOwned> {
|
||||
let start = Instant::now();
|
||||
|
||||
let mut known_files = HashSet::new();
|
||||
let mut entries = Vec::<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 ¶ms.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();
|
||||
|
||||
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(¶ms, &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.push(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 ¶ms.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
|
||||
}
|
||||
|
||||
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 entry in entries {
|
||||
self.entry_cache.push(entry.into());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_entries(&mut self) -> Vec<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);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
pub mod cached_fetcher;
|
||||
pub mod desktop_finder;
|
||||
pub mod http_client;
|
||||
pub mod pactl_wrapper;
|
||||
pub mod popup_manager;
|
||||
|
||||
@@ -11,11 +11,10 @@ use wgui::{
|
||||
sprite::{WidgetSprite, WidgetSpriteParams},
|
||||
},
|
||||
};
|
||||
use wlx_common::desktop_finder;
|
||||
|
||||
pub type AsyncExecutor = Rc<smol::LocalExecutor<'static>>;
|
||||
|
||||
use crate::util::desktop_finder;
|
||||
|
||||
// the compiler wants to scream
|
||||
#[allow(irrefutable_let_patterns)]
|
||||
pub fn get_desktop_file_icon_path(desktop_file: &desktop_finder::DesktopEntry) -> AssetPathOwned {
|
||||
|
||||
@@ -12,12 +12,11 @@ use wgui::{
|
||||
task::Tasks,
|
||||
widget::label::WidgetLabel,
|
||||
};
|
||||
use wlx_common::dash_interface::BoxDashInterface;
|
||||
use wlx_common::{dash_interface::BoxDashInterface, desktop_finder::DesktopEntry};
|
||||
|
||||
use crate::{
|
||||
frontend::{FrontendTask, FrontendTasks},
|
||||
settings::SettingsIO,
|
||||
util::desktop_finder::DesktopEntry,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq, EnumString, VariantNames, AsRefStr)]
|
||||
@@ -325,6 +324,7 @@ impl View {
|
||||
name: params.application.app_name.to_string(),
|
||||
args,
|
||||
resolution,
|
||||
icon: params.application.icon_path.as_ref().map(|x| x.as_ref().to_string()),
|
||||
userdata,
|
||||
},
|
||||
)?;
|
||||
|
||||
@@ -20,13 +20,9 @@ use wgui::{
|
||||
ConstructEssentials,
|
||||
},
|
||||
};
|
||||
use wlx_common::dash_interface::BoxDashInterface;
|
||||
use wlx_common::{dash_interface::BoxDashInterface, desktop_finder::DesktopEntry};
|
||||
|
||||
use crate::util::{
|
||||
self,
|
||||
desktop_finder::{self},
|
||||
various::get_desktop_file_icon_path,
|
||||
};
|
||||
use crate::util::{self, various::get_desktop_file_icon_path};
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Task {
|
||||
@@ -94,13 +90,13 @@ impl View {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_desktop_entry_from_process(process: &packet_server::WvrProcess) -> Option<desktop_finder::DesktopEntry> {
|
||||
fn get_desktop_entry_from_process(process: &packet_server::WvrProcess) -> Option<DesktopEntry> {
|
||||
// TODO: refactor this after we ditch old wayvr-dashboard completely
|
||||
let Some(dfile_str) = process.userdata.get("desktop-entry") else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let Ok(desktop_file) = serde_json::from_str::<desktop_finder::DesktopEntry>(dfile_str) else {
|
||||
let Ok(desktop_file) = serde_json::from_str::<DesktopEntry>(dfile_str) else {
|
||||
debug_assert!(false); // invalid json???
|
||||
return None;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user