1
0
Fork 0
mirror of https://github.com/Zedfrigg/ironbar.git synced 2025-08-16 14:21:03 +02:00

refactor: overhaul .desktop and image resolver systems

Rewrites the desktop file parser code and image resolver code to introduce caching system and make fully async. They should be much faster now.

BREAKING CHANGE: The `icon_theme` setting has been moved from per-bar to top-level
This commit is contained in:
Jake Stanger 2025-05-25 15:50:21 +01:00
parent ca524f19f6
commit 3e55d87c3a
No known key found for this signature in database
GPG key ID: C51FC8F9CB0BEA61
26 changed files with 1840 additions and 600 deletions

View file

@ -120,6 +120,7 @@ tokio = { version = "1.45.0", features = [
"sync",
"io-util",
"net",
"fs"
] }
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }

View file

@ -125,6 +125,12 @@ where
fn recv_glib<F>(self, f: F)
where
F: FnMut(T) + 'static;
/// Like [`BroadcastReceiverExt::recv_glib`], but the closure must return a [`Future`].
fn recv_glib_async<Fn, F>(self, f: Fn)
where
Fn: FnMut(T) -> F + 'static,
F: Future;
}
impl<T> BroadcastReceiverExt<T> for broadcast::Receiver<T>
@ -152,4 +158,29 @@ where
}
});
}
fn recv_glib_async<Fn, F>(mut self, mut f: Fn)
where
Fn: FnMut(T) -> F + 'static,
F: Future,
{
glib::spawn_future_local(async move {
loop {
match self.recv().await {
Ok(val) => {
f(val).await;
}
Err(broadcast::error::RecvError::Lagged(count)) => {
tracing::warn!(
"Channel lagged behind by {count}, this may result in unexpected or broken behaviour"
);
}
Err(err) => {
tracing::error!("{err:?}");
break;
}
}
}
});
}
}

View file

