Compare commits

...
Author SHA1 Message Date
galister db777d7937 XSHM & PipeWire CPU capture fixes 2026-10-23 21:11:33 +09:00
galister f94c0c4ace desktop_finder rework (#697)
* desktop_finder rework

* fix args for wayvrctl process-launch
2026-10-23 19:29:25 +09:00
Aleksander 5f4c97794c wayvr: prevent alpha from going NaN again 2026-10-22 18:04:40 +02:00
Aleksander d9288a877c dash-frontend: refactoring: deduplicate dropdown code 2026-10-21 19:57:14 +02:00
30 changed files with 591 additions and 386 deletions

No files matched your search

Generated
+118 -3
View File
@@ -689,6 +689,12 @@ dependencies = [
"wyz",
]
[[package]]
name = "block"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
[[package]]
name = "block-buffer"
version = "0.10.4"
@@ -729,6 +735,17 @@ dependencies = [
"piper",
]
[[package]]
name = "bstr"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f"
dependencies = [
"memchr",
"regex-automata",
"serde_core",
]
[[package]]
name = "built"
version = "0.8.1"
@@ -1401,6 +1418,7 @@ dependencies = [
"rust-embed",
"serde",
"serde_json",
"shell-words",
"smol",
"strum",
"ureq",
@@ -1921,6 +1939,21 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
[[package]]
name = "freedesktop-desktop-entry"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42a69ad6111a2b572cb41e0f3542d546386e841013cee7c45d705b61a15a87dd"
dependencies = [
"bstr",
"gettext-rs",
"log",
"memchr",
"thiserror 2.0.18",
"unicase",
"xdg 3.0.0",
]
[[package]]
name = "fs_extra"
version = "1.3.0"
@@ -2100,6 +2133,26 @@ dependencies = [
"rand_core 0.10.1",
]
[[package]]
name = "gettext-rs"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9df5e3fa8c077b15fdfa8ce130e2bc73d663b8aaa6d51b71421b8659980b1d80"
dependencies = [
"gettext-sys",
"locale_config",
]
[[package]]
name = "gettext-sys"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4caa7e4460b196107cc1bb2629488e8b6dbb7f34f6683ea6753c4ac22f590785"
dependencies = [
"cc",
"temp-dir",
]
[[package]]
name = "gif"
version = "0.14.2"
@@ -2869,6 +2922,19 @@ version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "locale_config"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d2c35b16f4483f6c26f0e4e9550717a2f6575bcd6f12a53ff0c490a94a6934"
dependencies = [
"lazy_static",
"objc",
"objc-foundation",
"regex",
"winapi",
]
[[package]]
name = "lock_api"
version = "0.4.14"
@@ -2930,6 +2996,15 @@ dependencies = [
"libc",
]
[[package]]
name = "malloc_buf"
version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
dependencies = [
"libc",
]
[[package]]
name = "matchers"
version = "0.2.0"
@@ -3221,6 +3296,26 @@ dependencies = [
"syn 2.0.118",
]
[[package]]
name = "objc"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
dependencies = [
"malloc_buf",
]
[[package]]
name = "objc-foundation"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
dependencies = [
"block",
"objc",
"objc_id",
]
[[package]]
name = "objc-sys"
version = "0.3.5"
@@ -3528,6 +3623,15 @@ dependencies = [
"objc2-foundation 0.2.2",
]
[[package]]
name = "objc_id"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
dependencies = [
"objc",
]
[[package]]
name = "object"
version = "0.37.3"
@@ -4669,8 +4773,7 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "rxscreen"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afbab13f83503a8272b7150d487494992cf6c59c299dcaf5d8ffac75bad9fc96"
source = "git+https://github.com/galister/rxscreen.git?rev=7197e1de6622b65859e6f2060b5750185e0dad2a#7197e1de6622b65859e6f2060b5750185e0dad2a"
dependencies = [
"libc",
]
@@ -5343,6 +5446,12 @@ version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
[[package]]
name = "temp-dir"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83176759e9416cf81ee66cb6508dbfe9c96f20b8b56265a39917551c23c70964"
[[package]]
name = "tempfile"
version = "3.27.0"
@@ -5693,6 +5802,12 @@ dependencies = [
"wlx-common",
]
[[package]]
name = "unicase"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
[[package]]
name = "unicode-bidi"
version = "0.3.18"
@@ -6739,6 +6854,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"freedesktop-desktop-entry",
"glam",
"glob",
"identicons-svg",
@@ -6746,7 +6862,6 @@ dependencies = [
"idmap-derive",
"log",
"rodio",
"rust-ini",
"serde",
"serde_json",
"serde_json5",
+1 -1
View File
@@ -32,6 +32,7 @@ rust-embed = "8.9.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1.0.145"
serde_json5 = "0.2.1"
shell-words = "1.1.1"
slotmap = "1.1.1"
smallvec = "1.15.2"
smol = "2.0.2"
@@ -44,7 +45,6 @@ wayland-protocols-misc = { version = "0.3.12" }
winit = "0.30.12"
xdg = "3.0.0"
[profile.dev]
opt-level = 1
debug = true
+1
View File
@@ -19,6 +19,7 @@ log.workspace = true
rust-embed.workspace = true
serde = { workspace = true, features = ["rc"] }
serde_json.workspace = true
shell-words.workspace = true
smol = { workspace = true }
strum.workspace = true
uuid.workspace = true
+14 -11
View File
@@ -61,7 +61,7 @@ impl<T> Tab<T> for TabApps<T> {
}
}
fn find_entry_from_app_name<'a>(app_id: &str, entries: &'a [DesktopEntry]) -> Option<&'a DesktopEntry> {
fn find_entry_from_app_name<'a>(app_id: &str, entries: &'a [Rc<DesktopEntry>]) -> Option<&'a Rc<DesktopEntry>> {
entries.iter().find(|&entry| *entry.app_id == *app_id)
}
@@ -72,7 +72,7 @@ impl<T> TabApps<T> {
let mut stale_entries = Vec::<Rc<str>>::new();
let mut pinned_desktop_entries = Vec::<(PinnedApp, &DesktopEntry)>::new();
let mut pinned_desktop_entries = Vec::<(PinnedApp, Rc<DesktopEntry>)>::new();
// collect pinned desktop entries
{
@@ -83,7 +83,7 @@ impl<T> TabApps<T> {
continue;
};
pinned_desktop_entries.push((pinned_app.clone(), desktop_entry));
pinned_desktop_entries.push((pinned_app.clone(), desktop_entry.clone()));
}
// cleanup:
// remove non-existent app ids from pinned apps
@@ -119,7 +119,7 @@ impl<T> TabApps<T> {
&mut state.parser_state,
&doc_params(frontend.globals.clone()),
self.pinned_apps_parent,
desktop_entry,
desktop_entry.as_ref(),
Some(tooltip_string),
)?;
@@ -139,8 +139,8 @@ impl<T> TabApps<T> {
struct AppList {
//data: Vec<ParserData>,
all_entries: Vec<DesktopEntry>,
entries_to_mount: VecDeque<DesktopEntry>,
all_entries: Vec<Rc<DesktopEntry>>,
entries_to_mount: VecDeque<Rc<DesktopEntry>>,
list_parent: WidgetPair,
prev_category_name: String,
}
@@ -150,7 +150,7 @@ fn on_app_click(
frontend_tasks: FrontendTasks,
tasks: Tasks<Task>,
globals: WguiGlobals,
entry: DesktopEntry,
entry: Rc<DesktopEntry>,
state: Rc<RefCell<State>>,
pinned_app: Option<PinnedApp>,
) -> ButtonClickCallback {
@@ -307,14 +307,17 @@ fn get_category_name_score(name: &str) -> u8 {
Scores::Unknown as u8
}
fn get_best_category_name(categories: &[Rc<str>]) -> Option<&Rc<str>> {
fn get_best_category_name<S>(categories: &[S]) -> Option<&str>
where
S: AsRef<str>,
{
let mut best_score: u8 = 0;
let mut best_category: Option<&Rc<str>> = None;
let mut best_category: Option<&str> = None;
for cat in categories {
let score = get_category_name_score(cat);
let score = get_category_name_score(cat.as_ref());
if score > best_score {
best_category = Some(cat);
best_category = Some(cat.as_ref());
best_score = score;
}
}
+57 -66
View File
@@ -1,16 +1,14 @@
use std::rc::Rc;
use crate::tab::settings::{self, SettingType, Task, horiz_cell, mount_requires_restart};
use crate::{
tab::settings::{self, SettingType, Task, horiz_cell, mount_requires_restart},
util::wgui_simple,
};
use wgui::{
components::{
button::{ButtonClickEvent, ComponentButton},
checkbox::ComponentCheckbox,
slider::ComponentSlider,
},
components::{button::ComponentButton, checkbox::ComponentCheckbox, slider::ComponentSlider},
layout::{Layout, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState, TemplateParams},
task::Tasks,
widget::label::WidgetLabel,
windowing::context_menu,
};
use wlx_common::{DesktopBackend, XrBackend, config::GeneralConfig, dash_interface::InterfaceFeats};
@@ -297,7 +295,6 @@ where
mp.idx += 1;
let mut params = TemplateParams::new();
params.insert("id", &id);
match setting.get_translation() {
Ok(translation) => params.insert("translation", translation),
@@ -310,69 +307,63 @@ where
let id_cell = horiz_cell(mp.layout, parent)?;
mp.parser_state
.instantiate_template(mp.doc_params, "DropdownButton", mp.layout, id_cell, params)?;
let setting_str = setting.as_ref();
let title = setting.get_enum_title(mp.config);
let feats = mp.feats;
let cells = EnumType::VARIANTS
.iter()
.filter_map(|item| {
if item.get_bool("Hidden").unwrap_or(false) {
return None;
}
if item
.get_str("Backend")
.is_some_and(|x| XrBackend::try_from(x).unwrap() != feats.xr_backend)
{
return None;
}
if item
.get_str("Desktop")
.is_some_and(|x| DesktopBackend::try_from(x).unwrap() != feats.desktop_backend)
{
return None;
}
let value = item.as_ref();
let title = SettingType::get_enum_title_inner(*item);
let tooltip = SettingType::get_enum_tooltip_inner(*item);
let text = &title.text;
let translated = if title.translated { "1" } else { "0" };
let action = format!("{setting_str};{id};{value};{text};{translated}");
Some(context_menu::Cell::Button(context_menu::Button {
action_name: Some(action.into()),
title,
tooltip,
..Default::default()
}))
})
.collect();
let tasks = mp.tasks.clone();
wgui_simple::mount_dropdown(wgui_simple::MountDropdownParams {
layout: mp.layout,
parser_state: mp.parser_state,
doc_params: mp.doc_params,
parent: id_cell,
id,
params,
current_text: title,
cells,
on_open: Box::new(move |pos, cells| tasks.push(Task::OpenContextMenu(pos, cells))),
})?;
if setting.requires_restart() {
mount_requires_restart(mp.layout, id_cell)?;
}
let setting_str = setting.as_ref();
let title = setting.get_enum_title(mp.config);
{
let mut label = mp
.parser_state
.fetch_widget_as::<WidgetLabel>(&mp.layout.state, &format!("{id}_value"))?;
label.set_text_simple(&mut mp.layout.state.globals.get(), title);
}
let btn = mp.parser_state.fetch_component_as::<ComponentButton>(&id)?;
btn.on_click(Rc::new({
let feats = mp.feats;
let tasks = mp.tasks.clone();
move |_common, e: ButtonClickEvent| {
tasks.push(Task::OpenContextMenu(
e.mouse_pos_absolute.into(),
EnumType::VARIANTS
.iter()
.filter_map(|item| {
if item.get_bool("Hidden").unwrap_or(false) {
return None;
}
if item
.get_str("Backend")
.is_some_and(|x| XrBackend::try_from(x).unwrap() != feats.xr_backend)
{
return None;
}
if item
.get_str("Desktop")
.is_some_and(|x| DesktopBackend::try_from(x).unwrap() != feats.desktop_backend)
{
return None;
}
let value = item.as_ref();
let title = SettingType::get_enum_title_inner(*item);
let tooltip = SettingType::get_enum_tooltip_inner(*item);
let text = &title.text;
let translated = if title.translated { "1" } else { "0" };
Some(context_menu::Cell::Button(context_menu::Button {
action_name: Some(format!("{setting_str};{id};{value};{text};{translated}").into()),
title,
tooltip,
..Default::default()
}))
})
.collect(),
));
Ok(())
}
}));
Ok(())
}
+65 -85
View File
@@ -1,7 +1,6 @@
use std::rc::Rc;
use wgui::{
components::button::{ButtonClickEvent, ComponentButton},
globals::WguiGlobals,
i18n::Translation,
layout::WidgetID,
@@ -9,7 +8,7 @@ use wgui::{
parser::{Fetchable, TemplateParams},
task::Tasks,
widget::label::WidgetLabel,
windowing::context_menu::{self},
windowing::context_menu,
};
use wlx_common::{async_executor::AsyncExecutor, config::GeneralConfig, dash_interface::ConfigChangeKind};
@@ -21,6 +20,7 @@ use crate::{
},
util::{
popup_manager::PopupHolder,
wgui_simple,
whisper::{
WHISPER_MODELS, WhisperModel, whisper_any_models_downloaded, whisper_delete_all_models, whisper_model_from_name,
whisper_model_path,
@@ -319,47 +319,35 @@ fn whisper_gpu_select_dropdown(
let parent = horiz_cell(mp.layout, parent)?;
let mut params = TemplateParams::new();
params.insert("id", &id);
params.insert("translation", "APP_SETTINGS.WHISPER_GPU_SELECT");
mp.parser_state
.instantiate_template(mp.doc_params, "DropdownButton", mp.layout, parent, params)?;
let current_text = Translation::from_raw_text_string(mp.config.whisper_gpu.to_string());
{
let mut label = mp
.parser_state
.fetch_widget_as::<WidgetLabel>(&mp.layout.state, &format!("{id}_value"))?;
label.set_text_simple(
&mut mp.layout.state.globals.get(),
Translation::from_raw_text_string(mp.config.whisper_gpu.to_string()),
);
}
let cells = vulkan_devices
.iter()
.enumerate()
.filter(|(_, name)| !name.contains("llvmpipe"))
.map(|(idx, name)| {
context_menu::Cell::Button(context_menu::Button {
action_name: Some(format!(";whisper_gpu;{};{}", id, idx).into()),
title: Translation::from_raw_text_string(name.to_string()),
..Default::default()
})
})
.collect();
let btn = mp.parser_state.fetch_component_as::<ComponentButton>(&id)?;
btn.on_click(Rc::new({
let parent_tasks = mp.tasks.clone();
move |_common, e: ButtonClickEvent| {
let mut cells = Vec::new();
for (idx, name) in vulkan_devices.iter().enumerate() {
if name.contains("llvmpipe") {
continue;
}
cells.push(context_menu::Cell::Button(context_menu::Button {
action_name: Some(format!(";whisper_gpu;{};{}", id, idx).into()),
title: Translation::from_raw_text_string(name.to_string()),
..Default::default()
}));
}
parent_tasks.push(ParentTask::OpenContextMenu(e.mouse_pos_absolute.into(), cells));
Ok(())
}
}));
Ok(())
let tasks = mp.tasks.clone();
wgui_simple::mount_dropdown(wgui_simple::MountDropdownParams {
layout: mp.layout,
parser_state: mp.parser_state,
doc_params: mp.doc_params,
parent,
id,
params,
current_text,
cells,
on_open: Box::new(move |pos, cells| tasks.push(ParentTask::OpenContextMenu(pos, cells))),
})
}
fn whisper_models_dropdown(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Result<()> {
@@ -382,57 +370,49 @@ fn whisper_models_dropdown(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Re
});
let mut params = TemplateParams::new();
params.insert("id", &id);
params.insert("translation", "APP_SETTINGS.WHISPER_MODEL");
params.insert("tooltip", "APP_SETTINGS.WHISPER_MODEL_HELP");
mp.parser_state
.instantiate_template(mp.doc_params, "DropdownButton", mp.layout, parent, params)?;
{
let mut label = mp
.parser_state
.fetch_widget_as::<WidgetLabel>(&mp.layout.state, &format!("{id}_value"))?;
label.set_text_simple(&mut mp.layout.state.globals.get(), current_translation);
}
let btn = mp.parser_state.fetch_component_as::<ComponentButton>(&id)?;
btn.on_click(Rc::new({
let parent_tasks = mp.tasks.clone();
move |_common, e: ButtonClickEvent| {
let mut cells = WHISPER_MODELS
.iter()
.enumerate()
.map(|(idx, item)| {
context_menu::Cell::Button(context_menu::Button {
action_name: Some(format!(";whisper_model;{id};{}", item.file_name).into()),
title: Translation::from_raw_text(item.display_name),
tooltip: None,
attribs: vec![],
sprite_src_builtin: if downloaded_models.contains(&idx) {
Some("@/dashboard/check.svg".into())
} else {
None
},
})
})
.collect::<Vec<_>>();
cells.insert(
0,
let mut cells = WHISPER_MODELS
.iter()
.enumerate()
.map(|(idx, item)| {
if downloaded_models.contains(&idx) {
context_menu::Cell::Button(context_menu::Button {
action_name: Some(format!(";whisper_model;{id};").into()),
title: Translation::from_translation_key("APP_SETTINGS.OPTION.NONE"),
tooltip: None,
attribs: vec![],
action_name: Some(format!(";whisper_model;{id};{}", item.file_name).into()),
title: Translation::from_raw_text(item.display_name),
sprite_src_builtin: Some("@/dashboard/check.svg".into()),
..Default::default()
}),
);
})
} else {
context_menu::Cell::Button(context_menu::Button {
action_name: Some(format!(";whisper_model;{id};{}", item.file_name).into()),
title: Translation::from_raw_text(item.display_name),
..Default::default()
})
}
})
.collect::<Vec<_>>();
parent_tasks.push(ParentTask::OpenContextMenu(e.mouse_pos_absolute.into(), cells));
Ok(())
}
}));
cells.insert(
0,
context_menu::Cell::Button(context_menu::Button {
action_name: Some(format!(";whisper_model;{id};").into()),
title: Translation::from_translation_key("APP_SETTINGS.OPTION.NONE"),
..Default::default()
}),
);
Ok(())
let tasks = mp.tasks.clone();
wgui_simple::mount_dropdown(wgui_simple::MountDropdownParams {
layout: mp.layout,
parser_state: mp.parser_state,
doc_params: mp.doc_params,
parent,
id,
params,
current_text: current_translation,
cells,
on_open: Box::new(move |pos, cells| tasks.push(ParentTask::OpenContextMenu(pos, cells))),
})
}
+43 -2
View File
@@ -1,12 +1,16 @@
use glam::{Mat4, Vec2};
use std::rc::Rc;
use wgui::{
animation::{Animation, AnimationDuration, AnimationEasing},
assets::AssetPathRef,
color::WguiColorName,
components::{self, button::ButtonClickCallback},
components::{
self,
button::{ButtonClickCallback, ButtonClickEvent, ComponentButton},
},
i18n::Translation,
layout::{Layout, LayoutTask, WidgetID},
parser::{Fetchable, ParseDocumentParams},
parser::{Fetchable, ParseDocumentParams, ParserState, TemplateParams},
renderer_vk::{
text::{FontWeight, TextStyle, custom_glyph::CustomGlyphData},
util::centered_matrix,
@@ -17,6 +21,7 @@ use wgui::{
label::{WidgetLabel, WidgetLabelParams},
sprite::{WidgetSprite, WidgetSpriteParams},
},
windowing::context_menu,
};
pub struct CreateButtonParams<'a> {
@@ -166,3 +171,39 @@ pub fn create_loading(par: CreateLoadingParams) -> anyhow::Result<WidgetID> {
Ok(id_root)
}
pub struct MountDropdownParams<'a> {
pub layout: &'a mut Layout,
pub parser_state: &'a mut ParserState,
pub doc_params: &'a ParseDocumentParams<'a>,
pub parent: WidgetID,
pub id: String,
pub params: TemplateParams,
pub current_text: Translation,
pub cells: Vec<context_menu::Cell>,
pub on_open: Box<dyn Fn(context_menu::Position, Vec<context_menu::Cell>)>,
}
pub fn mount_dropdown(par: MountDropdownParams) -> anyhow::Result<()> {
let mut params = par.params;
params.insert("id", &par.id);
par
.parser_state
.instantiate_template(par.doc_params, "DropdownButton", par.layout, par.parent, params)?;
{
let mut label = par
.parser_state
.fetch_widget_as::<WidgetLabel>(&par.layout.state, &format!("{}_value", par.id))?;
label.set_text_simple(&mut par.layout.state.globals.get(), par.current_text);
}
let btn = par.parser_state.fetch_component_as::<ComponentButton>(&par.id)?;
btn.on_click(Rc::new(move |_common, e: ButtonClickEvent| {
(par.on_open)(e.mouse_pos_absolute.into(), par.cells.clone());
Ok(())
}));
Ok(())
}
+21 -19
View File
@@ -53,7 +53,7 @@ struct LaunchParams<'a, T> {
pub struct View {
#[allow(dead_code)]
state: ParserState,
entry: DesktopEntry,
entry: Rc<DesktopEntry>,
tasks: Tasks<Task>,
frontend_tasks: FrontendTasks,
globals: WguiGlobals,
@@ -80,7 +80,7 @@ pub struct View {
pub struct Params<'a> {
pub globals: &'a WguiGlobals,
pub entry: DesktopEntry,
pub entry: Rc<DesktopEntry>,
pub layout: &'a mut Layout,
pub parent_id: WidgetID,
pub config: &'a GeneralConfig,
@@ -119,10 +119,8 @@ impl View {
{
let mut label_exec = state.fetch_widget_as::<WidgetLabel>(&params.layout.state, "label_exec")?;
label_exec.set_text_simple(
&mut params.globals.get(),
Translation::from_raw_text_string(format!("{} {}", params.entry.exec_path, params.entry.exec_args)),
);
let command = shell_words::join(params.entry.exec_argv.iter().map(|x| x.as_ref()));
label_exec.set_text_simple(&mut params.globals.get(), Translation::from_raw_text_string(command));
}
let tasks = Tasks::new();
@@ -406,14 +404,19 @@ impl View {
env.push("ELECTRON_OZONE_PLATFORM_HINT=wayland".into());
}
let args = match params.compositor_mode {
AppCompositorMode::Cage => format!("-- {} {}", params.application.exec_path, params.application.exec_args),
AppCompositorMode::Native => params.application.exec_args.to_string(),
};
let exec = match params.compositor_mode {
AppCompositorMode::Cage => "cage".to_string(),
AppCompositorMode::Native => params.application.exec_path.to_string(),
let argv = match params.compositor_mode {
AppCompositorMode::Cage => ["cage", "--"]
.iter()
.copied()
.chain(params.application.exec_argv.iter().map(|x| x.as_ref()))
.map(|x| x.to_string())
.collect::<Vec<_>>(),
AppCompositorMode::Native => params
.application
.exec_argv
.iter()
.map(|x| x.to_string())
.collect::<Vec<_>>(),
};
let pos_mode = match params.pos_mode {
@@ -423,7 +426,7 @@ impl View {
};
let mut userdata = HashMap::new();
userdata.insert("desktop-entry".to_string(), serde_json::to_string(params.application)?);
userdata.insert("desktop-entry".into(), serde_json::to_string(params.application)?);
let resolution = Self::calculate_resolution(params.res_mode, params.orientation_mode);
@@ -432,12 +435,11 @@ impl View {
params.auto_start,
WvrProcessLaunchParams {
env,
exec,
argv,
name: params.application.app_name.to_string(),
args,
resolution,
pos_mode,
icon: params.application.icon_path.as_ref().map(|x| x.as_ref().to_string()),
icon: params.application.icon_path.as_deref().map(String::from),
userdata,
},
)?;
@@ -486,7 +488,7 @@ impl View {
pub fn mount_popup(
frontend_tasks: FrontendTasks,
globals: WguiGlobals,
entry: DesktopEntry,
entry: Rc<DesktopEntry>,
popup: PopupHolder<View>,
on_app_pins_changed: Box<dyn Fn()>,
pinned_app: Option<PinnedApp>,
+17 -34
View File
@@ -3,17 +3,13 @@ use std::rc::Rc;
use strum::EnumProperty;
use wgui::{
assets::AssetPathRef,
components::{
button::{ButtonClickEvent, ComponentButton},
slider::ComponentSlider,
},
components::{button::ComponentButton, slider::ComponentSlider},
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID},
log::LogErr,
parser::{Fetchable, ParseDocumentParams, ParserState, TemplateParams},
task::Tasks,
widget::label::WidgetLabel,
windowing::context_menu::{self, TickResult},
};
use wlx_common::{
@@ -1012,7 +1008,7 @@ fn create_dropdown<B: 'static + BindingsDropdown>(
.iter()
.map(|item| {
context_menu::Cell::Button(context_menu::Button {
action_name: Some(item.action_str(&action, side)),
action_name: Some(item.action_str(&action, side).into()),
title: item.translation(),
..Default::default()
})
@@ -1023,7 +1019,7 @@ fn create_dropdown<B: 'static + BindingsDropdown>(
cells.insert(
0,
context_menu::Cell::Button(context_menu::Button {
action_name: Some(action_str),
action_name: Some(action_str.into()),
title: Translation::from_translation_key("APP_SETTINGS.OPTION.NONE"),
..Default::default()
}),
@@ -1036,35 +1032,24 @@ fn create_dropdown<B: 'static + BindingsDropdown>(
fn create_dropdown_with_cells(
mp: &mut MacroParams,
parent: WidgetID,
mut params: TemplateParams,
params: TemplateParams,
current_text: Translation,
cells: Vec<context_menu::Cell>,
) -> anyhow::Result<()> {
let id = mp.idx.to_string();
mp.idx += 1;
params.insert("id", &id);
mp.parser_state
.instantiate_template(mp.doc_params, "DropdownButton", mp.layout, parent, params)?;
{
let mut label = mp
.parser_state
.fetch_widget_as::<WidgetLabel>(&mp.layout.state, &format!("{id}_value"))?;
label.set_text_simple(&mut mp.layout.state.globals.get(), current_text);
}
let btn = mp.parser_state.fetch_component_as::<ComponentButton>(&id)?;
btn.on_click(Rc::new({
let tasks = mp.tasks.clone();
let cells = cells.clone();
move |_common, e: ButtonClickEvent| {
tasks.push(Task::OpenContextMenu(e.mouse_pos_absolute.into(), cells.clone()));
Ok(())
}
}));
Ok(())
let tasks = mp.tasks.clone();
wgui_simple::mount_dropdown(wgui_simple::MountDropdownParams {
layout: mp.layout,
parser_state: mp.parser_state,
doc_params: mp.doc_params,
parent,
id,
params,
current_text,
cells,
on_open: Box::new(move |pos, cells| tasks.push(Task::OpenContextMenu(pos, cells))),
})
}
fn apply_subpath(
@@ -1082,9 +1067,7 @@ fn apply_subpath(
};
if action_name == "scroll" {
*side_mut = Some(OneOrMany::One(format!(
"/user/hand/{side_str}/input/{subpath_str}"
)));
*side_mut = Some(OneOrMany::One(format!("/user/hand/{side_str}/input/{subpath_str}")));
return;
}
+3 -2
View File
@@ -2,7 +2,7 @@ use std::{rc::Rc, sync::Arc};
use crate::testbed::{Testbed, TestbedUpdateParams};
use dash_frontend::frontend::{self, FrontendUpdateParams};
use wgui::{gfx::WGfx, layout::Layout, theme::WguiTheme};
use wgui::{gfx::WGfx, i18n::Locale, layout::Layout, theme::WguiTheme};
use wlx_common::{
async_executor::AsyncExecutor, dash_interface_emulated::DashInterfaceEmulated,
locale::WayVRLangProvider,
@@ -15,8 +15,9 @@ pub struct TestbedDashboard {
impl TestbedDashboard {
pub fn new(gfx: Arc<WGfx>) -> anyhow::Result<Self> {
let interface = DashInterfaceEmulated::new(gfx);
let lang_provider = WayVRLangProvider::default();
let locale = Locale::from_env(&lang_provider);
let interface = DashInterfaceEmulated::new(gfx, &locale);
let palette_name = std::env::var("PALETTE").unwrap_or_else(|_| "Default".to_string());
let executor = wlx_common::async_executor::create_local();
+1 -2
View File
@@ -49,9 +49,8 @@ pub enum HandsfreeParams {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WvrProcessLaunchParams {
pub name: String,
pub exec: String,
pub argv: Vec<String>,
pub env: Vec<String>,
pub args: String,
pub icon: Option<String>,
pub resolution: [u32; 2],
pub pos_mode: PositionMode,
+22 -19
View File
@@ -6,6 +6,7 @@ use crate::{
backend::input::InputState,
ipc::{event_queue::SyncEventQueue, signal::WayVRSignal},
};
use anyhow::Context;
use bytes::BufMut;
use glam::Vec3A;
use interprocess::local_socket::{self, ToNsName, traits::Listener};
@@ -95,10 +96,6 @@ pub struct TickParams<'a> {
pub hid_wrapper: &'a mut HidWrapper,
}
pub fn gen_args_vec(input: &str) -> Vec<&str> {
input.split_whitespace().collect()
}
pub fn gen_env_vec(input: &[String]) -> Vec<(&str, &str)> {
input
.iter()
@@ -226,22 +223,28 @@ impl Connection {
serial: ipc::Serial,
packet_params: packet_client::WvrProcessLaunchParams,
) -> anyhow::Result<()> {
let args_vec = gen_args_vec(&packet_params.args);
let env_vec = gen_env_vec(&packet_params.env);
let res = {
let mut argv_iter = packet_params.argv.iter().map(String::as_str);
let exec_path = argv_iter
.next()
.context("No executable for process_launch")?;
let exec_args = argv_iter.collect::<Box<_>>();
let env_vec = gen_env_vec(&packet_params.env);
let res = params.wvr_server.spawn_process(
&packet_params.name,
&packet_params.exec,
&args_vec,
&env_vec,
packet_params.resolution,
packet_params.pos_mode,
None,
packet_params.icon.as_deref(),
packet_params.userdata,
);
let res = res.map(ProcessHandle::as_packet).map_err(|e| e.to_string());
params.wvr_server.spawn_process(
&packet_params.name,
exec_path,
&exec_args,
&env_vec,
packet_params.resolution,
packet_params.pos_mode,
None,
packet_params.icon.as_deref(),
packet_params.userdata,
)
}
.map(ProcessHandle::as_packet)
.map_err(|e| e.to_string());
send_packet(
&mut self.conn,
+9 -4
View File
@@ -4,6 +4,7 @@ use std::{
time::{Duration, Instant},
};
use anyhow::Context;
use dash_frontend::frontend::{self, FrontendTask, FrontendUpdateParams};
use glam::{Affine2, Affine3A, Quat, Vec2, vec2, vec3};
use wayvr_ipc::{
@@ -43,7 +44,7 @@ use crate::{
},
},
config::{none_if_0, save_settings},
ipc::ipc_server::{gen_args_vec, gen_env_vec},
ipc::ipc_server::gen_env_vec,
state::AppState,
subsystem::hid::WheelDelta,
windowing::{
@@ -440,7 +441,11 @@ impl DashInterface<AppState> for DashInterfaceLive {
) -> anyhow::Result<WvrProcessHandle> {
let wvr_server = app.wvr_server.as_mut().unwrap();
let args_vec = gen_args_vec(&params.args);
let mut argv_iter = params.argv.iter().map(String::as_str);
let exec_path = argv_iter
.next()
.context("No executable for process_launch")?;
let exec_args = argv_iter.collect::<Box<_>>();
let env_vec = gen_env_vec(&params.env);
if auto_start {
@@ -451,8 +456,8 @@ impl DashInterface<AppState> for DashInterfaceLive {
wvr_server
.spawn_process(
&params.name,
&params.exec,
&args_vec,
exec_path,
&exec_args,
&env_vec,
params.resolution,
params.pos_mode,
+63 -19
View File
@@ -31,6 +31,9 @@ use crate::{
const CURSOR_SIZE: f32 = 16. / 1440.;
// synchronize queue shared by the WlxCapture worker threads
static CAPTURE_QUEUE_MUTEX: Mutex<()> = Mutex::new(());
struct BufPass {
pass: WGfxPass<Vert2Uv>,
buf_vert: Arc<Buffer<Vert2Uv>>,
@@ -664,6 +667,17 @@ fn upload_image(
height: u32,
format: Format,
data: &[u8],
) -> Option<Arc<Image>> {
upload_image_with_row_length(me, width, height, format, data, 0)
}
fn upload_image_with_row_length(
me: &WlxCaptureIn,
width: u32,
height: u32,
format: Format,
data: &[u8],
buffer_row_length: u32,
) -> Option<Arc<Image>> {
let cmd_result = if me.use_capture_queue {
me.gfx
@@ -679,15 +693,23 @@ fn upload_image(
return None;
}
};
let image = match cmd_xfer.upload_image(width, height, format, data) {
Ok(x) => x,
Err(e) => {
log::error!("{}: Could not create vkImage: {:?}", me.name, e);
return None;
}
};
let image =
match cmd_xfer.upload_image_with_row_length(width, height, format, data, buffer_row_length)
{
Ok(x) => x,
Err(e) => {
log::error!("{}: Could not create vkImage: {:?}", me.name, e);
return None;
}
};
if let Err(e) = cmd_xfer.build_and_execute_now() {
let result = if me.use_capture_queue {
let _guard = CAPTURE_QUEUE_MUTEX.lock().unwrap(); // mutex poison only
cmd_xfer.build_and_execute_now()
} else {
cmd_xfer.build_and_execute_now()
};
if let Err(e) = result {
log::error!("{}: Could not execute upload: {:?}", me.name, e);
return None;
}
@@ -757,8 +779,22 @@ pub(super) fn receive_callback(me: &WlxCaptureIn, frame: WlxFrame) -> Option<Wlx
}
};
let data = unsafe { std::slice::from_raw_parts(frame.ptr as *const u8, frame.size) };
let image = upload_image(me, frame.format.width, frame.format.height, format, data)?;
// SAFETY: the MemPtr params & allocation and must be valid and must outlive the callback
let pixels = unsafe {
std::slice::from_raw_parts(
(frame.ptr as *const u8).add(frame.offset),
frame.size - frame.offset,
)
};
let buffer_row_length = (frame.stride / 4) as u32;
let image = upload_image_with_row_length(
me,
frame.format.width,
frame.format.height,
format,
pixels,
buffer_row_length,
)?;
Some(WlxCaptureOut {
image: me.gfx.create_image_view(image).ok()?,
@@ -811,6 +847,8 @@ pub(super) enum MainThreadFrame {
Cpu {
format: FrameFormat,
data: Vec<u8>,
offset: usize,
stride: usize,
mouse: Option<MouseMeta>,
},
Dmabuf {
@@ -870,12 +908,16 @@ where
MainThreadFrame::Cpu {
format,
data,
offset,
stride,
mouse,
} => {
let frame = wlx_frame::MemPtrFrame {
format,
ptr: data.as_ptr() as usize,
size: data.len(),
offset,
stride,
mouse,
};
receive_callback(
@@ -910,22 +952,24 @@ fn receive_callback_dummy(_: &DmaExporterProxy, frame: WlxFrame) -> Option<MainT
Some(MainThreadFrame::Cpu {
format: frame.format,
data: map.as_slice().to_vec(),
offset: frame.plane.offset as _,
stride: (frame.format.width as usize).checked_mul(4)?,
mouse: frame.mouse,
})
}
WlxFrame::MemPtr(frame) => {
let data = if frame.size == 0 {
Vec::new()
} else {
if frame.ptr == 0 {
log::error!("Main-thread CPU capture received a null MemPtr frame");
return None;
}
unsafe { std::slice::from_raw_parts(frame.ptr as *const u8, frame.size) }.to_vec()
// SAFETY: Allocation must be valid & must outlive the callback
let pixels = unsafe {
std::slice::from_raw_parts(
(frame.ptr as *const u8).add(frame.offset),
frame.size - frame.offset,
)
};
Some(MainThreadFrame::Cpu {
format: frame.format,
data,
data: pixels.to_vec(),
offset: frame.offset,
stride: frame.stride,
mouse: frame.mouse,
})
}
+1 -1
View File
@@ -179,7 +179,7 @@ impl AppState {
let ipc_server = ipc_server::WayVRServer::new()?;
let mut desktop_finder = DesktopFinder::new();
let mut desktop_finder = DesktopFinder::new(params.wgui_globals.i18n().get_locale());
desktop_finder.refresh();
#[cfg(feature = "pipewire")]
+6 -2
View File
@@ -320,8 +320,12 @@ impl OverlayWindowConfig {
return;
}
let to_hmd = (state.transform.translation - app.input_state.hmd.translation).normalize();
let watch_normal = state.transform.transform_vector3a(Vec3A::NEG_Z).normalize();
let to_hmd =
(state.transform.translation - app.input_state.hmd.translation).normalize_or_zero();
let watch_normal = state
.transform
.transform_vector3a(Vec3A::NEG_Z)
.normalize_or_zero();
let dot = to_hmd.dot(watch_normal);
let range = (app.session.config.watch_view_angle_max
+1 -1
View File
@@ -15,7 +15,7 @@ clap.workspace = true
log.workspace = true
serde.workspace = true
serde_json.workspace = true
shell-words.workspace = true
smol.workspace = true
env_logger = "0.11.11"
shell-words = "1.1.1"
+2 -4
View File
@@ -107,13 +107,12 @@ pub async fn wvr_process_terminate(
#[allow(clippy::too_many_arguments)]
pub async fn wvr_process_launch(
state: &mut WayVRClientState,
exec: String,
argv: Vec<String>,
name: String,
env: Vec<String>,
resolution: [u32; 2],
pos_mode: PositionMode,
icon: Option<String>,
args: String,
userdata: HashMap<String, String>,
) {
handle_result(
@@ -123,9 +122,8 @@ pub async fn wvr_process_launch(
state.serial_generator.increment_get(),
packet_client::WvrProcessLaunchParams {
env,
exec,
argv,
name,
args,
resolution,
pos_mode,
icon,
+8 -10
View File
@@ -157,13 +157,12 @@ async fn run_once(state: &mut WayVRClientState, args: Args) -> anyhow::Result<()
wvr_process_terminate(state, handle).await;
}
Subcommands::ProcessLaunch {
exec,
name,
env,
resolution,
pos,
icon,
args,
command,
} => {
let env = env.split(",").map(|s| s.to_string()).collect::<Vec<_>>();
let resolution = resolution
@@ -181,13 +180,12 @@ async fn run_once(state: &mut WayVRClientState, args: Args) -> anyhow::Result<()
wvr_process_launch(
state,
exec,
command,
name,
env,
resolution,
pos_mode,
icon,
args,
HashMap::new(),
)
.await;
@@ -316,17 +314,17 @@ enum Subcommands {
/// Environment variables, separated by comma
#[arg(short, long, default_value = "")]
env: String,
/// Executable to run
exec: String,
#[arg(default_value = "1920x1080")]
#[arg(short, long, default_value = "1920x1080")]
resolution: String,
/// Default positioning
#[arg(short, long, default_value = "anchored")]
pos: PosModeEnum,
/// Absolute path to the app icon
#[arg(short, long, default_value = "")]
icon: Option<String>,
/// Arguments to pass to executable
#[arg(default_value = "")]
args: String,
/// Executable and optional arguments
#[arg(required = true, num_args = 1..)]
command: Vec<String>,
},
/// Trigger haptics on the user's controller
Haptics {
+18 -3
View File
@@ -139,6 +139,17 @@ impl CommandBufferBuilder<CmdBufGfx> {
impl CommandBufferBuilder<CmdBufXfer> {
pub fn upload_image(&mut self, width: u32, height: u32, format: Format, data: &[u8]) -> anyhow::Result<Arc<Image>> {
self.upload_image_with_row_length(width, height, format, data, 0)
}
pub fn upload_image_with_row_length(
&mut self,
width: u32,
height: u32,
format: Format,
data: &[u8],
buffer_row_length: u32,
) -> anyhow::Result<Arc<Image>> {
let image = self.graphics.create_image(ImageCreateInfo::new_2d(
width,
height,
@@ -149,9 +160,13 @@ impl CommandBufferBuilder<CmdBufXfer> {
.graphics
.empty_buffer::<u8>(BufferUsage::TRANSFER_SRC, data.len() as u64)?;
staging.write()?.copy_from_slice(data);
self
.raw
.copy_buffer_to_image(&staging.raw, &image.raw, [0, 0, 0], [width, height, 1]);
self.raw.copy_buffer_to_image_with_row_length(
&staging.raw,
&image.raw,
[0, 0, 0],
[width, height, 1],
buffer_row_length,
);
self.keepalive.push(Box::new(staging));
self.keepalive.push(Box::new(image.clone()));
Ok(image)
+12 -1
View File
@@ -2494,11 +2494,22 @@ impl RawCommandBuffer {
image: &Arc<RawImage>,
offset: [u32; 3],
extent: [u32; 3],
) {
self.copy_buffer_to_image_with_row_length(buffer, image, offset, extent, 0);
}
pub(super) fn copy_buffer_to_image_with_row_length(
&mut self,
buffer: &RawBuffer,
image: &Arc<RawImage>,
offset: [u32; 3],
extent: [u32; 3],
buffer_row_length: u32,
) {
self.transition_image(image, ImageLayout::TransferDst);
let regions = [vk::BufferImageCopy::default()
.buffer_offset(0)
.buffer_row_length(0)
.buffer_row_length(buffer_row_length)
.buffer_image_height(0)
.image_subresource(vk::ImageSubresourceLayers {
aspect_mask: vk::ImageAspectFlags::COLOR,
+10
View File
@@ -69,6 +69,16 @@ pub trait LangsList {
}
impl Locale {
#[must_use]
/// Returns a clone of `self` without `region`
pub fn without_region(&self) -> Locale {
Locale {
lang: self.lang.clone(),
region: None,
matched: self.matched.clone(),
}
}
fn match_locale<'o>(
default_lang: &'static str,
lang: &str,
+3 -1
View File
@@ -31,7 +31,9 @@ wayland-protocols = {
drm-fourcc = "2.2.0"
rxscreen = {
version = "0.1.7",
git = "https://github.com/galister/rxscreen.git",
rev = "7197e1de6622b65859e6f2060b5750185e0dad2a",
# version = "0.1.7",
features = [
"mouse",
"shm",
+2
View File
@@ -128,6 +128,8 @@ pub struct MemPtrFrame {
pub format: FrameFormat,
pub ptr: usize,
pub size: usize,
pub offset: usize,
pub stride: usize,
pub mouse: Option<MouseMeta>,
}
+16 -2
View File
@@ -376,10 +376,24 @@ where
}
}
DataType::MemPtr => {
let raw = datas[0].as_raw();
let chunk = datas[0].chunk();
let offset = chunk.offset() as usize;
let chunk_size = chunk.size() as usize;
let Some(size) = offset.checked_add(chunk_size) else {
log::error!("{name}: PipeWire chunk range overflow");
return;
};
if size > raw.maxsize as usize {
log::error!("{name}: PipeWire chunk extends beyond allocation");
return;
}
let memptr = MemPtrFrame {
format: *format,
ptr: datas[0].as_raw().data as _,
size: datas[0].chunk().size() as _,
ptr: raw.data as _,
size,
offset,
stride: chunk.stride() as _,
mouse: mouse_meta,
};
+2
View File
@@ -109,6 +109,8 @@ where
transform: Transform::Normal,
},
ptr: unsafe { image.as_ptr() as _ },
offset: 0,
stride: image.bytes_per_line() as _,
size,
mouse: d.root_mouse_position().and_then(|root_pos| {
monitor.mouse_to_local(root_pos).map(|(x, y)| MouseMeta {
+1 -1
View File
@@ -24,6 +24,7 @@ smol.workspace = true
strum.workspace = true
xdg.workspace = true
freedesktop-desktop-entry = "0.8.4"
glob = "0.3.3"
identicons-svg = "0.1.0"
rodio = {
@@ -31,6 +32,5 @@ rodio = {
default-features = false,
features = ["hound", "mp3", "playback", "recording"]
}
rust-ini = "0.21.3"
walkdir = "2.5.0"
wgui = { path = "../wgui/" }
+3 -3
View File
@@ -5,7 +5,7 @@ use wayvr_ipc::{
packet_client::WvrProcessLaunchParams,
packet_server::{WvrProcess, WvrProcessHandle, WvrWindow, WvrWindowHandle},
};
use wgui::gfx::WGfx;
use wgui::{gfx::WGfx, i18n::Locale};
use crate::{
config::GeneralConfig,
@@ -73,7 +73,7 @@ pub struct DashInterfaceEmulated {
}
impl DashInterfaceEmulated {
pub fn new(gfx: Arc<WGfx>) -> Self {
pub fn new(gfx: Arc<WGfx>, locale: &Locale) -> Self {
let mut processes = DenseSlotMap::<EmuProcessHandle, EmuProcess>::default();
let process_handle = processes.insert(EmuProcess {
name: String::from("My app"),
@@ -85,7 +85,7 @@ impl DashInterfaceEmulated {
visible: true,
});
let mut desktop_finder = DesktopFinder::new();
let mut desktop_finder = DesktopFinder::new(locale);
desktop_finder.refresh();
// Use serde defaults
+67 -86
View File
@@ -10,37 +10,35 @@ use std::{
time::Instant,
};
use crate::cache_dir;
use crate::{cache_dir, locale::Language};
use anyhow::Context;
use ini::Ini;
use freedesktop_desktop_entry::DesktopEntry as FreedesktopDesktopEntry;
use serde::{Deserialize, Serialize};
use walkdir::WalkDir;
use wgui::i18n::Locale;
struct DesktopEntryOwned {
app_id: String,
exec_path: String,
exec_args: String,
app_name: String,
icon_path: Option<String>,
categories: Vec<String>,
app_id: Box<str>,
exec_argv: Box<[Box<str>]>,
app_name: Box<str>,
icon_path: Option<Box<str>>,
categories: Box<[Box<str>]>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DesktopEntry {
pub exec_path: Rc<str>,
pub exec_args: Rc<str>,
pub exec_argv: Rc<[Rc<str>]>,
pub app_id: Rc<str>,
pub app_name: Rc<str>,
pub icon_path: Option<Rc<str>>,
pub categories: Vec<Rc<str>>,
pub categories: Rc<[Rc<str>]>,
}
impl From<DesktopEntryOwned> for DesktopEntry {
fn from(value: DesktopEntryOwned) -> Self {
Self {
app_id: value.app_id.into(),
exec_path: value.exec_path.into(),
exec_args: value.exec_args.into(),
exec_argv: value.exec_argv.into_iter().map(|x| x.into()).collect(),
app_name: value.app_name.into(),
icon_path: value.icon_path.map(|x| x.into()),
categories: value.categories.into_iter().map(|x| x.into()).collect(),
@@ -60,17 +58,18 @@ struct DesktopFinderParams {
size_preferences: Vec<&'static OsStr>,
icon_folders: Vec<String>,
app_folders: Vec<String>,
langs: Vec<String>,
}
pub struct DesktopFinder {
params: Arc<DesktopFinderParams>,
entry_cache: HashMap<String, DesktopEntry>,
entry_cache: HashMap<String, Rc<DesktopEntry>>,
bg_task: Option<JoinHandle<HashMap<String, DesktopEntryOwned>>>,
}
impl DesktopFinder {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
pub fn new(locale: &Locale) -> Self {
let xdg = xdg::BaseDirectories::new();
let mut app_folders = vec![];
@@ -107,11 +106,21 @@ impl DesktopFinder {
.map(OsStr::new)
.collect();
let langs = {
let main_lang = locale.without_region().to_string();
if main_lang == Language::get_default().code() {
vec![main_lang]
} else {
vec![main_lang, Language::get_default().code().to_string()]
}
};
Self {
params: Arc::new(DesktopFinderParams {
app_folders,
icon_folders,
size_preferences,
langs,
}),
entry_cache: HashMap::new(),
bg_task: None,
@@ -146,79 +155,53 @@ impl DesktopFinder {
}
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;
};
let file_path = entry.path().display().to_string();
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,
let desktop = match FreedesktopDesktopEntry::from_path(entry.path(), Some(&params.langs)) {
Ok(desktop) => desktop,
Err(e) => {
log::debug!("Failed to read INI for .desktop file {}: {:?}, skipping", file_path, e);
log::debug!("Failed to read .desktop file {}: {:?}, skipping", file_path, e);
continue;
}
};
let Some(section) = ini.section(Some("Desktop Entry")) else {
if desktop.groups.desktop_entry().is_none() {
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"
{
// prefer entries from earlier data directories
let app_id = desktop.id();
if known_files.contains(app_id) {
continue;
}
if let Some(x) = section.get("NoDisplay")
&& x.eq_ignore_ascii_case("true")
{
continue; // This application is hidden
if desktop.only_show_in().is_some() || desktop.terminal() || desktop.no_display() || desktop.hidden() {
continue;
}
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 {
let Some(exec) = desktop.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;
}
if CMD_BLOCKLIST.iter().any(|blocked| exec.contains(blocked)) {
continue;
}
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 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) = section.get("Name") else {
let Some(app_name) = desktop.name(&params.langs) else {
log::debug!(
"Failed to get desktop entry application name for file {}, skipping",
file_path
@@ -226,33 +209,31 @@ impl DesktopFinder {
continue;
};
let icon_path = section
.get("Icon")
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::<String>::new();
if let Some(categories) = section.get("Categories") {
for cat in categories.split(";") {
if CATEGORY_TYPE_BLOCKLIST.contains(&cat) {
continue 'entries;
}
vec_categories.push(String::from(cat));
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(file_name.to_string());
known_files.insert(app_id.to_owned());
entries.insert(
app_id.clone(),
app_id.to_owned(),
DesktopEntryOwned {
app_id,
app_name: String::from(app_name),
exec_path: String::from(exec_path),
exec_args: exec_args.join(" "),
icon_path,
categories: vec_categories,
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(),
},
);
}
@@ -331,15 +312,15 @@ impl DesktopFinder {
self.entry_cache.clear();
for (app_id, entry) in entries {
self.entry_cache.insert(app_id, entry.into());
self.entry_cache.insert(app_id, Rc::new(entry.into()));
}
}
pub fn get_cached_entry(&self, app_id: &str) -> Option<&DesktopEntry> {
pub fn get_cached_entry(&self, app_id: &str) -> Option<&Rc<DesktopEntry>> {
self.entry_cache.get(app_id)
}
pub fn find_entries(&mut self) -> HashMap<String, DesktopEntry> {
pub fn find_entries(&mut self) -> HashMap<String, Rc<DesktopEntry>> {
self.wait_for_entries();
self.entry_cache.clone()
}
+4 -4
View File
@@ -95,10 +95,10 @@ pub struct OpenXrInputProfile {
fn strip_axis_component(path: &str) -> String {
if let Some((prefix, suffix)) = path.rsplit_once('/')
&& (suffix == "x" || suffix == "y")
&& (prefix.ends_with("/thumbstick") || prefix.ends_with("/trackpad") || prefix.ends_with("/joystick"))
{
return prefix.to_owned();
}
&& (prefix.ends_with("/thumbstick") || prefix.ends_with("/trackpad") || prefix.ends_with("/joystick"))
{
return prefix.to_owned();
}
path.to_owned()
}