dash-frontend: Application list

This commit is contained in:
Aleksander
2025-09-28 13:13:37 +02:00
parent eb12a6a319
commit b5a3ba2954
18 changed files with 907 additions and 115 deletions

View File

@@ -8,7 +8,7 @@ use wgui::{
globals::WguiGlobals,
i18n::Translation,
layout::{LayoutParams, RcLayout, WidgetID},
parser::{ParseDocumentParams, ParserState},
parser::{Fetchable, ParseDocumentParams, ParserState},
widget::label::WidgetLabel,
};
@@ -19,6 +19,7 @@ use crate::tab::{
mod assets;
mod tab;
mod util;
mod various;
pub struct Frontend {

View File

@@ -1,10 +1,23 @@
use wgui::parser::{ParseDocumentParams, ParserState};
use std::{collections::HashMap, rc::Rc};
use crate::tab::{Tab, TabParams, TabType};
use wgui::{
components::{self, button::ComponentButton},
layout::WidgetPair,
parser::{Fetchable, ParseDocumentParams, ParserData, ParserState},
taffy::{self, Dimension, prelude::length},
};
use crate::{
tab::{Tab, TabParams, TabType},
util::{self, desktop_finder::DesktopEntry},
};
pub struct TabApps {
#[allow(dead_code)]
pub state: ParserState,
entries: Vec<DesktopEntry>,
app_list: AppList,
}
impl Tab for TabApps {
@@ -13,19 +26,106 @@ impl Tab for TabApps {
}
}
#[derive(Default)]
struct AppList {
data: Vec<ParserData>,
}
impl TabApps {
pub fn new(params: TabParams) -> anyhow::Result<Self> {
let state = wgui::parser::parse_from_assets(
&ParseDocumentParams {
globals: params.globals.clone(),
path: "gui/tab/apps.xml",
extra: Default::default(),
},
params.layout,
params.listeners,
params.parent_id,
pub fn new(mut tab_params: TabParams) -> anyhow::Result<Self> {
let doc_params = &ParseDocumentParams {
globals: tab_params.globals.clone(),
path: "gui/tab/apps.xml",
extra: Default::default(),
};
let mut state = wgui::parser::parse_from_assets(
doc_params,
tab_params.layout,
tab_params.listeners,
tab_params.parent_id,
)?;
Ok(Self { state })
gtk::init()?;
let entries = util::desktop_finder::find_entries()?;
let app_list_parent = state.fetch_widget(&tab_params.layout.state, "app_list_parent")?;
let mut app_list = AppList::default();
app_list.mount_entries(&entries, &mut state, doc_params, &mut tab_params, &app_list_parent)?;
Ok(Self {
app_list,
state,
entries,
})
}
}
impl AppList {
fn mount_entry(
&mut self,
parser_state: &mut ParserState,
doc_params: &ParseDocumentParams,
params: &mut TabParams,
list_parent: &WidgetPair,
entry: &DesktopEntry,
) -> anyhow::Result<()> {
let mut template_params = HashMap::new();
// entry icon
template_params.insert(
Rc::from("src_ext"),
entry
.icon_path
.as_ref()
.map_or_else(|| Rc::from(""), |icon_path| Rc::from(icon_path.as_str())),
);
// entry fallback (question mark) icon
template_params.insert(
Rc::from("src"),
if entry.icon_path.is_none() {
Rc::from("dashboard/terminal.svg")
} else {
Rc::from("")
},
);
template_params.insert(Rc::from("name"), Rc::from(entry.app_name.as_str()));
let data = parser_state.parse_template(
doc_params,
"AppEntry",
params.layout,
params.listeners,
list_parent.id,
template_params,
)?;
let button = data.fetch_component_as::<ComponentButton>("button")?;
button.on_click(Box::new(move |common, evt| {
log::info!("click");
Ok(())
}));
self.data.push(data);
Ok(())
}
fn mount_entries(
&mut self,
entries: &[DesktopEntry],
parser_state: &mut ParserState,
doc_params: &ParseDocumentParams,
params: &mut TabParams,
list_parent: &WidgetPair,
) -> anyhow::Result<()> {
for entry in entries {
self.mount_entry(parser_state, doc_params, params, list_parent, entry)?;
}
Ok(())
}
}

View File

@@ -1,7 +1,7 @@
use wgui::{
components::button::ComponentButton,
i18n::Translation,
parser::{ParseDocumentParams, ParserState},
parser::{Fetchable, ParseDocumentParams, ParserState},
widget::label::WidgetLabel,
};

View File

@@ -0,0 +1,129 @@
use gio::prelude::{AppInfoExt, IconExt};
use gtk::traits::IconThemeExt;
#[derive(Debug)]
pub struct DesktopEntry {
pub exec_path: String,
pub exec_args: Vec<String>,
pub app_name: String,
pub icon_path: Option<String>,
pub categories: Vec<String>,
}
pub struct EntrySearchCell {
pub exec_path: String,
pub exec_args: Vec<String>,
pub app_name: String,
pub icon_name: Option<String>,
pub categories: Vec<String>,
}
const CMD_BLACKLIST: [&str; 1] = [
"lsp-plugins", // LSP Plugins collection. They clutter the application list a lot
];
const CATEGORY_TYPE_BLACKLIST: [&str; 5] = ["GTK", "Qt", "X-XFCE", "X-Bluetooth", "ConsoleOnly"];
pub fn find_entries() -> anyhow::Result<Vec<DesktopEntry>> {
let Some(icon_theme) = gtk::IconTheme::default() else {
anyhow::bail!("Failed to get current icon theme information");
};
let mut res = Vec::<DesktopEntry>::new();
let info = gio::AppInfo::all();
log::debug!("app entry count {}", info.len());
'outer: for app_entry in info {
let Some(app_entry_id) = app_entry.id() else {
log::warn!(
"failed to get desktop entry ID for application named \"{}\"",
app_entry.name()
);
continue;
};
let Some(desktop_app) = gio::DesktopAppInfo::new(&app_entry_id) else {
log::warn!(
"failed to find desktop app file from application named \"{}\"",
app_entry.name()
);
continue;
};
if desktop_app.is_nodisplay() || desktop_app.is_hidden() {
continue;
}
let Some(cmd) = desktop_app.commandline() else {
continue;
};
let name = String::from(desktop_app.name());
let exec = String::from(cmd.to_string_lossy());
for blacklisted in CMD_BLACKLIST {
if exec.contains(blacklisted) {
continue 'outer;
}
}
let (exec_path, exec_args) = match exec.split_once(" ") {
Some((left, right)) => (
String::from(left),
right
.split(" ")
.filter(|arg| !arg.starts_with('%')) // exclude arguments like "%f"
.map(String::from)
.collect(),
),
None => (exec, Vec::new()),
};
let icon_path = match desktop_app.icon() {
Some(icon) => {
if let Some(icon_str) = icon.to_string() {
if let Some(s_icon) = icon_theme.lookup_icon(&icon_str, 128, gtk::IconLookupFlags::GENERIC_FALLBACK) {
s_icon.filename().map(|p| String::from(p.to_string_lossy()))
} else {
None
}
} else {
None
}
}
None => None,
};
let categories: Vec<String> = match desktop_app.categories() {
Some(categories) => categories
.split(";")
.filter(|s| !s.is_empty())
.filter(|s| {
for b in CATEGORY_TYPE_BLACKLIST {
if *s == b {
return false;
}
}
true
})
.map(String::from)
.collect(),
None => Vec::new(),
};
let entry = DesktopEntry {
app_name: name,
categories,
exec_path,
exec_args,
icon_path,
};
res.push(entry);
}
Ok(res)
}

View File

@@ -0,0 +1 @@
pub mod desktop_finder;