@ -295,12 +295,6 @@ pub struct BarConfig {
#[serde(default)]
pub autohide: Option<u64>,
/// The name of the GTK icon theme to use.
/// Leave unset to use the default Adwaita theme.
///
/// **Default**: `null`
pub icon_theme: Option<String>,
/// An array of modules to append to the start of the bar.
/// Depending on the orientation, this is either the top of the left edge.
///
@ -348,7 +342,6 @@ impl Default for BarConfig {
height: default_bar_height(),
start_hidden: None,
autohide: None,
icon_theme: None,
#[cfg(feature = "label")]
start: Some(vec![ModuleConfig::Label(
LabelModule::new(" Using default config".to_string()).into(),
@ -403,6 +396,12 @@ pub struct Config {
/// Providing this option overrides the single, global `bar` option.
pub monitors: Option<HashMap<String, MonitorConfig>>,
/// The name of the GTK icon theme to use.
/// Leave unset to use the default Adwaita theme.
///
/// **Default**: `null`
pub icon_theme: Option<String>,
/// Map of app IDs (or classes) to icon names,
/// overriding the app's default icon.
///

View file

@ -1,36 +1,267 @@
use std::collections::{HashMap, HashSet};
use crate::spawn;
use color_eyre::Result;
use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use tracing::warn;
use std::sync::Arc;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::sync::Mutex;
use tracing::debug;
use walkdir::{DirEntry, WalkDir};
use crate::lock;
type DesktopFile = HashMap<String, Vec<String>>;
fn desktop_files() -> &'static Mutex<HashMap<PathBuf, DesktopFile>> {
static DESKTOP_FILES: OnceLock<Mutex<HashMap<PathBuf, DesktopFile>>> = OnceLock::new();
DESKTOP_FILES.get_or_init(|| Mutex::new(HashMap::new()))
#[derive(Debug, Clone)]
enum DesktopFileRef {
Unloaded(PathBuf),
Loaded(DesktopFile),
}
fn desktop_files_look_out_keys() -> &'static HashSet<&'static str> {
static DESKTOP_FILES_LOOK_OUT_KEYS: OnceLock<HashSet<&'static str>> = OnceLock::new();
DESKTOP_FILES_LOOK_OUT_KEYS
.get_or_init(|| HashSet::from(["Name", "StartupWMClass", "Exec", "Icon"]))
impl DesktopFileRef {
async fn get(&mut self) -> Result<DesktopFile> {
match self {
DesktopFileRef::Unloaded(path) => {
let (tx, rx) = tokio::sync::oneshot::channel();
let path = path.clone();
spawn(async move { tx.send(Self::load(&path).await) });
let file = rx.await??;
*self = DesktopFileRef::Loaded(file.clone());
Ok(file)
}
DesktopFileRef::Loaded(file) => Ok(file.clone()),
}
}
async fn load(file_path: &Path) -> Result<DesktopFile> {
debug!("loading applications file: {}", file_path.display());
let file = tokio::fs::File::open(file_path).await?;
let mut desktop_file = DesktopFile::new(
file_path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string(),
);
let mut lines = BufReader::new(file).lines();
let mut has_name = false;
let mut has_type = false;
let mut has_wm_class = false;
let mut has_exec = false;
let mut has_icon = false;
while let Ok(Some(line)) = lines.next_line().await {
let Some((key, value)) = line.split_once('=') else {
continue;
};
match key {
"Name" => {
desktop_file.name = Some(value.to_string());
has_name = true;
}
"Type" => {
desktop_file.app_type = Some(value.to_string());
has_type = true;
}
"StartupWMClass" => {
desktop_file.startup_wm_class = Some(value.to_string());
has_wm_class = true;
}
"Exec" => {
desktop_file.exec = Some(value.to_string());
has_exec = true;
}
"Icon" => {
desktop_file.icon = Some(value.to_string());
has_icon = true;
}
_ => {}
}
// parsing complete - don't bother with the rest of the lines
if has_name && has_type && has_wm_class && has_exec && has_icon {
break;
}
}
Ok(desktop_file)
}
}
/// Finds directories that should contain `.desktop` files
/// and exist on the filesystem.
fn find_application_dirs() -> Vec<PathBuf> {
#[derive(Debug, Clone)]
pub struct DesktopFile {
pub file_name: String,
pub name: Option<String>,
pub app_type: Option<String>,
pub startup_wm_class: Option<String>,
pub exec: Option<String>,
pub icon: Option<String>,
}
impl DesktopFile {
fn new(file_name: String) -> Self {
Self {
file_name,
name: None,
app_type: None,
startup_wm_class: None,
exec: None,
icon: None,
}
}
}
type FileMap = HashMap<Box<str>, DesktopFileRef>;
/// Desktop file cache and resolver.
///
/// Files are lazy-loaded as required on resolution.
#[derive(Debug, Clone)]
pub struct DesktopFiles {
files: Arc<Mutex<FileMap>>,
}
impl Default for DesktopFiles {
fn default() -> Self {
Self::new()
}
}
impl DesktopFiles {
/// Creates a new instance,
/// scanning disk to generate a list of (unloaded) file refs in the process.
pub fn new() -> Self {
let desktop_files: FileMap = dirs()
.iter()
.flat_map(|path| files(path))
.map(|file| {
(
file.file_stem()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
.to_string()
.into(),
DesktopFileRef::Unloaded(file),
)
})
.collect();
debug!("resolved {} files", desktop_files.len());
Self {
files: Arc::new(Mutex::new(desktop_files)),
}
}
/// Attempts to locate a applications file by file name or contents.
///
/// Input should typically be the app id, app name or icon.
pub async fn find(&self, input: &str) -> Result<Option<DesktopFile>> {
let mut res = self.find_by_file_name(input).await?;
if res.is_none() {
res = self.find_by_file_contents(input).await?;
}
debug!("found match for app_id {input}: {}", res.is_some());
Ok(res)
}
/// Checks file names for an exact or partial match of the provided input.
async fn find_by_file_name(&self, input: &str) -> Result<Option<DesktopFile>> {
let mut files = self.files.lock().await;
let mut file_ref = files
.iter_mut()
.find(|&(name, _)| name.eq_ignore_ascii_case(input));
if file_ref.is_none() {
file_ref = files.iter_mut().find(
|&(name, _)| // this will attempt to find flatpak apps that are in the format
// `com.company.app` or `com.app.something`
input
.split(&[' ', ':', '@', '.', '_'][..])
.any(|part| name.eq_ignore_ascii_case(part)),
);
}
let file_ref = file_ref.map(|(_, file)| file);
if let Some(file_ref) = file_ref {
let file = file_ref.get().await?;
Ok(Some(file))
} else {
Ok(None)
}
}
/// Checks file contents for an exact or partial match of the provided input.
async fn find_by_file_contents(&self, app_id: &str) -> Result<Option<DesktopFile>> {
let mut files = self.files.lock().await;
// first pass - check name for exact match
for (_, file_ref) in files.iter_mut() {
let file = file_ref.get().await?;
if let Some(name) = &file.name {
if name.eq_ignore_ascii_case(app_id) {
return Ok(Some(file));
}
}
}
// second pass - check name for partial match
for (_, file_ref) in files.iter_mut() {
let file = file_ref.get().await?;
if let Some(name) = &file.name {
if name.to_lowercase().contains(app_id) {
return Ok(Some(file));
}
}
}
// third pass - check remaining fields for partial match
for (_, file_ref) in files.iter_mut() {
let file = file_ref.get().await?;
if let Some(name) = &file.exec {
if name.to_lowercase().contains(app_id) {
return Ok(Some(file));
}
}
if let Some(name) = &file.startup_wm_class {
if name.to_lowercase().contains(app_id) {
return Ok(Some(file));
}
}
if let Some(name) = &file.icon {
if name.to_lowercase().contains(app_id) {
return Ok(Some(file));
}
}
}
Ok(None)
}
}
/// Gets a list of paths to all directories
/// containing `.applications` files.
fn dirs() -> Vec<PathBuf> {
let mut dirs = vec![
PathBuf::from("/usr/share/applications"), // system installed apps
PathBuf::from("/var/lib/flatpak/exports/share/applications"), // flatpak apps
];
let xdg_dirs = env::var_os("XDG_DATA_DIRS");
if let Some(xdg_dirs) = xdg_dirs {
let xdg_dirs = env::var("XDG_DATA_DIRS");
if let Ok(xdg_dirs) = xdg_dirs {
for mut xdg_dir in env::split_paths(&xdg_dirs) {
xdg_dir.push("applications");
dirs.push(xdg_dir);
@ -43,157 +274,66 @@ fn find_application_dirs() -> Vec<PathBuf> {
dirs.push(user_dir);
}
dirs.into_iter().filter(|dir| dir.exists()).collect()
dirs.into_iter().filter(|dir| dir.exists()).rev().collect()
}
/// Finds all the desktop files
fn find_desktop_files() -> Vec<PathBuf> {
let dirs = find_application_dirs();
dirs.into_iter()
.flat_map(|dir| {
WalkDir::new(dir)
.max_depth(5)
.into_iter()
.filter_map(Result::ok)
.map(DirEntry::into_path)
.filter(|file| file.is_file() && file.extension().unwrap_or_default() == "desktop")
})
/// Gets a list of all `.applications` files in the provided directory.
///
/// The directory is recursed to a maximum depth of 5.
fn files(dir: &Path) -> Vec<PathBuf> {
WalkDir::new(dir)
.max_depth(5)
.into_iter()
.filter_map(Result::ok)
.map(DirEntry::into_path)
.filter(|file| file.is_file() && file.extension().unwrap_or_default() == "desktop")
.collect()
}
/// Attempts to locate a `.desktop` file for an app id
pub fn find_desktop_file(app_id: &str) -> Option<PathBuf> {
// this is necessary to invalidate the cache
let files = find_desktop_files();
#[cfg(test)]
mod tests {
use super::*;
find_desktop_file_by_filename(app_id, &files)
.or_else(|| find_desktop_file_by_filedata(app_id, &files))
}
/// Finds the correct desktop file using a simple condition check
fn find_desktop_file_by_filename(app_id: &str, files: &[PathBuf]) -> Option<PathBuf> {
let with_names = files
.iter()
.map(|f| {
(
f,
f.file_stem()
.unwrap_or_default()
.to_string_lossy()
.to_lowercase(),
)
})
.collect::<Vec<_>>();
with_names
.iter()
// first pass - check for exact match
.find(|(_, name)| name.eq_ignore_ascii_case(app_id))
// second pass - check for substring
.or_else(|| {
with_names.iter().find(|(_, name)| {
// this will attempt to find flatpak apps that are in the format
// `com.company.app` or `com.app.something`
app_id
.split(&[' ', ':', '@', '.', '_'][..])
.any(|part| name.eq_ignore_ascii_case(part))
})
})
.map(|(file, _)| file.into())
}
/// Finds the correct desktop file using the keys in `DESKTOP_FILES_LOOK_OUT_KEYS`
fn find_desktop_file_by_filedata(app_id: &str, files: &[PathBuf]) -> Option<PathBuf> {
let app_id = &app_id.to_lowercase();
let mut desktop_files_cache = lock!(desktop_files());
let files = files
.iter()
.filter_map(|file| {
let parsed_desktop_file = parse_desktop_file(file)?;
desktop_files_cache.insert(file.clone(), parsed_desktop_file.clone());
Some((file.clone(), parsed_desktop_file))
})
.collect::<Vec<_>>();
let file = files
.iter()
// first pass - check name key for exact match
.find(|(_, desktop_file)| {
desktop_file
.get("Name")
.is_some_and(|names| names.iter().any(|name| name.eq_ignore_ascii_case(app_id)))
})
// second pass - check name key for substring
.or_else(|| {
files.iter().find(|(_, desktop_file)| {
desktop_file.get("Name").is_some_and(|names| {
names
.iter()
.any(|name| name.to_lowercase().contains(app_id))
})
})
})
// third pass - check all keys for substring
.or_else(|| {
files.iter().find(|(_, desktop_file)| {
desktop_file
.values()
.flatten()
.any(|value| value.to_lowercase().contains(app_id))
})
});
file.map(|(path, _)| path).cloned()
}
/// Parses a desktop file into a hashmap of keys/vector(values).
fn parse_desktop_file(path: &Path) -> Option<DesktopFile> {
let Ok(file) = fs::read_to_string(path) else {
warn!("Couldn't Open File: {}", path.display());
return None;
};
let mut desktop_file: DesktopFile = DesktopFile::new();
file.lines()
.filter_map(|line| {
let (key, value) = line.split_once('=')?;
let key = key.trim();
let value = value.trim();
if desktop_files_look_out_keys().contains(key) {
Some((key, value))
} else {
None
}
})
.for_each(|(key, value)| {
desktop_file
.entry(key.to_string())
.or_default()
.push(value.to_string());
});
Some(desktop_file)
}
/// Attempts to get the icon name from the app's `.desktop` file.
pub fn get_desktop_icon_name(app_id: &str) -> Option<String> {
let path = find_desktop_file(app_id)?;
let mut desktop_files_cache = lock!(desktop_files());
let desktop_file = match desktop_files_cache.get(&path) {
Some(desktop_file) => desktop_file,
_ => desktop_files_cache
.entry(path.clone())
.or_insert_with(|| parse_desktop_file(&path).expect("desktop_file")),
};
let mut icons = desktop_file.get("Icon").into_iter().flatten();
icons.next().map(std::string::ToString::to_string)
fn setup() {
unsafe {
let pwd = env::current_dir().unwrap();
env::set_var("XDG_DATA_DIRS", format!("{}/test-configs", pwd.display()));
}
}
#[tokio::test]
async fn find_by_filename() {
setup();
let desktop_files = DesktopFiles::new();
let file = desktop_files.find_by_file_name("firefox").await.unwrap();
assert!(file.is_some());
assert_eq!(file.unwrap().file_name, "firefox.desktop");
}
#[tokio::test]
async fn find_by_file_contents() {
setup();
let desktop_files = DesktopFiles::new();
let file = desktop_files.find_by_file_contents("427520").await.unwrap();
assert!(file.is_some());
assert_eq!(file.unwrap().file_name, "Factorio.desktop");
}
#[tokio::test]
async fn parser() {
let mut file_ref =
DesktopFileRef::Unloaded(PathBuf::from("test-configs/applications/firefox.desktop"));
let file = file_ref.get().await.unwrap();
assert_eq!(file.name, Some("Firefox".to_string()));
assert_eq!(file.icon, Some("firefox".to_string()));
assert_eq!(file.exec, Some("/usr/lib/firefox/firefox %u".to_string()));
assert_eq!(file.startup_wm_class, Some("firefox".to_string()));
assert_eq!(file.app_type, Some("Application".to_string()));
}
}

View file

@ -1,7 +1,7 @@
use super::ImageProvider;
use crate::gtk_helpers::{IronbarGtkExt, IronbarLabelExt};
use crate::image;
use gtk::prelude::*;
use gtk::{Button, IconTheme, Image, Label, Orientation};
use gtk::{Button, Image, Label, Orientation};
use std::ops::Deref;
#[derive(Debug, Clone)]
@ -29,27 +29,33 @@ pub struct IconButton {
feature = "workspaces",
))]
impl IconButton {
pub fn new(input: &str, icon_theme: &IconTheme, size: i32) -> Self {
pub fn new(input: &str, size: i32, image_provider: image::Provider) -> Self {
let button = Button::new();
let image = Image::new();
let label = Label::new(Some(input));
if ImageProvider::is_definitely_image_input(input) {
if image::Provider::is_explicit_input(input) {
image.add_class("image");
image.add_class("icon");
match ImageProvider::parse(input, icon_theme, false, size)
.map(|provider| provider.load_into_image(&image))
{
Some(_) => {
let image = image.clone();
let label = label.clone();
let button = button.clone();
let input = input.to_string(); // ew
glib::spawn_future_local(async move {
if let Ok(true) = image_provider
.load_into_image(&input, size, false, &image)
.await
{
button.set_image(Some(&image));
button.set_always_show_image(true);
}
None => {
} else {
button.set_child(Some(&label));
label.show();
}
}
});
} else {
button.set_child(Some(&label));
label.show();
@ -82,17 +88,17 @@ impl Deref for IconButton {
#[cfg(any(feature = "keyboard", feature = "music", feature = "workspaces"))]
pub struct IconLabel {
provider: image::Provider,
container: gtk::Box,
label: Label,
image: Image,
icon_theme: IconTheme,
size: i32,
}
#[cfg(any(feature = "keyboard", feature = "music", feature = "workspaces"))]
impl IconLabel {
pub fn new(input: &str, icon_theme: &IconTheme, size: i32) -> Self {
pub fn new(input: &str, size: i32, image_provider: &image::Provider) -> Self {
let container = gtk::Box::new(Orientation::Horizontal, 0);
let label = Label::builder().use_markup(true).build();
@ -106,21 +112,34 @@ impl IconLabel {
container.add(&image);
container.add(&label);
if ImageProvider::is_definitely_image_input(input) {
ImageProvider::parse(input, icon_theme, false, size)
.map(|provider| provider.load_into_image(&image));
if image::Provider::is_explicit_input(input) {
let image = image.clone();
let label = label.clone();
let image_provider = image_provider.clone();
image.show();
let input = input.to_string();
glib::spawn_future_local(async move {
let res = image_provider
.load_into_image(&input, size, false, &image)
.await;
if matches!(res, Ok(true)) {
image.show();
} else {
label.set_text(&input);
label.show();
}
});
} else {
label.set_text(input);
label.show();
}
Self {
provider: image_provider.clone(),
container,
label,
image,
icon_theme: icon_theme.clone(),
size,
}
}
@ -130,12 +149,26 @@ impl IconLabel {
let image = &self.image;
if let Some(input) = input {
if ImageProvider::is_definitely_image_input(input) {
ImageProvider::parse(input, &self.icon_theme, false, self.size)
.map(|provider| provider.load_into_image(image));
if image::Provider::is_explicit_input(input) {
let provider = self.provider.clone();
let size = self.size;
label.hide();
image.show();
let label = label.clone();
let image = image.clone();
let input = input.to_string();
glib::spawn_future_local(async move {
let res = provider.load_into_image(&input, size, false, &image).await;
if matches!(res, Ok(true)) {
label.hide();
image.show();
} else {
label.set_label_escaped(&input);
image.hide();
label.show();
}
});
} else {
label.set_label_escaped(input);

View file

@ -16,4 +16,4 @@ mod provider;
feature = "workspaces",
))]
pub use self::gtk::*;
pub use provider::ImageProvider;
pub use provider::{Provider, create_and_load_surface};

View file

@ -1,115 +1,198 @@
use crate::channels::{AsyncSenderExt, MpscReceiverExt};
use crate::desktop_file::get_desktop_icon_name;
#[cfg(feature = "http")]
use crate::spawn;
use cfg_if::cfg_if;
use crate::desktop_file::DesktopFiles;
use crate::{arc_mut, lock};
use color_eyre::{Help, Report, Result};
use gtk::cairo::Surface;
use gtk::gdk::ffi::gdk_cairo_surface_create_from_pixbuf;
use gtk::gdk_pixbuf::Pixbuf;
use gtk::gio::{Cancellable, MemoryInputStream};
use gtk::prelude::*;
use gtk::{IconLookupFlags, IconTheme};
use std::path::{Path, PathBuf};
#[cfg(feature = "http")]
use tokio::sync::mpsc;
use tracing::{debug, warn};
use gtk::{IconLookupFlags, IconTheme, Image};
use std::cell::RefCell;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use tracing::{debug, trace, warn};
cfg_if!(
if #[cfg(feature = "http")] {
use gtk::gio::{Cancellable, MemoryInputStream};
use tracing::error;
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
struct ImageRef {
size: i32,
location: Option<ImageLocation>,
theme: IconTheme,
}
impl ImageRef {
fn new(size: i32, location: Option<ImageLocation>, theme: IconTheme) -> Self {
Self {
size,
location,
theme,
}
}
);
}
#[derive(Debug)]
enum ImageLocation<'a> {
Icon {
name: String,
theme: &'a IconTheme,
},
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
enum ImageLocation {
Icon(String),
Local(PathBuf),
Steam(String),
#[cfg(feature = "http")]
Remote(reqwest::Url),
}
pub struct ImageProvider<'a> {
location: ImageLocation<'a>,
size: i32,
#[derive(Debug)]
struct Cache {
location_cache: HashMap<(Box<str>, i32), ImageRef>,
pixbuf_cache: HashMap<ImageRef, Option<Pixbuf>>,
}
impl<'a> ImageProvider<'a> {
/// Attempts to parse the image input to find its location.
/// Errors if no valid location type can be found.
impl Cache {
fn new() -> Self {
Self {
location_cache: HashMap::new(),
pixbuf_cache: HashMap::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct Provider {
desktop_files: DesktopFiles,
icon_theme: RefCell<Option<IconTheme>>,
overrides: HashMap<String, String>,
cache: Arc<Mutex<Cache>>,
}
impl Provider {
pub fn new(desktop_files: DesktopFiles, overrides: &mut HashMap<String, String>) -> Self {
let mut overrides_map = HashMap::with_capacity(overrides.len());
overrides_map.extend(overrides.drain());
Self {
desktop_files,
icon_theme: RefCell::new(None),
overrides: overrides_map,
cache: arc_mut!(Cache::new()),
}
}
/// Attempts to resolve the provided input into a `Pixbuf`,
/// and load that `Pixbuf` into the provided `Image` widget.
///
/// Note this checks that icons exist in theme, or files exist on disk
/// but no other check is performed.
pub fn parse(input: &str, theme: &'a IconTheme, use_fallback: bool, size: i32) -> Option<Self> {
let location = Self::get_location(input, theme, size, use_fallback, 0)?;
debug!("Resolved {input} --> {location:?} (size: {size})");
Some(Self { location, size })
}
/// Returns true if the input starts with a prefix
/// that is supported by the parser
/// (ie the parser would not fallback to checking the input).
pub fn is_definitely_image_input(input: &str) -> bool {
input.starts_with("icon:")
|| input.starts_with("file://")
|| input.starts_with("http://")
|| input.starts_with("https://")
|| input.starts_with('/')
}
fn get_location(
/// If `use_fallback` is `true`, a fallback icon will be used
/// where an image cannot be found.
///
/// Returns `true` if the image was successfully loaded,
/// or `false` if the image could not be found.
/// May also return an error if the resolution or loading process failed.
pub async fn load_into_image(
&self,
input: &str,
theme: &'a IconTheme,
size: i32,
use_fallback: bool,
recurse_depth: usize,
) -> Option<ImageLocation<'a>> {
macro_rules! fallback {
() => {
if use_fallback {
Some(Self::get_fallback_icon(theme))
} else {
None
}
};
image: &Image,
) -> Result<bool> {
let image_ref = self.get_ref(input, size).await?;
debug!("image ref for {input}: {:?}", image_ref);
let pixbuf = if let Some(pixbuf) = lock!(self.cache).pixbuf_cache.get(&image_ref) {
pixbuf.clone()
} else {
let pixbuf = Self::get_pixbuf(&image_ref, image.scale_factor(), use_fallback).await?;
lock!(self.cache)
.pixbuf_cache
.insert(image_ref, pixbuf.clone());
pixbuf
};
if let Some(ref pixbuf) = pixbuf {
create_and_load_surface(pixbuf, image)?;
}
const MAX_RECURSE_DEPTH: usize = 2;
Ok(pixbuf.is_some())
}
let should_parse_desktop_file = !Self::is_definitely_image_input(input);
/// Like [`Provider::load_into_image`], but does not return an error if the image could not be found.
///
/// If an image is not resolved, a warning is logged. Errors are also logged.
pub async fn load_into_image_silent(
&self,
input: &str,
size: i32,
use_fallback: bool,
image: &Image,
) {
match self.load_into_image(input, size, use_fallback, image).await {
Ok(true) => {}
Ok(false) => warn!("failed to resolve image: {input}"),
Err(e) => warn!("failed to load image: {input}: {e:?}"),
}
}
/// Returns the `ImageRef` for the provided input.
///
/// This contains the location of the image if it can be resolved.
/// The ref will be loaded from cache if present.
async fn get_ref(&self, input: &str, size: i32) -> Result<ImageRef> {
let key = (input.into(), size);
if let Some(location) = lock!(self.cache).location_cache.get(&key) {
Ok(location.clone())
} else {
let location = self.resolve_location(input, size, 0).await?;
let image_ref = ImageRef::new(size, location, self.icon_theme());
lock!(self.cache)
.location_cache
.insert(key, image_ref.clone());
Ok(image_ref)
}
}
/// Attempts to resolve the provided input into an `ImageLocation`.
///
/// This will resolve all of:
/// - The current icon theme
/// - The file on disk
/// - Steam icons
/// - Desktop files (`Icon` keys)
/// - HTTP(S) URLs
async fn resolve_location(
&self,
input: &str,
size: i32,
recurse_depth: u8,
) -> Result<Option<ImageLocation>> {
const MAX_RECURSE_DEPTH: u8 = 2;
let input = self
.overrides
.get(input)
.map_or(input, String::as_str);
let should_parse_desktop_file = !Self::is_explicit_input(input);
let (input_type, input_name) = input
.split_once(':')
.map_or((None, input), |(t, n)| (Some(t), n));
match input_type {
Some(input_type) if input_type == "icon" => Some(ImageLocation::Icon {
name: input_name.to_string(),
theme,
}),
Some(input_type) if input_type == "file" => Some(ImageLocation::Local(PathBuf::from(
let location = match input_type {
Some(_t @ "icon") => Some(ImageLocation::Icon(input.to_string())),
Some(_t @ "file") => Some(ImageLocation::Local(PathBuf::from(
input_name[2..].to_string(),
))),
#[cfg(feature = "http")]
Some(input_type) if input_type == "http" || input_type == "https" => {
input.parse().ok().map(ImageLocation::Remote)
}
Some(_t @ ("http" | "https")) => input.parse().ok().map(ImageLocation::Remote),
None if input.starts_with("steam_app_") => Some(ImageLocation::Steam(
input_name.chars().skip("steam_app_".len()).collect(),
)),
None if theme
None if self
.icon_theme()
.lookup_icon(input, size, IconLookupFlags::empty())
.is_some() =>
{
Some(ImageLocation::Icon {
name: input_name.to_string(),
theme,
})
Some(ImageLocation::Icon(input.to_string()))
}
Some(input_type) => {
warn!(
@ -117,173 +200,154 @@ impl<'a> ImageProvider<'a> {
Report::msg(format!("Unsupported image type: {input_type}"))
.note("You may need to recompile with support if available")
);
fallback!()
None
}
None if PathBuf::from(input_name).is_file() => {
Some(ImageLocation::Local(PathBuf::from(input_name)))
}
None if recurse_depth == MAX_RECURSE_DEPTH => fallback!(),
None if recurse_depth == MAX_RECURSE_DEPTH => None,
None if should_parse_desktop_file => {
if let Some(location) = get_desktop_icon_name(input_name).map(|input| {
Self::get_location(&input, theme, size, use_fallback, recurse_depth + 1)
}) {
location
} else {
warn!("Failed to find image: {input}");
fallback!()
}
}
None => {
warn!("Failed to find image: {input}");
fallback!()
}
}
}
let location = self
.desktop_files
.find(input_name)
.await?
.and_then(|input| input.icon);
/// Attempts to fetch the image from the location
/// and load it into the provided `GTK::Image` widget.
pub fn load_into_image(&self, image: &gtk::Image) -> Result<()> {
// handle remote locations async to avoid blocking UI thread while downloading
#[cfg(feature = "http")]
if let ImageLocation::Remote(url) = &self.location {
let url = url.clone();
let (tx, rx) = mpsc::channel(64);
spawn(async move {
let bytes = Self::get_bytes_from_http(url).await;
if let Ok(bytes) = bytes {
tx.send_expect(bytes).await;
}
});
{
let size = self.size;
let image = image.clone();
rx.recv_glib(move |bytes| {
let stream = MemoryInputStream::from_bytes(&bytes);
let scale = image.scale_factor();
let scaled_size = size * scale;
let pixbuf = Pixbuf::from_stream_at_scale(
&stream,
scaled_size,
scaled_size,
true,
Some(&Cancellable::new()),
);
// Different error types makes this a bit awkward
match pixbuf.map(|pixbuf| Self::create_and_load_surface(&pixbuf, &image)) {
Ok(Err(err)) => error!("{err:?}"),
Err(err) => error!("{err:?}"),
_ => {}
if let Some(location) = location {
if location == input {
None
} else {
Box::pin(self.resolve_location(&location, size, recurse_depth + 1)).await?
}
});
} else {
None
}
}
} else {
self.load_into_image_sync(image)?;
None => None,
};
#[cfg(not(feature = "http"))]
self.load_into_image_sync(image)?;
Ok(())
Ok(location)
}
/// Attempts to synchronously fetch an image from location
/// and load into into the image.
fn load_into_image_sync(&self, image: &gtk::Image) -> Result<()> {
let scale = image.scale_factor();
let pixbuf = match &self.location {
ImageLocation::Icon { name, theme } => self.get_from_icon(name, theme, scale),
ImageLocation::Local(path) => self.get_from_file(path, scale),
ImageLocation::Steam(steam_id) => self.get_from_steam_id(steam_id, scale),
#[cfg(feature = "http")]
_ => unreachable!(), // handled above
}?;
Self::create_and_load_surface(&pixbuf, image)
}
/// Attempts to create a Cairo surface from the provided `Pixbuf`,
/// using the provided scaling factor.
/// The surface is then loaded into the provided image.
/// Attempts to load the provided `ImageRef` into a `Pixbuf`.
///
/// This is necessary for HiDPI since `Pixbuf`s are always treated as scale factor 1.
pub fn create_and_load_surface(pixbuf: &Pixbuf, image: &gtk::Image) -> Result<()> {
let surface = unsafe {
let ptr = gdk_cairo_surface_create_from_pixbuf(
pixbuf.as_ptr(),
image.scale_factor(),
std::ptr::null_mut(),
);
Surface::from_raw_full(ptr)
/// If `use_fallback` is `true`, a fallback icon will be used
/// where an image cannot be found.
async fn get_pixbuf(
image_ref: &ImageRef,
scale: i32,
use_fallback: bool,
) -> Result<Option<Pixbuf>> {
const FALLBACK_ICON_NAME: &str = "dialog-question-symbolic";
let buf = match &image_ref.location {
Some(ImageLocation::Icon(name)) => image_ref.theme.load_icon_for_scale(
name,
image_ref.size,
scale,
IconLookupFlags::FORCE_SIZE,
),
Some(ImageLocation::Local(path)) => {
let scaled_size = image_ref.size * scale;
Pixbuf::from_file_at_scale(path, scaled_size, scaled_size, true).map(Some)
}
Some(ImageLocation::Steam(app_id)) => {
let path = dirs::data_dir().map_or_else(
|| Err(Report::msg("Missing XDG data dir")),
|dir| Ok(dir.join(format!("icons/hicolor/32x32/apps/steam_icon_{app_id}.png"))),
)?;
let scaled_size = image_ref.size * scale;
Pixbuf::from_file_at_scale(path, scaled_size, scaled_size, true).map(Some)
}
#[cfg(feature = "http")]
Some(ImageLocation::Remote(uri)) => {
let res = reqwest::get(uri.clone()).await?;
let status = res.status();
let bytes = if status.is_success() {
let bytes = res.bytes().await?;
Ok(glib::Bytes::from_owned(bytes))
} else {
Err(Report::msg(format!(
"Received non-success HTTP code ({status})"
)))
}?;
let stream = MemoryInputStream::from_bytes(&bytes);
let scaled_size = image_ref.size * scale;
Pixbuf::from_stream_at_scale(
&stream,
scaled_size,
scaled_size,
true,
Some(&Cancellable::new()),
)
.map(Some)
}
None if use_fallback => image_ref.theme.load_icon_for_scale(
FALLBACK_ICON_NAME,
image_ref.size,
scale,
IconLookupFlags::empty(),
),
None => Ok(None),
}?;
image.set_from_surface(Some(&surface));
Ok(())
Ok(buf)
}
/// Attempts to get a `Pixbuf` from the GTK icon theme.
fn get_from_icon(&self, name: &str, theme: &IconTheme, scale: i32) -> Result<Pixbuf> {
let pixbuf =
match theme.lookup_icon_for_scale(name, self.size, scale, IconLookupFlags::empty()) {
Some(_) => theme.load_icon(name, self.size * scale, IconLookupFlags::FORCE_SIZE),
None => Ok(None),
}?;
pixbuf.map_or_else(
|| Err(Report::msg("Icon theme does not contain icon '{name}'")),
Ok,
)
/// Returns true if the input starts with a prefix
/// that is supported by the parser
/// (i.e. the parser would not fall back to checking the input).
pub fn is_explicit_input(input: &str) -> bool {
input.starts_with("icon:")
|| input.starts_with("file://")
|| input.starts_with("http://")
|| input.starts_with("https://")
|| input.starts_with('/')
}
/// Attempts to get a `Pixbuf` from a local file.
fn get_from_file(&self, path: &Path, scale: i32) -> Result<Pixbuf> {
let scaled_size = self.size * scale;
let pixbuf = Pixbuf::from_file_at_scale(path, scaled_size, scaled_size, true)?;
Ok(pixbuf)
pub fn icon_theme(&self) -> IconTheme {
self.icon_theme
.borrow()
.clone()
.expect("theme should be set at startup")
}
/// Attempts to get a `Pixbuf` from a local file,
/// using the Steam game ID to look it up.
fn get_from_steam_id(&self, steam_id: &str, scale: i32) -> Result<Pixbuf> {
// TODO: Can we load this from icon theme with app id `steam_icon_{}`?
let path = dirs::data_dir().map_or_else(
|| Err(Report::msg("Missing XDG data dir")),
|dir| {
Ok(dir.join(format!(
"icons/hicolor/32x32/apps/steam_icon_{steam_id}.png"
)))
},
)?;
/// Sets the custom icon theme name.
/// If no name is provided, the system default is used.
pub fn set_icon_theme(&self, theme: Option<&str>) {
trace!("Setting icon theme to {:?}", theme);
self.get_from_file(&path, scale)
}
/// Attempts to get `Bytes` from an HTTP resource asynchronously.
#[cfg(feature = "http")]
async fn get_bytes_from_http(url: reqwest::Url) -> Result<glib::Bytes> {
let res = reqwest::get(url).await?;
let status = res.status();
if status.is_success() {
let bytes = res.bytes().await?;
Ok(glib::Bytes::from_owned(bytes))
*self.icon_theme.borrow_mut() = if theme.is_some() {
let icon_theme = IconTheme::new();
icon_theme.set_custom_theme(theme);
Some(icon_theme)
} else {
Err(Report::msg(format!(
"Received non-success HTTP code ({status})"
)))
}
}
fn get_fallback_icon(theme: &'a IconTheme) -> ImageLocation<'a> {
ImageLocation::Icon {
name: "dialog-question-symbolic".to_string(),
theme,
}
IconTheme::default()
};
}
}
/// Attempts to create a Cairo `Surface` from the provided `Pixbuf`,
/// using the provided scaling factor.
/// The surface is then loaded into the provided image.
///
/// This is necessary for HiDPI since `Pixbuf`s are always treated as scale factor 1.
pub fn create_and_load_surface(pixbuf: &Pixbuf, image: &Image) -> Result<()> {
let surface = unsafe {
let ptr = gdk_cairo_surface_create_from_pixbuf(
pixbuf.as_ptr(),
image.scale_factor(),
std::ptr::null_mut(),
);
Surface::from_raw_full(ptr)
}?;
image.set_from_surface(Some(&surface));
Ok(())
}

View file

@ -29,6 +29,7 @@ use crate::channels::SyncSenderExt;
use crate::clients::Clients;
use crate::clients::wayland::OutputEventType;
use crate::config::{Config, MonitorConfig};
use crate::desktop_file::DesktopFiles;
use crate::error::ExitCode;
#[cfg(feature = "ipc")]
use crate::ironvar::{VariableManager, WritableNamespace};
@ -106,7 +107,7 @@ fn run_with_args() {
error!("{err:#}");
exit(ExitCode::IpcResponseError as i32)
}
};
}
});
}
None => start_ironbar(),
@ -119,17 +120,26 @@ pub struct Ironbar {
clients: Rc<RefCell<Clients>>,
config: Rc<RefCell<Config>>,
config_dir: PathBuf,
desktop_files: DesktopFiles,
image_provider: image::Provider,
}
impl Ironbar {
fn new() -> Self {
let (config, config_dir) = load_config();
let (mut config, config_dir) = load_config();
let desktop_files = DesktopFiles::new();
let image_provider =
image::Provider::new(desktop_files.clone(), &mut config.icon_overrides);
Self {
bars: Rc::new(RefCell::new(vec![])),
clients: Rc::new(RefCell::new(Clients::new())),
config: Rc::new(RefCell::new(config)),
config_dir,
desktop_files,
image_provider,
}
}
@ -215,6 +225,10 @@ impl Ironbar {
let _hold = activate_rx.recv().expect("to receive activation signal");
debug!("Received activation signal, initialising bars");
instance
.image_provider
.set_icon_theme(instance.config.borrow().icon_theme.as_deref());
while let Ok(event) = rx_outputs.recv().await {
match event.event_type {
OutputEventType::New => {
@ -270,6 +284,16 @@ impl Ironbar {
.clone()
}
#[must_use]
pub fn desktop_files(&self) -> DesktopFiles {
self.desktop_files.clone()
}
#[must_use]
pub fn image_provider(&self) -> image::Provider {
self.image_provider.clone()
}
/// Gets clones of bars by their name.
///
/// Since the bars contain mostly GTK objects,

View file

@ -147,7 +147,8 @@ impl Module<Button> for ClipboardModule {
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> color_eyre::Result<ModuleParts<Button>> {
let button = IconButton::new(&self.icon, info.icon_theme, self.icon_size);
let button = IconButton::new(&self.icon, self.icon_size, context.ironbar.image_provider());
button.label().set_angle(self.layout.angle(info));
button.label().set_justify(self.layout.justify.into());

View file

@ -1,11 +1,9 @@
use crate::build;
use crate::dynamic_value::dynamic_string;
use gtk::Image;
use gtk::prelude::*;
use serde::Deserialize;
use crate::build;
use crate::dynamic_value::dynamic_string;
use crate::image::ImageProvider;
use super::{CustomWidget, CustomWidgetContext};
#[derive(Debug, Deserialize, Clone)]
@ -48,11 +46,15 @@ impl CustomWidget for ImageWidget {
{
let gtk_image = gtk_image.clone();
let icon_theme = context.icon_theme.clone();
dynamic_string(&self.src, move |src| {
ImageProvider::parse(&src, &icon_theme, false, self.size)
.map(|image| image.load_into_image(&gtk_image));
let gtk_image = gtk_image.clone();
let image_provider = context.image_provider.clone();
glib::spawn_future_local(async move {
image_provider
.load_into_image_silent(&src, self.size, false, &gtk_image)
.await;
});
});
}

View file

@ -21,7 +21,7 @@ use crate::script::Script;
use crate::{module_impl, spawn};
use color_eyre::Result;
use gtk::prelude::*;
use gtk::{Button, IconTheme, Orientation};
use gtk::{Button, Orientation};
use serde::Deserialize;
use std::cell::RefCell;
use std::rc::Rc;
@ -93,9 +93,9 @@ struct CustomWidgetContext<'a> {
tx: &'a mpsc::Sender<ExecEvent>,
bar_orientation: Orientation,
is_popup: bool,
icon_theme: &'a IconTheme,
popup_buttons: Rc<RefCell<Vec<Button>>>,
module_factory: AnyModuleFactory,
image_provider: crate::image::Provider,
}
trait CustomWidget {
@ -134,7 +134,7 @@ pub fn set_length<W: WidgetExt>(widget: &W, length: i32, bar_orientation: Orient
Orientation::Horizontal => widget.set_width_request(length),
Orientation::Vertical => widget.set_height_request(length),
_ => {}
};
}
}
impl WidgetOrModule {
@ -236,10 +236,10 @@ impl Module<gtk::Box> for CustomModule {
tx: &context.controller_tx,
bar_orientation: orientation,
is_popup: false,
icon_theme: info.icon_theme,
popup_buttons: popup_buttons.clone(),
module_factory: BarModuleFactory::new(context.ironbar.clone(), context.popup.clone())
.into(),
image_provider: context.ironbar.image_provider(),
};
self.bar.clone().into_iter().for_each(|widget| {
@ -283,8 +283,8 @@ impl Module<gtk::Box> for CustomModule {
tx: &context.controller_tx,
bar_orientation: Orientation::Horizontal,
is_popup: true,
icon_theme: info.icon_theme,
popup_buttons: Rc::new(RefCell::new(vec![])),
image_provider: context.ironbar.image_provider(),
module_factory: PopupModuleFactory::new(
context.ironbar,
context.popup,

View file

@ -3,7 +3,6 @@ use crate::clients::wayland::{self, ToplevelEvent};
use crate::config::{CommonConfig, LayoutConfig, TruncateMode};
use crate::gtk_helpers::IronbarGtkExt;
use crate::gtk_helpers::IronbarLabelExt;
use crate::image::ImageProvider;
use crate::modules::{Module, ModuleInfo, ModuleParts, WidgetContext};
use crate::{module_impl, spawn};
use color_eyre::Result;
@ -93,7 +92,7 @@ impl Module<gtk::Box> for FocusedModule {
tx.send_update(Some((focused.title.clone(), focused.app_id)))
.await;
};
}
while let Ok(event) = wlrx.recv().await {
match event {
@ -153,31 +152,33 @@ impl Module<gtk::Box> for FocusedModule {
container.add(&label);
{
let icon_overrides = info.icon_overrides.clone();
let icon_theme = info.icon_theme.clone();
let image_provider = context.ironbar.image_provider();
context.subscribe().recv_glib(move |data| {
if let Some((name, mut id)) = data {
if self.show_icon {
if let Some(icon) = icon_overrides.get(&id) {
id = icon.clone();
context.subscribe().recv_glib_async(move |data| {
let icon = icon.clone();
let label = label.clone();
let image_provider = image_provider.clone();
async move {
if let Some((name, id)) = data {
if self.show_icon {
match image_provider
.load_into_image(&id, self.icon_size, true, &icon)
.await
{
Ok(true) => icon.show(),
_ => icon.hide(),
}
}
match ImageProvider::parse(&id, &icon_theme, true, self.icon_size)
.map(|image| image.load_into_image(&icon))
{
Some(Ok(())) => icon.show(),
_ => icon.hide(),
if self.show_title {
label.show();
label.set_label(&name);
}
} else {
icon.hide();
label.hide();
}
if self.show_title {
label.show();
label.set_label(&name);
}
} else {
icon.hide();
label.hide();
}
});
}

View file

@ -231,7 +231,7 @@ impl Module<gtk::Box> for KeyboardModule {
tracing::error!("{err:?}");
break;
}
};
}
}
});
}
@ -257,9 +257,15 @@ impl Module<gtk::Box> for KeyboardModule {
) -> Result<ModuleParts<gtk::Box>> {
let container = gtk::Box::new(self.layout.orientation(info), 0);
let caps = IconLabel::new(&self.icons.caps_off, info.icon_theme, self.icon_size);
let num = IconLabel::new(&self.icons.num_off, info.icon_theme, self.icon_size);
let scroll = IconLabel::new(&self.icons.scroll_off, info.icon_theme, self.icon_size);
let image_provider = context.ironbar.image_provider();
let caps = IconLabel::new(&self.icons.caps_off, self.icon_size, &image_provider);
let num = IconLabel::new(&self.icons.num_off, self.icon_size, &image_provider);
let scroll = IconLabel::new(
&self.icons.scroll_off,
self.icon_size,
&image_provider,
);
caps.label().set_angle(self.layout.angle(info));
caps.label().set_justify(self.layout.justify.into());
@ -270,7 +276,7 @@ impl Module<gtk::Box> for KeyboardModule {
scroll.label().set_angle(self.layout.angle(info));
scroll.label().set_justify(self.layout.justify.into());
let layout_button = IconButton::new("", info.icon_theme, self.icon_size);
let layout_button = IconButton::new("", self.icon_size, image_provider);
if self.show_caps {
caps.add_class("key");

View file

@ -3,20 +3,18 @@ use crate::channels::AsyncSenderExt;
use crate::clients::wayland::ToplevelInfo;
use crate::config::{BarPosition, TruncateMode};
use crate::gtk_helpers::{IronbarGtkExt, IronbarLabelExt};
use crate::image::ImageProvider;
use crate::modules::ModuleUpdateEvent;
use crate::modules::launcher::{ItemEvent, LauncherUpdate};
use crate::read_lock;
use crate::{image, read_lock};
use glib::Propagation;
use gtk::gdk::{BUTTON_MIDDLE, BUTTON_PRIMARY};
use gtk::prelude::*;
use gtk::{Align, Button, IconTheme, Image, Justification, Label, Orientation};
use gtk::{Align, Button, Image, Justification, Label, Orientation};
use indexmap::IndexMap;
use std::ops::Deref;
use std::rc::Rc;
use std::sync::RwLock;
use tokio::sync::mpsc::Sender;
use tracing::error;
#[derive(Debug, Clone)]
pub struct Item {
@ -166,7 +164,7 @@ impl ItemButton {
pub fn new(
item: &Item,
appearance: AppearanceOptions,
icon_theme: &IconTheme,
image_provider: image::Provider,
bar_position: BarPosition,
tx: &Sender<ModuleUpdateEvent<LauncherUpdate>>,
controller_tx: &Sender<ItemEvent>,
@ -188,14 +186,13 @@ impl ItemButton {
} else {
item.app_id.clone()
};
let image = ImageProvider::parse(&input, icon_theme, true, appearance.icon_size);
if let Some(image) = image {
button.set_always_show_image(true);
if let Err(err) = image.load_into_image(&button.image) {
error!("{err:?}");
}
};
let button = button.clone();
glib::spawn_future_local(async move {
image_provider
.load_into_image_silent(&input, appearance.icon_size, true, &button.image)
.await;
});
}
button.add_class("item");

View file

@ -8,7 +8,6 @@ use super::{Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, Wid
use crate::channels::{AsyncSenderExt, BroadcastReceiverExt};
use crate::clients::wayland::{self, ToplevelEvent};
use crate::config::{CommonConfig, EllipsizeMode, LayoutConfig, TruncateMode};
use crate::desktop_file::find_desktop_file;
use crate::gtk_helpers::{IronbarGtkExt, IronbarLabelExt};
use crate::modules::launcher::item::ImageTextButton;
use crate::modules::launcher::pagination::{IconContext, Pagination};
@ -18,11 +17,10 @@ use gtk::prelude::*;
use gtk::{Button, Orientation};
use indexmap::IndexMap;
use serde::Deserialize;
use std::ops::Deref;
use std::process::{Command, Stdio};
use std::sync::Arc;
use tokio::sync::mpsc;
use tracing::{debug, error, trace};
use tracing::{debug, error, trace, warn};
#[derive(Debug, Deserialize, Clone)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
@ -212,7 +210,7 @@ impl Module<gtk::Box> for LauncherModule {
fn spawn_controller(
&self,
info: &ModuleInfo,
_info: &ModuleInfo,
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
mut rx: mpsc::Receiver<Self::ReceiveMessage>,
) -> crate::Result<()> {
@ -223,14 +221,9 @@ impl Module<gtk::Box> for LauncherModule {
favorites
.iter()
.map(|app_id| {
let icon_override = info
.icon_overrides
.get(app_id)
.map_or_else(String::new, ToString::to_string);
(
app_id.to_string(),
Item::new(app_id.to_string(), icon_override, OpenState::Closed, true),
Item::new(app_id.to_string(), OpenState::Closed, true),
)
})
.collect::<IndexMap<_, _>>()
@ -239,8 +232,6 @@ impl Module<gtk::Box> for LauncherModule {
let items = arc_mut!(items);
let items2 = Arc::clone(&items);
let icon_overrides = info.icon_overrides.clone();
let tx = context.tx.clone();
let tx2 = context.tx.clone();
@ -258,12 +249,7 @@ impl Module<gtk::Box> for LauncherModule {
if let Some(item) = item {
item.merge_toplevel(info.clone());
} else {
let mut item = Item::from(info.clone());
if let Some(icon) = icon_overrides.get(&info.app_id) {
item.icon_override.clone_from(icon);
}
let item = Item::from(info.clone());
items.insert(info.app_id.clone(), item);
}
}
@ -271,6 +257,7 @@ impl Module<gtk::Box> for LauncherModule {
{
let items = {
let items = lock!(items);
items
.iter()
.map(|(_, item)| item.clone())
@ -296,12 +283,7 @@ impl Module<gtk::Box> for LauncherModule {
let item = items.get_mut(&info.app_id);
match item {
None => {
let mut item: Item = info.into();
if let Some(icon) = icon_overrides.get(&app_id) {
item.icon_override.clone_from(icon);
}
let item: Item = info.into();
items.insert(app_id.clone(), item.clone());
ItemOrWindow::Item(item)
@ -378,7 +360,7 @@ impl Module<gtk::Box> for LauncherModule {
.await?;
}
None => {}
};
}
}
}
}
@ -389,17 +371,16 @@ impl Module<gtk::Box> for LauncherModule {
// listen to ui events
let minimize_focused = self.minimize_focused;
let wl = context.client::<wayland::Client>();
let desktop_files = context.ironbar.desktop_files();
spawn(async move {
while let Some(event) = rx.recv().await {
if let ItemEvent::OpenItem(app_id) = event {
find_desktop_file(&app_id).map_or_else(
|| error!("Could not find desktop file for {}", app_id),
|file| {
match desktop_files.find(&app_id).await {
Ok(Some(file)) => {
if let Err(err) = Command::new("gtk-launch")
.arg(
file.file_name()
.expect("File segment missing from path to desktop file"),
)
.arg(file.file_name)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
@ -408,11 +389,13 @@ impl Module<gtk::Box> for LauncherModule {
"{:?}",
Report::new(err)
.wrap_err("Failed to run gtk-launch command.")
.suggestion("Perhaps the desktop file is invalid?")
.suggestion("Perhaps the applications file is invalid?")
);
}
},
);
}
Ok(None) => warn!("Could not find applications file for {}", app_id),
Err(err) => error!("Failed to find parse file for {}: {}", app_id, err),
}
} else {
tx.send_expect(ModuleUpdateEvent::ClosePopup).await;
@ -457,11 +440,11 @@ impl Module<gtk::Box> for LauncherModule {
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> crate::Result<ModuleParts<gtk::Box>> {
let icon_theme = info.icon_theme;
let container = gtk::Box::new(self.layout.orientation(info), 0);
let page_size = self.page_size;
let image_provider = context.ironbar.image_provider();
let pagination = Pagination::new(
&container,
self.page_size,
@ -471,11 +454,11 @@ impl Module<gtk::Box> for LauncherModule {
fwd: &self.icons.page_forward,
size: self.pagination_icon_size,
},
&image_provider,
);
{
let container = container.clone();
let icon_theme = icon_theme.clone();
let controller_tx = context.controller_tx.clone();
@ -516,7 +499,7 @@ impl Module<gtk::Box> for LauncherModule {
let button = ItemButton::new(
&item,
appearance_options,
&icon_theme,
image_provider.clone(),
bar_position,
&tx,
&controller_tx,

View file

@ -1,7 +1,8 @@
use crate::gtk_helpers::IronbarGtkExt;
use crate::image;
use crate::image::IconButton;
use gtk::prelude::*;
use gtk::{Button, IconTheme, Orientation};
use gtk::{Button, Orientation};
use std::cell::RefCell;
use std::ops::Deref;
use std::rc::Rc;
@ -24,20 +25,21 @@ impl Pagination {
container: &gtk::Box,
page_size: usize,
orientation: Orientation,
icon_context: IconContext,
icon_context: &IconContext,
image_provider: &image::Provider,
) -> Self {
let scroll_box = gtk::Box::new(orientation, 0);
let scroll_back = IconButton::new(
icon_context.icon_back,
icon_context.icon_theme,
icon_context.icon_size,
icon_context.back,
icon_context.size,
image_provider.clone(),
);
let scroll_fwd = IconButton::new(
icon_context.icon_fwd,
icon_context.icon_theme,
icon_context.icon_size,
icon_context.fwd,
icon_context.size,
image_provider.clone(),
);
scroll_back.set_sensitive(false);

View file

@ -1,4 +1,3 @@
use std::collections::HashMap;
use std::fmt::Debug;
use std::rc::Rc;
use std::sync::Arc;
@ -7,7 +6,7 @@ use color_eyre::Result;
use glib::IsA;
use gtk::gdk::{EventMask, Monitor};
use gtk::prelude::*;
use gtk::{Application, Button, EventBox, IconTheme, Orientation, Revealer, Widget};
use gtk::{Application, Button, EventBox, Orientation, Revealer, Widget};
use tokio::sync::{broadcast, mpsc};
use tracing::debug;
@ -78,8 +77,6 @@ pub struct ModuleInfo<'a> {
pub bar_position: BarPosition,
pub monitor: &'a Monitor,
pub output_name: &'a str,
pub icon_theme: &'a IconTheme,
pub icon_overrides: Arc<HashMap<String, String>>,
}
#[derive(Debug, Clone)]

View file

@ -7,10 +7,10 @@ use std::time::Duration;
use color_eyre::Result;
use glib::{Propagation, PropertySet};
use gtk::prelude::*;
use gtk::{Button, IconTheme, Label, Orientation, Scale};
use gtk::{Button, Label, Orientation, Scale};
use regex::Regex;
use tokio::sync::mpsc;
use tracing::error;
use tracing::{error, warn};
pub use self::config::MusicModule;
use self::config::PlayerType;
@ -20,12 +20,12 @@ use crate::clients::music::{
self, MusicClient, PlayerState, PlayerUpdate, ProgressTick, Status, Track,
};
use crate::gtk_helpers::{IronbarGtkExt, IronbarLabelExt};
use crate::image::{IconButton, IconLabel, ImageProvider};
use crate::image::{IconButton, IconLabel};
use crate::modules::PopupButton;
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
};
use crate::{module_impl, spawn};
use crate::{image, module_impl, spawn};
mod config;
@ -142,7 +142,7 @@ impl Module<Button> for MusicModule {
},
PlayerUpdate::ProgressTick(progress_tick) => {
tx.send_update(ControllerEvent::UpdateProgress(progress_tick))
.await
.await;
}
}
}
@ -184,8 +184,10 @@ impl Module<Button> for MusicModule {
button.add(&button_contents);
let icon_play = IconLabel::new(&self.icons.play, info.icon_theme, self.icon_size);
let icon_pause = IconLabel::new(&self.icons.pause, info.icon_theme, self.icon_size);
let image_provider = context.ironbar.image_provider();
let icon_play = IconLabel::new(&self.icons.play, self.icon_size, &image_provider);
let icon_pause = IconLabel::new(&self.icons.pause, self.icon_size, &image_provider);
icon_play.label().set_angle(self.layout.angle(info));
icon_play.label().set_justify(self.layout.justify.into());
@ -267,9 +269,9 @@ impl Module<Button> for MusicModule {
fn into_popup(
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
_info: &ModuleInfo,
) -> Option<gtk::Box> {
let icon_theme = info.icon_theme;
let image_provider = context.ironbar.image_provider();
let container = gtk::Box::new(Orientation::Vertical, 10);
let main_container = gtk::Box::new(Orientation::Horizontal, 10);
@ -283,9 +285,9 @@ impl Module<Button> for MusicModule {
let icons = self.icons;
let info_box = gtk::Box::new(Orientation::Vertical, 10);
let title_label = IconPrefixedLabel::new(&icons.track, None, icon_theme);
let album_label = IconPrefixedLabel::new(&icons.album, None, icon_theme);
let artist_label = IconPrefixedLabel::new(&icons.artist, None, icon_theme);
let title_label = IconPrefixedLabel::new(&icons.track, None, &image_provider);
let album_label = IconPrefixedLabel::new(&icons.album, None, &image_provider);
let artist_label = IconPrefixedLabel::new(&icons.artist, None, &image_provider);
title_label.container.add_class("title");
album_label.container.add_class("album");
@ -298,16 +300,16 @@ impl Module<Button> for MusicModule {
let controls_box = gtk::Box::new(Orientation::Horizontal, 0);
controls_box.add_class("controls");
let btn_prev = IconButton::new(&icons.prev, icon_theme, self.icon_size);
let btn_prev = IconButton::new(&icons.prev, self.icon_size, image_provider.clone());
btn_prev.add_class("btn-prev");
let btn_play = IconButton::new(&icons.play, icon_theme, self.icon_size);
let btn_play = IconButton::new(&icons.play, self.icon_size, image_provider.clone());
btn_play.add_class("btn-play");
let btn_pause = IconButton::new(&icons.pause, icon_theme, self.icon_size);
let btn_pause = IconButton::new(&icons.pause, self.icon_size, image_provider.clone());
btn_pause.add_class("btn-pause");
let btn_next = IconButton::new(&icons.next, icon_theme, self.icon_size);
let btn_next = IconButton::new(&icons.next, self.icon_size, image_provider.clone());
btn_next.add_class("btn-next");
controls_box.add(&*btn_prev);
@ -324,7 +326,7 @@ impl Module<Button> for MusicModule {
volume_slider.set_inverted(true);
volume_slider.add_class("slider");
let volume_icon = IconLabel::new(&icons.volume, icon_theme, self.icon_size);
let volume_icon = IconLabel::new(&icons.volume, self.icon_size, &image_provider);
volume_icon.add_class("icon");
volume_box.pack_start(&volume_slider, true, true, 0);
@ -402,7 +404,6 @@ impl Module<Button> for MusicModule {
container.show_all();
{
let icon_theme = icon_theme.clone();
let image_size = self.cover_image_size;
let mut prev_cover = None;
@ -413,19 +414,43 @@ impl Module<Button> for MusicModule {
let new_cover = update.song.cover_path;
if prev_cover != new_cover {
prev_cover.clone_from(&new_cover);
let res = if let Some(image) = new_cover.and_then(|cover_path| {
ImageProvider::parse(&cover_path, &icon_theme, false, image_size)
}) {
album_image.show();
image.load_into_image(&album_image)
if let Some(cover_path) = new_cover {
let image_provider = image_provider.clone();
let album_image = album_image.clone();
glib::spawn_future_local(async move {
let success = match image_provider
.load_into_image(
&cover_path,
image_size,
false,
&album_image,
)
.await
{
Ok(true) => {
album_image.show();
true
}
Ok(false) => {
warn!("failed to parse image: {}", cover_path);
false
}
Err(err) => {
error!("failed to load image: {}", err);
false
}
};
if !success {
album_image.set_from_pixbuf(None);
album_image.hide();
}
});
} else {
album_image.set_from_pixbuf(None);
album_image.hide();
Ok(())
};
if let Err(err) = res {
error!("{err:?}");
}
}
@ -490,7 +515,7 @@ impl Module<Button> for MusicModule {
}
}
_ => {}
};
}
});
}
@ -544,10 +569,10 @@ struct IconPrefixedLabel {
}
impl IconPrefixedLabel {
fn new(icon_input: &str, label: Option<&str>, icon_theme: &IconTheme) -> Self {
fn new(icon_input: &str, label: Option<&str>, image_provider: &image::Provider) -> Self {
let container = gtk::Box::new(Orientation::Horizontal, 5);
let icon = IconLabel::new(icon_input, icon_theme, 24);
let icon = IconLabel::new(icon_input, 24, image_provider);
let mut builder = Label::builder().use_markup(true);

View file

@ -1,3 +1,9 @@
use crate::channels::{AsyncSenderExt, BroadcastReceiverExt};
use crate::clients::networkmanager::{Client, ClientState};
use crate::config::CommonConfig;
use crate::gtk_helpers::IronbarGtkExt;
use crate::modules::{Module, ModuleInfo, ModuleParts, WidgetContext};
use crate::{module_impl, spawn};
use color_eyre::Result;
use futures_lite::StreamExt;
use futures_signals::signal::SignalExt;
@ -6,14 +12,6 @@ use gtk::{Box as GtkBox, Image};
use serde::Deserialize;
use tokio::sync::mpsc::Receiver;
use crate::channels::{AsyncSenderExt, BroadcastReceiverExt};
use crate::clients::networkmanager::{Client, ClientState};
use crate::config::CommonConfig;
use crate::gtk_helpers::IronbarGtkExt;
use crate::image::ImageProvider;
use crate::modules::{Module, ModuleInfo, ModuleParts, WidgetContext};
use crate::{module_impl, spawn};
#[derive(Debug, Deserialize, Clone)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct NetworkManagerModule {
@ -58,18 +56,30 @@ impl Module<GtkBox> for NetworkManagerModule {
context: WidgetContext<ClientState, ()>,
info: &ModuleInfo,
) -> Result<ModuleParts<GtkBox>> {
const INITIAL_ICON_NAME: &str = "content-loading-symbolic";
let container = GtkBox::new(info.bar_position.orientation(), 0);
let icon = Image::new();
icon.add_class("icon");
container.add(&icon);
let icon_theme = info.icon_theme.clone();
let image_provider = context.ironbar.image_provider();
let initial_icon_name = "content-loading-symbolic";
ImageProvider::parse(initial_icon_name, &icon_theme, false, self.icon_size)
.map(|provider| provider.load_into_image(&icon));
glib::spawn_future_local({
let image_provider = image_provider.clone();
let icon = icon.clone();
async move {
image_provider
.load_into_image_silent(INITIAL_ICON_NAME, self.icon_size, false, &icon)
.await;
}
});
context.subscribe().recv_glib_async(move |state| {
let image_provider = image_provider.clone();
let icon = icon.clone();
context.subscribe().recv_glib(move |state| {
let icon_name = match state {
ClientState::WiredConnected => "network-wired-symbolic",
ClientState::WifiConnected => "network-wireless-symbolic",
@ -79,8 +89,12 @@ impl Module<GtkBox> for NetworkManagerModule {
ClientState::Offline => "network-wireless-disabled-symbolic",
ClientState::Unknown => "dialog-question-symbolic",
};
ImageProvider::parse(icon_name, &icon_theme, false, self.icon_size)
.map(|provider| provider.load_into_image(&icon));
async move {
image_provider
.load_into_image_silent(icon_name, self.icon_size, false, &icon)
.await;
}
});
Ok(ModuleParts::new(container, None))

View file

@ -1,4 +1,4 @@
use crate::image::ImageProvider;
use crate::image::create_and_load_surface;
use crate::modules::tray::interface::TrayMenu;
use color_eyre::{Report, Result};
use glib::ffi::g_strfreev;
@ -40,21 +40,21 @@ fn get_icon_theme_search_paths(icon_theme: &IconTheme) -> HashSet<String> {
pub fn get_image(
item: &TrayMenu,
icon_theme: &IconTheme,
size: u32,
prefer_icons: bool,
icon_theme: &IconTheme,
) -> Result<Image> {
if !prefer_icons && item.icon_pixmap.is_some() {
get_image_from_pixmap(item, size)
} else {
get_image_from_icon_name(item, icon_theme, size)
get_image_from_icon_name(item, size, icon_theme)
.or_else(|_| get_image_from_pixmap(item, size))
}
}
/// Attempts to get a GTK `Image` component
/// for the status notifier item's icon.
fn get_image_from_icon_name(item: &TrayMenu, icon_theme: &IconTheme, size: u32) -> Result<Image> {
fn get_image_from_icon_name(item: &TrayMenu, size: u32, icon_theme: &IconTheme) -> Result<Image> {
if let Some(path) = item.icon_theme_path.as_ref() {
if !path.is_empty() && !get_icon_theme_search_paths(icon_theme).contains(path) {
icon_theme.append_search_path(path);
@ -68,7 +68,7 @@ fn get_image_from_icon_name(item: &TrayMenu, icon_theme: &IconTheme, size: u32)
if let Some(icon_info) = icon_info {
let pixbuf = icon_info.load_icon()?;
let image = Image::new();
ImageProvider::create_and_load_surface(&pixbuf, &image)?;
create_and_load_surface(&pixbuf, &image)?;
Ok(image)
} else {
Err(Report::msg("could not find icon"))
@ -122,6 +122,6 @@ fn get_image_from_pixmap(item: &TrayMenu, size: u32) -> Result<Image> {
.unwrap_or(pixbuf);
let image = Image::new();
ImageProvider::create_and_load_surface(&pixbuf, &image)?;
create_and_load_surface(&pixbuf, &image)?;
Ok(image)
}

View file

@ -93,7 +93,7 @@ impl Module<gtk::Box> for TrayModule {
while let Some(cmd) = rx.recv().await {
if let Err(err) = client.activate(cmd).await {
error!("{err:?}");
};
}
}
Ok::<_, Report>(())
@ -120,7 +120,7 @@ impl Module<gtk::Box> for TrayModule {
{
let container = container.clone();
let mut menus = HashMap::new();
let icon_theme = info.icon_theme.clone();
let icon_theme = context.ironbar.image_provider().icon_theme();
// listen for UI updates
context.subscribe().recv_glib(move |update| {
@ -159,12 +159,12 @@ fn on_update(
let mut menu_item = TrayMenu::new(&address, *item);
container.pack_start(&menu_item.event_box, true, true, 0);
if let Ok(image) = icon::get_image(&menu_item, icon_theme, icon_size, prefer_icons) {
if let Ok(image) = icon::get_image(&menu_item, icon_size, prefer_icons, icon_theme) {
menu_item.set_image(&image);
} else {
let label = menu_item.title.clone().unwrap_or(address.clone());
menu_item.set_label(&label);
};
}
menu_item.event_box.show();
menus.insert(address.into(), menu_item);
@ -185,10 +185,10 @@ fn on_update(
UpdateEvent::Icon(icon) => {
if icon.as_ref() != menu_item.icon_name() {
menu_item.set_icon_name(icon);
match icon::get_image(menu_item, icon_theme, icon_size, prefer_icons) {
match icon::get_image(menu_item, icon_size, prefer_icons, icon_theme) {
Ok(image) => menu_item.set_image(&image),
Err(_) => menu_item.show_label(),
};
}
}
}
UpdateEvent::OverlayIcon(_icon) => {
@ -219,5 +219,5 @@ fn on_update(
container.remove(&menu.event_box);
}
}
};
}
}

View file

@ -3,6 +3,7 @@ use futures_lite::stream::StreamExt;
use gtk::{Button, prelude::*};
use gtk::{Label, Orientation};
use serde::Deserialize;
use std::fmt::Write;
use tokio::sync::mpsc;
use zbus;
use zbus::fdo::PropertiesProxy;
@ -11,7 +12,6 @@ use crate::channels::{AsyncSenderExt, BroadcastReceiverExt};
use crate::clients::upower::BatteryState;
use crate::config::{CommonConfig, LayoutConfig};
use crate::gtk_helpers::{IronbarGtkExt, IronbarLabelExt};
use crate::image::ImageProvider;
use crate::modules::PopupButton;
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
@ -171,7 +171,6 @@ impl Module<Button> for UpowerModule {
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleParts<Button>> {
let icon_theme = info.icon_theme.clone();
let icon = gtk::Image::new();
icon.add_class("icon");
@ -202,15 +201,20 @@ impl Module<Button> for UpowerModule {
let format = self.format.clone();
let rx = context.subscribe();
rx.recv_glib(move |properties| {
let provider = context.ironbar.image_provider();
rx.recv_glib_async(move |properties| {
let state = properties.state;
let is_charging =
state == BatteryState::Charging || state == BatteryState::PendingCharge;
let time_remaining = if is_charging {
seconds_to_string(properties.time_to_full)
} else {
seconds_to_string(properties.time_to_empty)
};
}
.unwrap_or_default();
let format = format
.replace("{percentage}", &properties.percentage.to_string())
.replace("{time_remaining}", &time_remaining)
@ -219,10 +223,16 @@ impl Module<Button> for UpowerModule {
let mut icon_name = String::from("icon:");
icon_name.push_str(&properties.icon_name);
ImageProvider::parse(&icon_name, &icon_theme, false, self.icon_size)
.map(|provider| provider.load_into_image(&icon));
let provider = provider.clone();
let icon = icon.clone();
label.set_label_escaped(&format);
async move {
provider
.load_into_image_silent(&icon_name, self.icon_size, false, &icon)
.await;
}
});
let popup = self
@ -254,7 +264,7 @@ impl Module<Button> for UpowerModule {
BatteryState::Charging | BatteryState::PendingCharge => {
let ttf = properties.time_to_full;
if ttf > 0 {
format!("Full in {}", seconds_to_string(ttf))
format!("Full in {}", seconds_to_string(ttf).unwrap_or_default())
} else {
String::new()
}
@ -262,7 +272,7 @@ impl Module<Button> for UpowerModule {
BatteryState::Discharging | BatteryState::PendingDischarge => {
let tte = properties.time_to_empty;
if tte > 0 {
format!("Empty in {}", seconds_to_string(tte))
format!("Empty in {}", seconds_to_string(tte).unwrap_or_default())
} else {
String::new()
}
@ -279,21 +289,22 @@ impl Module<Button> for UpowerModule {
}
}
fn seconds_to_string(seconds: i64) -> String {
fn seconds_to_string(seconds: i64) -> Result<String> {
let mut time_string = String::new();
let days = seconds / (DAY);
if days > 0 {
time_string += &format!("{days}d");
write!(time_string, "{days}d")?;
}
let hours = (seconds % DAY) / HOUR;
if hours > 0 {
time_string += &format!(" {hours}h");
write!(time_string, " {hours}h")?;
}
let minutes = (seconds % HOUR) / MINUTE;
if minutes > 0 {
time_string += &format!(" {minutes}m");
write!(time_string, " {minutes}m")?;
}
time_string.trim_start().to_string()
Ok(time_string.trim_start().to_string())
}
const fn u32_to_battery_state(number: u32) -> Result<BatteryState, u32> {

View file

@ -16,7 +16,7 @@ impl Button {
pub fn new(id: i64, name: &str, open_state: OpenState, context: &WorkspaceItemContext) -> Self {
let label = context.name_map.get(name).map_or(name, String::as_str);
let button = IconButton::new(label, &context.icon_theme, context.icon_size);
let button = IconButton::new(label, context.icon_size, context.image_provider.clone());
button.set_widget_name(name);
button.add_class("item");

View file

@ -9,9 +9,8 @@ use crate::config::{CommonConfig, LayoutConfig};
use crate::modules::workspaces::button_map::{ButtonMap, Identifier};
use crate::modules::workspaces::open_state::OpenState;
use crate::modules::{Module, ModuleInfo, ModuleParts, WidgetContext};
use crate::{module_impl, spawn};
use crate::{image, module_impl, spawn};
use color_eyre::{Report, Result};
use gtk::IconTheme;
use gtk::prelude::*;
use serde::Deserialize;
use std::cmp::Ordering;
@ -145,8 +144,8 @@ const fn default_icon_size() -> i32 {
#[derive(Debug, Clone)]
pub struct WorkspaceItemContext {
name_map: HashMap<String, String>,
icon_theme: IconTheme,
icon_size: i32,
image_provider: image::Provider,
tx: mpsc::Sender<i64>,
}
@ -240,8 +239,8 @@ impl Module<gtk::Box> for WorkspacesModule {
let item_context = WorkspaceItemContext {
name_map: self.name_map.clone(),
icon_theme: info.icon_theme.clone(),
icon_size: self.icon_size,
image_provider: context.ironbar.image_provider(),
tx: context.controller_tx.clone(),
};

View file

@ -0,0 +1,8 @@
[Desktop Entry]
Name=Factorio
Comment=Play this game on Steam
Exec=steam steam://rungameid/427520
Icon=steam_icon_427520
Terminal=false
Type=Application
Categories=Game;

View file

@ -0,0 +1,902 @@
[Desktop Entry]
Version=1.0
Type=Application
Exec=/usr/lib/firefox/firefox %u
Terminal=false
X-MultipleArgs=false
Icon=firefox
StartupWMClass=firefox
DBusActivatable=false
Categories=GNOME;GTK;Network;WebBrowser;
MimeType=application/json;application/pdf;application/rdf+xml;application/rss+xml;application/x-xpinstall;application/xhtml+xml;application/xml;audio/flac;audio/ogg;audio/webm;image/avif;image/gif;image/jpeg;image/png;image/svg+xml;image/webp;text/html;text/xml;video/ogg;video/webm;x-scheme-handler/chrome;x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/mailto;
StartupNotify=true
Actions=new-window;new-private-window;open-profile-manager;
Name=Firefox
Name[ach]=Firefox
Name[af]=Firefox
Name[an]=Firefox
Name[ar]=Firefox
Name[ast]=Firefox
Name[az]=Firefox
Name[be]=Firefox
Name[bg]=Firefox
Name[bn]=Firefox
Name[br]=Firefox
Name[brx]=Firefox
Name[bs]=Firefox
Name[ca]=Firefox
Name[ca_valencia]=Firefox
Name[cak]=Firefox
Name[ckb]=Firefox
Name[cs]=Firefox
Name[cy]=Firefox
Name[da]=Firefox
Name[de]=Firefox
Name[dsb]=Firefox
Name[el]=Firefox
Name[en_CA]=Firefox
Name[en_GB]=Firefox
Name[eo]=Firefox
Name[es_AR]=Firefox
Name[es_CL]=Firefox
Name[es_ES]=Firefox
Name[es_MX]=Firefox
Name[et]=Firefox
Name[eu]=Firefox
Name[fa]=Firefox
Name[ff]=Firefox
Name[fi]=Firefox
Name[fr]=Firefox
Name[fur]=Firefox
Name[fy_NL]=Firefox
Name[ga_IE]=Firefox
Name[gd]=Firefox
Name[gl]=Firefox
Name[gn]=Firefox
Name[gu_IN]=Firefox
Name[he]=Firefox
Name[hi_IN]=Firefox
Name[hr]=Firefox
Name[hsb]=Firefox
Name[hu]=Firefox
Name[hy_AM]=Firefox
Name[hye]=Firefox
Name[ia]=Firefox
Name[id]=Firefox
Name[is]=Firefox
Name[it]=Firefox
Name[ja]=Firefox
Name[ka]=Firefox
Name[kab]=Firefox
Name[kk]=Firefox
Name[km]=Firefox
Name[kn]=Firefox
Name[ko]=Firefox
Name[lij]=Firefox
Name[lo]=Firefox
Name[lt]=Firefox
Name[ltg]=Firefox
Name[lv]=Firefox
Name[meh]=Firefox
Name[mk]=Firefox
Name[mr]=Firefox
Name[ms]=Firefox
Name[my]=Firefox
Name[nb_NO]=Firefox
Name[ne_NP]=Firefox
Name[nl]=Firefox
Name[nn_NO]=Firefox
Name[oc]=Firefox
Name[pa_IN]=Firefox
Name[pl]=Firefox
Name[pt_BR]=Firefox
Name[pt_PT]=Firefox
Name[rm]=Firefox
Name[ro]=Firefox
Name[ru]=Firefox
Name[sat]=Firefox
Name[sc]=Firefox
Name[sco]=Firefox
Name[si]=Firefox
Name[sk]=Firefox
Name[skr]=Firefox
Name[sl]=Firefox
Name[son]=Firefox
Name[sq]=Firefox
Name[sr]=Firefox
Name[sv_SE]=Firefox
Name[szl]=Firefox
Name[ta]=Firefox
Name[te]=Firefox
Name[tg]=Firefox
Name[th]=Firefox
Name[tl]=Firefox
Name[tr]=Firefox
Name[trs]=Firefox
Name[uk]=Firefox
Name[ur]=Firefox
Name[uz]=Firefox
Name[vi]=Firefox
Name[wo]=Firefox
Name[xh]=Firefox
Name[zh_CN]=Firefox
Name[zh_TW]=Firefox
Comment=Browse the World Wide Web
Comment[ach]=Browse the World Wide Web
Comment[af]=Browse the World Wide Web
Comment[an]=Browse the World Wide Web
Comment[ar]=تصفح شبكة الوِب العالمية
Comment[ast]=Browse the World Wide Web
Comment[az]=Browse the World Wide Web
Comment[be]=Аглядайце Сеціва
Comment[bg]=Разгледайте световната мрежа
Comment[bn]=Browse the World Wide Web
Comment[br]=Ergerzhout ar World Wide Web
Comment[brx]=Browse the World Wide Web
Comment[bs]=Pretražujte World Wide Web
Comment[ca]=Navegeu pel Web
Comment[ca_valencia]=Browse the World Wide Web
Comment[cak]=Tok chupam Word Wide Web
Comment[ckb]=Browse the World Wide Web
Comment[cs]=Prohlížení stránek World Wide Webu
Comment[cy]=Pori'r We Fyd Eang
Comment[da]=Brug internettet
Comment[de]=Im Internet surfen
Comment[dsb]=Pśeglědajśo World Wide Web
Comment[el]=Περιηγηθείτε στον παγκόσμιο ιστό
Comment[en_CA]=Browse the World Wide Web
Comment[en_GB]=Browse the World Wide Web
Comment[eo]=Retumi en la reto
Comment[es_AR]=Navegar la World Wide Web
Comment[es_CL]=Navegar por la World Wide Web
Comment[es_ES]=Navegar por la web
Comment[es_MX]=Navegar por la web
Comment[et]=Browse the World Wide Web
Comment[eu]=Arakatu World Wide Web-a
Comment[fa]=Browse the World Wide Web
Comment[ff]=Browse the World Wide Web
Comment[fi]=Selaa Internetiä
Comment[fr]=Naviguer sur le Web
Comment[fur]=Navighe sul Web
Comment[fy_NL]=Navigearje op it wrâldwide web
Comment[ga_IE]=Browse the World Wide Web
Comment[gd]=Rùraich lìon na cruinne
Comment[gl]=Navegar pola World Wide Web
Comment[gn]=Eikundaha World Wide Web rupi
Comment[gu_IN]=Browse the World Wide Web
Comment[he]=גלישה באינטרנט
Comment[hi_IN]=Browse the World Wide Web
Comment[hr]=Pregledaj World Wide Web
Comment[hsb]=Přehladajće World Wide Web
Comment[hu]=Böngésszen a világhálón
Comment[hy_AM]=Զննի՛ր համաշխարհային սարդոստայնը
Comment[hye]=Browse the World Wide Web
Comment[ia]=Navigar sur le Web
Comment[id]=Jelajahi World Wide Web
Comment[is]=Vafraðu um veraldarvefinn
Comment[it]=Naviga sul Web
Comment[ja]=World Wide Web
Comment[ka]=
Comment[kab]=Inig deg Web
Comment[kk]=Ғаламторды шолу
Comment[km]=Browse the World Wide Web
Comment[kn]=Browse the World Wide Web
Comment[ko]=
Comment[lij]=Browse the World Wide Web
Comment[lo]=
Comment[lt]=Browse the World Wide Web
Comment[ltg]=Browse the World Wide Web
Comment[lv]=Pārlūkojiet globālo tīmekli
Comment[meh]=Browse the World Wide Web
Comment[mk]=Browse the World Wide Web
Comment[mr]=Browse the World Wide Web
Comment[ms]=Browse the World Wide Web
Comment[my]=Browse the World Wide Web
Comment[nb_NO]=Surf på nettet
Comment[ne_NP]=Browse the World Wide Web
Comment[nl]=Navigeren op het wereldwijde web
Comment[nn_NO]=Surf på nettet
Comment[oc]=Navegar pel Web
Comment[pa_IN]=
Comment[pl]=Przeglądaj Internet
Comment[pt_BR]=Navegue na World Wide Web
Comment[pt_PT]=Navegar na Internet
Comment[rm]=Navigar en il web
Comment[ro]=Browse the World Wide Web
Comment[ru]=Доступ в Интернет
Comment[sat]=World Wide Web
Comment[sc]=Nàviga su Web
Comment[sco]=Browse the World Wide Web
Comment[si]=
Comment[sk]=Prehľadávať web (www)
Comment[skr]=ورلڈ وائیڈ ویب براؤز کرو
Comment[sl]=Brskanje po svetovnem spletu
Comment[son]=Browse the World Wide Web
Comment[sq]=Shfletoni në World Wide Web
Comment[sr]=Истражите интернет
Comment[sv_SE]=Surfa på webben
Comment[szl]=Browse the World Wide Web
Comment[ta]=Browse the World Wide Web
Comment[te]=Browse the World Wide Web
Comment[tg]=Ба шабакаи ҷаҳонии Интернет дастрасӣ пайдо намоед
Comment[th]=
Comment[tl]=Browse the World Wide Web
Comment[tr]=Webde gezin
Comment[trs]=Gāchē nu ngà World Wide Web
Comment[uk]=Переглядайте всесвітню мережу
Comment[ur]=Browse the World Wide Web
Comment[uz]=Browse the World Wide Web
Comment[vi]=Duyt web trên toàn thế gii
Comment[wo]=Browse the World Wide Web
Comment[xh]=Browse the World Wide Web
Comment[zh_CN]=
Comment[zh_TW]=
GenericName=Web Browser
GenericName[ach]=Web Browser
GenericName[af]=Web Browser
GenericName[an]=Web Browser
GenericName[ar]=متصفح الإنترنت
GenericName[ast]=Web Browser
GenericName[az]=Web Browser
GenericName[be]=Вэб-браўзер
GenericName[bg]=Уеб браузър
GenericName[bn]=Web Browser
GenericName[br]=Merdeer Web
GenericName[brx]=Web Browser
GenericName[bs]=Web pretraživač
GenericName[ca]=Navegador web
GenericName[ca_valencia]=Web Browser
GenericName[cak]=Web Okik'amaya'l
GenericName[ckb]=Web Browser
GenericName[cs]=Webový prohlížeč
GenericName[cy]=Porwr Gwe
GenericName[da]=Webbrowser
GenericName[de]=Internet-Browser
GenericName[dsb]=Webwobglědowak
GenericName[el]=Πρόγραμμα περιήγησης
GenericName[en_CA]=Web Browser
GenericName[en_GB]=Web Browser
GenericName[eo]=Retumilo
GenericName[es_AR]=Navegador web
GenericName[es_CL]=Navegador Web
GenericName[es_ES]=Navegador web
GenericName[es_MX]=Navegador Web
GenericName[et]=Web Browser
GenericName[eu]=Web nabigatzailea
GenericName[fa]=Web Browser
GenericName[ff]=Web Browser
GenericName[fi]=Verkkoselain
GenericName[fr]=Navigateur web
GenericName[fur]=Navigadôr Web
GenericName[fy_NL]=Webbrowser
GenericName[ga_IE]=Web Browser
GenericName[gd]=Brabhsair-lìn
GenericName[gl]=Navegador web
GenericName[gn]=Ñanduti Kundahára
GenericName[gu_IN]=Web Browser
GenericName[he]=דפדפן אינטרנט
GenericName[hi_IN]=Web Browser
GenericName[hr]=Web preglednik
GenericName[hsb]=Webwobhladowak
GenericName[hu]=Webböngésző
GenericName[hy_AM]=Վեբ դիտարկիչ
GenericName[hye]=Web Browser
GenericName[ia]=Navigator web
GenericName[id]=Peramban Web
GenericName[is]=Vafri
GenericName[it]=Browser web
GenericName[ja]=
GenericName[ka]=
GenericName[kab]=Iminig web
GenericName[kk]=Веб-браузері
GenericName[km]=Web Browser
GenericName[kn]=Web Browser
GenericName[ko]=
GenericName[lij]=Navegatô Web
GenericName[lo]=
GenericName[lt]=Web Browser
GenericName[ltg]=Web Browser
GenericName[lv]=Tīmekļa pārlūks
GenericName[meh]=Web Browser
GenericName[mk]=Web Browser
GenericName[mr]=Web Browser
GenericName[ms]=Web Browser
GenericName[my]=Web Browser
GenericName[nb_NO]=Nettleser
GenericName[ne_NP]=Web Browser
GenericName[nl]=Webbrowser
GenericName[nn_NO]=Nettlesar
GenericName[oc]=Navegador Web
GenericName[pa_IN]=
GenericName[pl]=Przeglądarka internetowa
GenericName[pt_BR]=Navegador web
GenericName[pt_PT]=Navegador Web
GenericName[rm]=Navigatur web
GenericName[ro]=Web Browser
GenericName[ru]=Веб-браузер
GenericName[sat]=
GenericName[sc]=Navigadore web
GenericName[sco]=Web Browser
GenericName[si]=
GenericName[sk]=Webový prehliadač
GenericName[skr]=ویب براؤزر
GenericName[sl]=Spletni brskalnik
GenericName[son]=Web Browser
GenericName[sq]=Shfletues
GenericName[sr]=Веб прегледач
GenericName[sv_SE]=Webbläsare
GenericName[szl]=Web Browser
GenericName[ta]=Web Browser
GenericName[te]=Web Browser
GenericName[tg]=Браузери веб
GenericName[th]=
GenericName[tl]=Web Browser
GenericName[tr]=Web Tarayıcısı
GenericName[trs]=Web riña gāchē nu
GenericName[uk]=Браузер
GenericName[ur]=Web Browser
GenericName[uz]=Web Browser
GenericName[vi]=Trình duyt web
GenericName[wo]=Web Browser
GenericName[xh]=Web Browser
GenericName[zh_CN]=Web
GenericName[zh_TW]=
Keywords=Internet;WWW;Browser;Web;Explorer;
Keywords[ach]=Internet;WWW;Browser;Web;Explorer;
Keywords[af]=Internet;WWW;Browser;Web;Explorer;
Keywords[an]=Internet;WWW;Browser;Web;Explorer;
Keywords[ar]=Internet;WWW;Browser;Web;Explorer;
Keywords[ast]=Internet;WWW;Browser;Web;Explorer;
Keywords[az]=Internet;WWW;Browser;Web;Explorer;
Keywords[be]=Internet;WWW;Browser;Web;Explorer;
Keywords[bg]=Internet;WWW;Browser;Web;Explorer;
Keywords[bn]=Internet;WWW;Browser;Web;Explorer;
Keywords[br]=Internet;WWW;Merdeer;Web;Ergerzhout;
Keywords[brx]=Internet;WWW;Browser;Web;Explorer;
Keywords[bs]=Internet;WWW;Pretraživač;Web;Istraživač;
Keywords[ca]=Internet;WWW;Browser;Web;Explorador;Navegador;
Keywords[ca_valencia]=Internet;WWW;Browser;Web;Explorer;
Keywords[cak]=K'amaya'l;WWW;Okik'amaya'l;Kanob'äl;
Keywords[ckb]=Internet;WWW;Browser;Web;Explorer;
Keywords[cs]=internet;WWW;prohlížeč;web;
Keywords[cy]=Rhyngrwyd;WWW;Porwr;Gwe;Archwiliwr;
Keywords[da]=Internet;WWW;Browser;Nettet;Explorer;
Keywords[de]=Internet;WWW;Browser;Web;Explorer;
Keywords[dsb]=Internet;WWW;wobglědowak;Web;Explorer;
Keywords[el]=Internet;WWW;Browser;Web;Explorer;Διαδίκτυο;Ιστός;Ίντερνετ;
Keywords[en_CA]=Internet;WWW;Browser;Web;Explorer;
Keywords[en_GB]=Internet;WWW;Browser;Web;Explorer;
Keywords[eo]=Interreto;Retumilo;TTT;Teksaĵo;Reto;Internet;WWW;Browser;Web;Explorer;
Keywords[es_AR]=Internet;WWW;Navegador;Web;Explorador;
Keywords[es_CL]=Internet;WWW;Navegador;Web;Explorador;
Keywords[es_ES]=Internet;WWW;Navegador;Web;Explorador;
Keywords[es_MX]=Internet;WWW;Navegador;Web;Explorador;
Keywords[et]=Internet;WWW;Browser;Web;Explorer;
Keywords[eu]=Internet;WWW;Nabigatzailea;Web;Arakatzailea;
Keywords[fa]=Internet;WWW;Browser;Web;Explorer;
Keywords[ff]=Internet;WWW;Browser;Web;Explorer;
Keywords[fi]=Internet;WWW;Browser;Web;Explorer;netti;webbi;selain;
Keywords[fr]=Internet;WWW;Navigateur;Web;Explorer;
Keywords[fur]=Internet;WWW;Browser;Navigadôr;Web;Esploradôr;Explorer;
Keywords[fy_NL]=Ynternet;WWW;Browser;Web;Ferkenner;
Keywords[ga_IE]=Internet;WWW;Browser;Web;Explorer;
Keywords[gd]=Internet;WWW;Browser;Web;Explorer;eadar-lìon;brabhsair;brobhsair;lìon;taisgealaiche;
Keywords[gl]=Internet;WWW;Navegador;Web;Explorador;
Keywords[gn]=Internet;WWW;Browser;Web;Explorer;
Keywords[gu_IN]=Internet;WWW;Browser;Web;Explorer;
Keywords[he]=אינטרנט;WWW;דפדפן;רשת;סייר;מרשתת;
Keywords[hi_IN]=Internet;WWW;Browser;Web;Explorer;
Keywords[hr]=Internet;WWW;Preglednik;Web;Istraživač;
Keywords[hsb]=Internet;WWW;wobhladowak;Web;Explorer;
Keywords[hu]=Internet;WWW;Böngésző;Web;Világháló;
Keywords[hy_AM]=Համացանց,WWW,Զննիչ,Վեբ,Ցանցախույզ:
Keywords[hye]=Internet;WWW;Browser;Web;Explorer;
Keywords[ia]=Internet;WWW;Navigator;Web;Explorator;
Keywords[id]=Internet;WWW;Browser;Web;Explorer;
Keywords[is]=Internet;WWW; Vafri; Vefur; Explorer;
Keywords[it]=Internet;WWW;Browser;Web;Explorer;Navigatore;
Keywords[ja]=Internet;WWW;Browser;Web;Explorer;;;;
Keywords[ka]=;WWW;;; ;
Keywords[kab]=Internet;WWW;Browser;Web;Explorer;
Keywords[kk]=Internet;WWW;Browser;Web;Explorer;Интернет;Ғаламтор;Браузер;Желі;Шолғыш;
Keywords[km]=Internet;WWW;Browser;Web;Explorer;
Keywords[kn]=Internet;WWW;Browser;Web;Explorer;
Keywords[ko]=;;;;Internet;WWW;Browser;Web;Explorer;
Keywords[lij]=Internet;WWW;Browser;Web;Explorer;Navegatô;
Keywords[lo]=Internet;WWW;Browser;Web;Explorer;
Keywords[lt]=Internet;WWW;Browser;Web;Explorer;
Keywords[ltg]=Internet;WWW;Browser;Web;Explorer;
Keywords[lv]=Internets;WWW;Pārlūkprogramma;Tīmeklis;
Keywords[meh]=Internet;WWW;Browser;Web;Explorer;
Keywords[mk]=Internet;WWW;Browser;Web;Explorer;
Keywords[mr]=Internet;WWW;Browser;Web;Explorer;
Keywords[ms]=Internet;WWW;Browser;Web;Explorer;
Keywords[my]=Internet;WWW;Browser;Web;Explorer;
Keywords[nb_NO]=Internett;WWW;Nettleser;Web;Utforsker;
Keywords[ne_NP]=Internet;WWW;Browser;Web;Explorer;
Keywords[nl]=Internet;WWW;Browser;Web;Verkenner;
Keywords[nn_NO]=Internett;WWW;Nettlesar;Web;Utforskar;
Keywords[oc]=Internet;WWW;Navegador;Navigador;Navegator;Navigator;Web;Explorer;
Keywords[pa_IN]=;WWW;;;;;;
Keywords[pl]=Internet;WWW;Przeglądarka;Browser;Wyszukiwarka;Web;Sieć;Explorer;Eksplorer;Strony;Witryny;internetowe;
Keywords[pt_BR]=Internet;WWW;Browser;Web;Explorer;Navegador;
Keywords[pt_PT]=Internet;WWW;Navegador;Web;Explorador;
Keywords[rm]=Internet;WWW;Browser;Web;Explorer;navigatur;
Keywords[ro]=Internet;WWW;Browser;Web;Explorer;
Keywords[ru]=Сеть;Интернет;Браузер;Доступ в Интернет;
Keywords[sat]=Internet;WWW;Browser;Web;Explorer;
Keywords[sc]=Internet;WWW;Navigadore;Web;Explorer;
Keywords[sco]=Internet;WWW;Browser;Web;Explorer;
Keywords[si]=;;;;Internet;WWW;Browser;Web;Explorer;
Keywords[sk]=Internet;WWW;Prehliadač;Web;Prieskumník;
Keywords[skr]=Internet;WWW;Browser;Web;Explorer;
Keywords[sl]=internet;www;brskalnik;splet;
Keywords[son]=Internet;WWW;Browser;Web;Explorer;
Keywords[sq]=Internet;WWW;Shfletues;Web;Eksplorues;
Keywords[sr]=Internet;WWW;Browser;Web;Explorer;
Keywords[sv_SE]=Internet;WWW;Webbläsare;Webb;Utforskare;
Keywords[szl]=Internet;WWW;Browser;Web;Explorer;
Keywords[ta]=Internet;WWW;Browser;Web;Explorer;
Keywords[te]=Internet;WWW;Browser;Web;Explorer;
Keywords[tg]=Интернет;WWW;Браузер;Сомона;Ҷустуҷӯгар;
Keywords[th]=;;;Internet;WWW;Browser;Web;Explorer;
Keywords[tl]=Internet;WWW;Browser;Web;Explorer;
Keywords[tr]=Internet;WWW;Browser;Web;Explorer;İnternet;Tarayıcı;
Keywords[trs]=Internet;WWW;Browser;Web;Explorer;
Keywords[uk]=Інтернет;WWW;Браузер;Веб;Переглядач;
Keywords[ur]=Internet;WWW;Browser;Web;Explorer;
Keywords[uz]=Internet;WWW;Browser;Web;Explorer;
Keywords[vi]=Internet;WWW;Trình duyt;Web;Duyt web;
Keywords[wo]=Internet;WWW;Browser;Web;Explorer;
Keywords[xh]=Internet;WWW;Browser;Web;Explorer;
Keywords[zh_CN]=Internet;WWW;Browser;Web;Explorer;
Keywords[zh_TW]=;;;;;Internet;WWW;Browser;Web;Explorer;
X-GNOME-FullName=Firefox Web Browser
X-GNOME-FullName[ach]=Firefox Web Browser
X-GNOME-FullName[af]=Firefox Web Browser
X-GNOME-FullName[an]=Firefox Web Browser
X-GNOME-FullName[ar]=متصفح Firefox
X-GNOME-FullName[ast]=Firefox Web Browser
X-GNOME-FullName[az]=Firefox Web Browser
X-GNOME-FullName[be]=Вэб-браўзер Firefox
X-GNOME-FullName[bg]=Firefox Уеб браузър
X-GNOME-FullName[bn]=Firefox Web Browser
X-GNOME-FullName[br]=Merdeer Web Firefox
X-GNOME-FullName[brx]=Firefox Web Browser
X-GNOME-FullName[bs]=Firefox web pretraživač
X-GNOME-FullName[ca]=Navegador web Firefox
X-GNOME-FullName[ca_valencia]=Firefox Web Browser
X-GNOME-FullName[cak]=Firefox Web Browser
X-GNOME-FullName[ckb]=Firefox Web Browser
X-GNOME-FullName[cs]=Webový prohlížeč Firefox
X-GNOME-FullName[cy]=Porwr Gwe Firefox
X-GNOME-FullName[da]=Firefox-browser
X-GNOME-FullName[de]=Firefox-Web-Browser
X-GNOME-FullName[dsb]=Webwobglědowak Firefox
X-GNOME-FullName[el]=Πρόγραμμα περιήγησης Firefox
X-GNOME-FullName[en_CA]=Firefox Web Browser
X-GNOME-FullName[en_GB]=Firefox Web Browser
X-GNOME-FullName[eo]=Retumilo Firefox
X-GNOME-FullName[es_AR]=Navegador web Firefox
X-GNOME-FullName[es_CL]=Navegador web Firefox
X-GNOME-FullName[es_ES]=Navegador web Firefox
X-GNOME-FullName[es_MX]=Navegador web Firefox
X-GNOME-FullName[et]=Firefox Web Browser
X-GNOME-FullName[eu]=Firefox web nabigatzailea
X-GNOME-FullName[fa]=Firefox Web Browser
X-GNOME-FullName[ff]=Firefox Web Browser
X-GNOME-FullName[fi]=Firefox-verkkoselain
X-GNOME-FullName[fr]=Navigateur web Firefox
X-GNOME-FullName[fur]=Navigadôr web Firefox
X-GNOME-FullName[fy_NL]=Firefox-webbrowser
X-GNOME-FullName[ga_IE]=Firefox Web Browser
X-GNOME-FullName[gd]=Brabhsair-lìn Firefox
X-GNOME-FullName[gl]=Navegador web Firefox
X-GNOME-FullName[gn]=Firefox Ñanduti Kundahára
X-GNOME-FullName[gu_IN]=Firefox Web Browser
X-GNOME-FullName[he]=דפדפן אינטרנט Firefox
X-GNOME-FullName[hi_IN]=Firefox
X-GNOME-FullName[hr]=Firefox web preglednik
X-GNOME-FullName[hsb]=Webwobhladowak Firefox
X-GNOME-FullName[hu]=Firefox webböngésző
X-GNOME-FullName[hy_AM]=Firefox վեբ դիտարկիչ
X-GNOME-FullName[hye]=Firefox Web Browser
X-GNOME-FullName[ia]=Navigator web Firefox
X-GNOME-FullName[id]=Firefox Peramban Web
X-GNOME-FullName[is]=Firefox-vafri
X-GNOME-FullName[it]=Browser web Firefox
X-GNOME-FullName[ja]=Firefox
X-GNOME-FullName[ka]=Firefox-
X-GNOME-FullName[kab]=Iminig web Firefox
X-GNOME-FullName[kk]=Firefox веб-браузері
X-GNOME-FullName[km]=Firefox Web Browser
X-GNOME-FullName[kn]=Firefox Web Browser
X-GNOME-FullName[ko]=Firefox
X-GNOME-FullName[lij]=Firefox Navegatô Web
X-GNOME-FullName[lo]=Firefox
X-GNOME-FullName[lt]=Firefox Web Browser
X-GNOME-FullName[ltg]=Firefox Web Browser
X-GNOME-FullName[lv]=Firefox tīmekļa pārlūks
X-GNOME-FullName[meh]=Firefox Web Browser
X-GNOME-FullName[mk]=Firefox Web Browser
X-GNOME-FullName[mr]=Firefox Web Browser
X-GNOME-FullName[ms]=Firefox Web Browser
X-GNOME-FullName[my]=Firefox Web Browser
X-GNOME-FullName[nb_NO]=Firefox-nettleser
X-GNOME-FullName[ne_NP]=Firefox Web Browser
X-GNOME-FullName[nl]=Firefox-webbrowser
X-GNOME-FullName[nn_NO]=Firefox-nettlesar
X-GNOME-FullName[oc]=Navegador web Firefox
X-GNOME-FullName[pa_IN]=Firefox
X-GNOME-FullName[pl]=Przeglądarka Firefox
X-GNOME-FullName[pt_BR]=Navegador web Firefox
X-GNOME-FullName[pt_PT]=Navegador Web Firefox
X-GNOME-FullName[rm]=Navigatur-web Firefox
X-GNOME-FullName[ro]=Firefox Web Browser
X-GNOME-FullName[ru]=Веб-браузер Firefox
X-GNOME-FullName[sat]=Firefox
X-GNOME-FullName[sc]=Navigadore web Firefox
X-GNOME-FullName[sco]=Firefox Web Browser
X-GNOME-FullName[si]=Firefox
X-GNOME-FullName[sk]=Webový prehliadač Firefox
X-GNOME-FullName[skr]=Firefox ویب براؤزر
X-GNOME-FullName[sl]=Spletni brskalnik Firefox
X-GNOME-FullName[son]=Firefox Web Browser
X-GNOME-FullName[sq]=Shfletuesi Firefox
X-GNOME-FullName[sr]=Firefox веб прегледач
X-GNOME-FullName[sv_SE]=Firefox webbläsare
X-GNOME-FullName[szl]=Firefox Web Browser
X-GNOME-FullName[ta]=Firefox Web Browser
X-GNOME-FullName[te]=Firefox Web Browser
X-GNOME-FullName[tg]=Браузери интернетии «Firefox»
X-GNOME-FullName[th]= Firefox
X-GNOME-FullName[tl]=Firefox Web Browser
X-GNOME-FullName[tr]=Firefox Web Tarayıcısı
X-GNOME-FullName[trs]=Firefox Web riña gāchē nu
X-GNOME-FullName[uk]=Браузер Firefox
X-GNOME-FullName[ur]=Firefox Web Browser
X-GNOME-FullName[uz]=Firefox Web Browser
X-GNOME-FullName[vi]=Trình duyt Web Firefox
X-GNOME-FullName[wo]=Firefox Web Browser
X-GNOME-FullName[xh]=Firefox Web Browser
X-GNOME-FullName[zh_CN]=Firefox
X-GNOME-FullName[zh_TW]=Firefox
[Desktop Action new-window]
Exec=/usr/lib/firefox/firefox --new-window %u
Name=New Window
Name[ach]=New Window
Name[af]=New Window
Name[an]=New Window
Name[ar]=نافذة جديدة
Name[ast]=New Window
Name[az]=New Window
Name[be]=Новае акно
Name[bg]=Нов прозорец
Name[bn]=New Window
Name[br]=Prenestr nevez
Name[brx]=New Window
Name[bs]=Novi prozor
Name[ca]=Finestra nova
Name[ca_valencia]=New Window
Name[cak]=K'ak'a' Tzuwäch
Name[ckb]=New Window
Name[cs]=Nové okno
Name[cy]=Ffenestr Newydd
Name[da]=Nyt vindue
Name[de]=Neues Fenster
Name[dsb]=Nowe wokno
Name[el]=Νέο παράθυρο
Name[en_CA]=New Window
Name[en_GB]=New Window
Name[eo]=Nova fenestro
Name[es_AR]=Nueva ventana
Name[es_CL]=Nueva ventana
Name[es_ES]=Nueva ventana
Name[es_MX]=Nueva ventana
Name[et]=New Window
Name[eu]=Leiho berria
Name[fa]=New Window
Name[ff]=New Window
Name[fi]=Uusi ikkuna
Name[fr]=Nouvelle fenêtre
Name[fur]=Gnûf barcon
Name[fy_NL]=Nij finster
Name[ga_IE]=New Window
Name[gd]=Uinneag ùr
Name[gl]=Nova xanela
Name[gn]=Ovetã pyahu
Name[gu_IN]=New Window
Name[he]=חלון חדש
Name[hi_IN]=New Window
Name[hr]=Novi prozor
Name[hsb]=Nowe wokno
Name[hu]=Új ablak
Name[hy_AM]=Նոր պատուհան
Name[hye]=New Window
Name[ia]=Nove fenestra
Name[id]=Jendela Baru
Name[is]=Nýr gluggi
Name[it]=Nuova finestra
Name[ja]=
Name[ka]=
Name[kab]=Asfaylu amaynut
Name[kk]=Жаңа терезе
Name[km]=New Window
Name[kn]=New Window
Name[ko]=
Name[lij]=Neuvo Barcon
Name[lo]=
Name[lt]=New Window
Name[ltg]=New Window
Name[lv]=Jauns logs
Name[meh]=New Window
Name[mk]=New Window
Name[mr]=New Window
Name[ms]=New Window
Name[my]=New Window
Name[nb_NO]=Nytt vindu
Name[ne_NP]=New Window
Name[nl]=Nieuw venster
Name[nn_NO]=Nytt vindauge
Name[oc]=Fenèstra novèla
Name[pa_IN]= ਿ
Name[pl]=Nowe okno
Name[pt_BR]=Nova janela
Name[pt_PT]=Nova janela
Name[rm]=Nova fanestra
Name[ro]=New Window
Name[ru]=Новое окно
Name[sat]=
Name[sc]=Ventana noa
Name[sco]=New Window
Name[si]=
Name[sk]=Nové okno
Name[skr]=نویں ونڈو
Name[sl]=Novo okno
Name[son]=New Window
Name[sq]=Dritare e Re
Name[sr]=Нови прозор
Name[sv_SE]=Nytt fönster
Name[szl]=New Window
Name[ta]=New Window
Name[te]=New Window
Name[tg]=Равзанаи нав
Name[th]=
Name[tl]=New Window
Name[tr]=Yeni pencere
Name[trs]=Bēntanâ nākàa
Name[uk]=Нове вікно
Name[ur]=New Window
Name[uz]=New Window
Name[vi]=Ca s mi
Name[wo]=New Window
Name[xh]=New Window
Name[zh_CN]=
Name[zh_TW]=
[Desktop Action new-private-window]
Exec=/usr/lib/firefox/firefox --private-window %u
Name=New Private Window
Name[ach]=New Private Window
Name[af]=New Private Window
Name[an]=New Private Window
Name[ar]=نافذة خاصة جديدة
Name[ast]=New Private Window
Name[az]=New Private Window
Name[be]=Новае прыватнае акно
Name[bg]=Нов личен прозорец
Name[bn]=New Private Window
Name[br]=Prenestr prevez nevez
Name[brx]=New Private Window
Name[bs]=Novi privatni prozor
Name[ca]=Finestra privada nova
Name[ca_valencia]=New Private Window
Name[cak]=K'ak'a' Ichinan Tzuwäch
Name[ckb]=New Private Window
Name[cs]=Nové anonymní okno
Name[cy]=Ffenestr Breifat Newydd
Name[da]=Nyt privat vindue
Name[de]=Neues privates Fenster
Name[dsb]=Nowe priwatne wokno
Name[el]=Νέο ιδιωτικό παράθυρο
Name[en_CA]=New Private Window
Name[en_GB]=New Private Window
Name[eo]=Nova privata fenestro
Name[es_AR]=Nueva ventana privada
Name[es_CL]=Nueva ventana privada
Name[es_ES]=Nueva ventana privada
Name[es_MX]=Nueva ventana privada
Name[et]=New Private Window
Name[eu]=Leiho pribatu berria
Name[fa]=New Private Window
Name[ff]=New Private Window
Name[fi]=Uusi yksityinen ikkuna
Name[fr]=Nouvelle fenêtre privée
Name[fur]=Gnûf barcon privât
Name[fy_NL]=Nij priveefinster
Name[ga_IE]=New Private Window
Name[gd]=Uinneag phrìobhaideach ùr
Name[gl]=Nova xanela privada
Name[gn]=Ovetã ñemi pyahu
Name[gu_IN]=New Private Window
Name[he]=חלון פרטי חדש
Name[hi_IN]=New Private Window
Name[hr]=Novi privatni prozor
Name[hsb]=Nowe priwatne wokno
Name[hu]=Új privát ablak
Name[hy_AM]=Նոր գաղտնի պատուհան
Name[hye]=New Private Window
Name[ia]=Nove fenestra private
Name[id]=Jendela Mode Pribadi Baru
Name[is]=Nýr huliðsgluggi
Name[it]=Nuova finestra anonima
Name[ja]=
Name[ka]=
Name[kab]=Asfaylu amaynut n tunigin tusligt
Name[kk]=Жаңа жекелік терезе
Name[km]=New Private Window
Name[kn]=New Private Window
Name[ko]=
Name[lij]=Neuvo Barcon Privòu
Name[lo]=
Name[lt]=New Private Window
Name[ltg]=New Private Window
Name[lv]=Jauns privātais logs
Name[meh]=New Private Window
Name[mk]=New Private Window
Name[mr]=New Private Window
Name[ms]=New Private Window
Name[my]=New Private Window
Name[nb_NO]=Nytt privat vindu
Name[ne_NP]=New Private Window
Name[nl]=Nieuw privévenster
Name[nn_NO]=Nytt privat vindauge
Name[oc]=Fenèstra privada novèla
Name[pa_IN]= ਿ
Name[pl]=Nowe okno prywatne
Name[pt_BR]=Nova janela privativa
Name[pt_PT]=Nova janela privada
Name[rm]=Nova fanestra privata
Name[ro]=New Private Window
Name[ru]=Новое приватное окно
Name[sat]=
Name[sc]=Ventana privada noa
Name[sco]=New Private Window
Name[si]= .
Name[sk]=Nové súkromné okno
Name[skr]=نویں نجی ونڈو
Name[sl]=Novo zasebno okno
Name[son]=New Private Window
Name[sq]=Dritare e Re Private
Name[sr]=Нови приватни прозор
Name[sv_SE]=Nytt privat fönster
Name[szl]=New Private Window
Name[ta]=New Private Window
Name[te]=New Private Window
Name[tg]=Равзанаи хусусии нав
Name[th]=
Name[tl]=New Private Window
Name[tr]=Yeni gizli pencere
Name[trs]=Bēntanâ huì nākàa
Name[uk]=Приватне вікно
Name[ur]=New Private Window
Name[uz]=New Private Window
Name[vi]=Ca s riêng tư mi
Name[wo]=New Private Window
Name[xh]=New Private Window
Name[zh_CN]=
Name[zh_TW]=
[Desktop Action open-profile-manager]
Exec=/usr/lib/firefox/firefox --ProfileManager
Name=Open Profile Manager
Name[ach]=Open Profile Manager
Name[af]=Open Profile Manager
Name[an]=Open Profile Manager
Name[ar]=افتح مدير الملف الشخصي
Name[ast]=Open Profile Manager
Name[az]=Open Profile Manager
Name[be]=Адкрыць менеджар профіляў
Name[bg]=Отваряне на мениджъра на профили
Name[bn]=Open Profile Manager
Name[br]=Digeriñ an ardoer aeladoù
Name[brx]=Open Profile Manager
Name[bs]=Otvori Menadžera profila
Name[ca]=Obre el gestor de perfils
Name[ca_valencia]=Open Profile Manager
Name[cak]=Open Profile Manager
Name[ckb]=Open Profile Manager
Name[cs]=Otevřete Správce profilů
Name[cy]=Agorwch y Rheolwr Proffil
Name[da]=Åbn profilhåndtering
Name[de]=Profilverwaltung öffnen
Name[dsb]=Profilowy zastojnik wócyniś
Name[el]=Άνοιγμα Διαχείρισης προφίλ
Name[en_CA]=Open Profile Manager
Name[en_GB]=Open Profile Manager
Name[eo]=Malfermi administranton de profiloj
Name[es_AR]=Abrir administrador de perfiles
Name[es_CL]=Abrir administrador de perfiles
Name[es_ES]=Abrir administrador de perfiles
Name[es_MX]=Abrir administrador de perfiles
Name[et]=Open Profile Manager
Name[eu]=Ireki profilen kudeatzailea
Name[fa]=Open Profile Manager
Name[ff]=Open Profile Manager
Name[fi]=Avaa profiilien hallinta
Name[fr]=Ouvrir le gestionnaire de profils
Name[fur]=Vierç gjestôr profîi
Name[fy_NL]=Profylbehearder iepenje
Name[ga_IE]=Open Profile Manager
Name[gd]=Fosgail manaidsear nam pròifilean
Name[gl]=Abrir o xestor de perfís
Name[gn]=Embojuruja mbaete ñangarekoha
Name[gu_IN]=Open Profile Manager
Name[he]=פתיחת מנהל הפרופילים
Name[hi_IN]=Open Profile Manager
Name[hr]=Otvori upravljač profila
Name[hsb]=Zrjadowak profilow wočinić
Name[hu]=Profilkezelő megnyitása
Name[hy_AM]=Բացեք պրոֆիլի կառավարիչը
Name[hye]=Open Profile Manager
Name[ia]=Aperir le gestor de profilo
Name[id]=Buka Pengelola Profil
Name[is]=Opna umsýslu notandasniða
Name[it]=Apri gestore profili
Name[ja]=
Name[ka]=
Name[kab]=Ldi amsefrak n umaɣnu
Name[kk]=Профильдер бақарушысын ашу
Name[km]=Open Profile Manager
Name[kn]=Open Profile Manager
Name[ko]=
Name[lij]=Open Profile Manager
Name[lo]=
Name[lt]=Open Profile Manager
Name[ltg]=Open Profile Manager
Name[lv]=Atvērt profilu pārvaldnieku
Name[meh]=Open Profile Manager
Name[mk]=Open Profile Manager
Name[mr]=Open Profile Manager
Name[ms]=Open Profile Manager
Name[my]=Open Profile Manager
Name[nb_NO]=Åpne profilbehandler
Name[ne_NP]=Open Profile Manager
Name[nl]=Profielbeheerder openen
Name[nn_NO]=Opne profilhandsaming
Name[oc]=Dobrir lo gestionari de perfils
Name[pa_IN]=
Name[pl]=Menedżer profili
Name[pt_BR]=Abrir gerenciador de perfis
Name[pt_PT]=Abrir o Gestor de Perfis
Name[rm]=Avrir l'administraziun da profils
Name[ro]=Open Profile Manager
Name[ru]=Открыть менеджер профилей
Name[sat]=
Name[sc]=Aberi su gestore de profilos
Name[sco]=Open Profile Manager
Name[si]=
Name[sk]=Otvoriť Správcu profilov
Name[skr]=پروفائل منیجر کھولو
Name[sl]=Odpri upravitelja profilov
Name[son]=Open Profile Manager
Name[sq]=Hapni Përgjegjës Profilesh
Name[sr]=Отворите управљач профила
Name[sv_SE]=Öppna Profilhanteraren
Name[szl]=Open Profile Manager
Name[ta]=Open Profile Manager
Name[te]=Open Profile Manager
Name[tg]=Кушодани мудири профилҳо
Name[th]=
Name[tl]=Open Profile Manager
Name[tr]=Profil yöneticisini aç
Name[trs]=Sa nīkāj ñuūnj nej perfî huā nìnï̀nj ïn
Name[uk]=Відкрити менеджер профілів
Name[ur]=Open Profile Manager
Name[uz]=Open Profile Manager
Name[vi]=M trình qun lý h sơ
Name[wo]=Open Profile Manager
Name[xh]=Open Profile Manager
Name[zh_CN]=
Name[zh_TW]=