dash-frontend: Application list
This commit is contained in:
@@ -10,3 +10,5 @@ glam = { workspace = true }
|
||||
log = { workspace = true }
|
||||
rust-embed = { workspace = true }
|
||||
chrono = "0.4.42"
|
||||
gio = "0.21.2"
|
||||
gtk = "0.18.2"
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
<layout>
|
||||
<include src="t_tab_title.xml" />
|
||||
|
||||
<template name="AppEntry">
|
||||
<Button
|
||||
id="button" width="116" max_width="140" min_height="100" flex_grow="1"
|
||||
flex_direction="column" overflow="visible" align_items="center" justify_content="center" gap="8">
|
||||
<div>
|
||||
<sprite src="${src}" src_ext="${src_ext}" width="64" height="64" />
|
||||
</div>
|
||||
<div align_items="center" justify_content="center">
|
||||
<label weight="bold" text="${name}" size="12" />
|
||||
</div>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<elements>
|
||||
<TabTitle translation="APPLICATIONS" icon="dashboard/apps.svg" />
|
||||
<!-- placeholders for now -->
|
||||
@@ -17,5 +30,12 @@
|
||||
<label text="Search" color="#FFFFFF88" weight="bold" />
|
||||
</rectangle>
|
||||
</div>
|
||||
<div
|
||||
id="app_list_parent"
|
||||
flex_direction="row"
|
||||
flex_wrap="wrap"
|
||||
justify_content="stretch"
|
||||
gap="4"
|
||||
/>
|
||||
</elements>
|
||||
</layout>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use wgui::{
|
||||
components::button::ComponentButton,
|
||||
i18n::Translation,
|
||||
parser::{ParseDocumentParams, ParserState},
|
||||
parser::{Fetchable, ParseDocumentParams, ParserState},
|
||||
widget::label::WidgetLabel,
|
||||
};
|
||||
|
||||
|
||||
129
dash-frontend/src/util/desktop_finder.rs
Normal file
129
dash-frontend/src/util/desktop_finder.rs
Normal 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)
|
||||
}
|
||||
1
dash-frontend/src/util/mod.rs
Normal file
1
dash-frontend/src/util/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod desktop_finder;
|
||||
Reference in New Issue
Block a user