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

Merge pull request #1003 from JakeStanger/refactor/image

Overhaul `.desktop` and image resolver code
This commit is contained in:
Jake Stanger 2025-05-25 19:29:07 +01:00 committed by GitHub
commit e99a04923d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 1891 additions and 682 deletions

20
.idea/runConfigurations/Test.xml generated Normal file
View file

@ -0,0 +1,20 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Test" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="buildProfileId" value="test" />
<option name="command" value="test" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<envs />
<option name="emulateTerminal" value="true" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>

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

@ -280,11 +280,12 @@ Check [here](config) for an example config file for a fully configured bar in ea
The following table lists each of the top-level bar config options:
| Name | Type | Default | Description |
|--------------------|-----------------------------------------|---------|-------------------------------------------------------------------------------|
| `ironvar_defaults` | `Map<string, string>` | `{}` | Map of [ironvar](ironvars) keys against their default values. |
| `monitors` | `Map<string, BarConfig or BarConfig[]>` | `null` | Map of monitor names against bar configs. |
| `icon_overrides` | `Map<string, string>` | `{}` | Map of app IDs (or classes) to icon names, overriding the app's default icon. |
| Name | Type | Default | Description |
|--------------------|-----------------------------------------|---------|--------------------------------------------------------------------------------------------------------------------------------|
| `ironvar_defaults` | `Map<string, string>` | `{}` | Map of [ironvar](ironvars) keys against their default values. |
| `monitors` | `Map<string, BarConfig or BarConfig[]>` | `null` | Map of monitor names against bar configs. |
| `icon_theme` | `string` | `null` | Name of the GTK icon theme to use. Leave blank to use default. |
| `icon_overrides` | `Map<string, string>` | `{}` | Map of image inputs to override names. Usually used for app IDs (or classes) to icon names, overriding the app's default icon. |
> [!TIP]
> `monitors` is only required if you are following **2b** or **2c** (ie not the same bar across all monitors).
@ -309,7 +310,6 @@ The following table lists each of the bar-level bar config options:
| `layer` | `background` or `bottom` or `top` or `overlay` | `top` | The layer-shell layer to place the bar on. |
| `exclusive_zone` | `boolean` | `true` unless `start_hidden` is enabled. | Whether the bar should reserve an exclusive zone around it. |
| `popup_gap` | `integer` | `5` | The gap between the bar and popup window. |
| `icon_theme` | `string` | `null` | Name of the GTK icon theme to use. Leave blank to use default. |
| `start_hidden` | `boolean` | `false`, or `true` if `autohide` set | Whether the bar should be hidden when the application starts. Enabled by default when `autohide` is set. |
| `autohide` | `integer` | `null` | The duration in milliseconds before the bar is hidden after the cursor leaves. Leave unset to disable auto-hide behaviour. |
| `start` | `Module[]` | `[]` | Array of left or top modules. |

View file

@ -6,11 +6,9 @@ use color_eyre::Result;
use glib::Propagation;
use gtk::gdk::Monitor;
use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, IconTheme, Orientation, Window, WindowType};
use gtk::{Application, ApplicationWindow, Orientation, Window, WindowType};
use gtk_layer_shell::LayerShell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Arc;
use std::time::Duration;
use tracing::{debug, info};
@ -25,7 +23,6 @@ pub struct Bar {
name: String,
monitor_name: String,
monitor_size: (i32, i32),
icon_overrides: Arc<HashMap<String, String>>,
position: BarPosition,
ironbar: Rc<Ironbar>,
@ -46,7 +43,6 @@ impl Bar {
app: &Application,
monitor_name: String,
monitor_size: (i32, i32),
icon_overrides: Arc<HashMap<String, String>>,
config: BarConfig,
ironbar: Rc<Ironbar>,
) -> Self {
@ -96,7 +92,6 @@ impl Bar {
name,
monitor_name,
monitor_size,
icon_overrides,
position,
ironbar,
window,
@ -257,11 +252,6 @@ impl Bar {
monitor: &Monitor,
output_size: (i32, i32),
) -> Result<BarLoadResult> {
let icon_theme = IconTheme::new();
if let Some(ref theme) = config.icon_theme {
icon_theme.set_custom_theme(Some(theme));
}
let app = &self.window.application().expect("to exist");
macro_rules! info {
@ -272,15 +262,13 @@ impl Bar {
monitor,
output_name: &self.monitor_name,
location: $location,
icon_theme: &icon_theme,
icon_overrides: self.icon_overrides.clone(),
}
};
}
// popup ignores module location so can bodge this for now
let popup = Popup::new(
self.ironbar.clone(),
&self.ironbar,
&info!(ModuleLocation::Left),
output_size,
config.popup_gap,
@ -404,17 +392,9 @@ pub fn create_bar(
monitor: &Monitor,
monitor_name: String,
monitor_size: (i32, i32),
icon_overrides: Arc<HashMap<String, String>>,
config: BarConfig,
ironbar: Rc<Ironbar>,
) -> Result<Bar> {
let bar = Bar::new(
app,
monitor_name,
monitor_size,
icon_overrides,
config,
ironbar,
);
let bar = Bar::new(app, monitor_name, monitor_size, config, ironbar);
bar.init(monitor)
}

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

@ -140,7 +140,7 @@ pub async fn create_client() -> Result<Arc<Client>> {
spawn(async move {
if let Err(error) = client.run().await {
error!("{}", error);
};
}
});
}
Ok(client)

View file

@ -157,7 +157,7 @@ impl Client {
Event::Toplevel(event) => toplevel_tx.send_expect(event),
#[cfg(feature = "clipboard")]
Event::Clipboard(item) => clipboard_tx.send_expect(item),
};
}
}
});
}

View file

@ -25,13 +25,11 @@ pub struct LayoutConfig {
impl LayoutConfig {
pub fn orientation(&self, info: &ModuleInfo) -> gtk::Orientation {
self.orientation
.map(ModuleOrientation::into)
.unwrap_or(info.bar_position.orientation())
.map_or(info.bar_position.orientation(), ModuleOrientation::into)
}
pub fn angle(&self, info: &ModuleInfo) -> f64 {
self.orientation
.map(ModuleOrientation::to_angle)
.unwrap_or(info.bar_position.angle())
.map_or(info.bar_position.angle(), ModuleOrientation::to_angle)
}
}

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,195 @@
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 +197,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

@ -18,12 +18,11 @@ pub fn handle_command(command: IronvarCommand) -> Response {
if key.contains('.') {
for part in key.split('.') {
ns = match ns.get_namespace(part) {
Some(ns) => ns.clone(),
None => {
key = part.into();
break;
}
ns = if let Some(ns) = ns.get_namespace(part) {
ns.clone()
} else {
key = part.into();
break;
};
}
}

View file

@ -80,7 +80,7 @@ impl Namespace for VariableManager {
let namespaces = read_lock!(self.namespaces);
let ns = namespaces.get(ns)?;
ns.get(key).map(|v| v.to_owned())
ns.get(key).as_deref().map(ToOwned::to_owned)
} else {
read_lock!(self.variables).get(key).and_then(IronVar::get)
}

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,
@ -370,7 +394,7 @@ fn load_output_bars(
};
let config = ironbar.config.borrow();
let icon_overrides = Arc::new(config.icon_overrides.clone());
let display = get_display();
let pos = output.logical_position.unwrap_or_default();
@ -392,7 +416,6 @@ fn load_output_bars(
&monitor,
monitor_name.to_string(),
output_size,
icon_overrides,
config.clone(),
ironbar.clone(),
)?]
@ -405,7 +428,6 @@ fn load_output_bars(
&monitor,
monitor_name.to_string(),
output_size,
icon_overrides.clone(),
config.clone(),
ironbar.clone(),
)
@ -416,7 +438,6 @@ fn load_output_bars(
&monitor,
monitor_name.to_string(),
output_size,
icon_overrides,
config.bar.clone(),
ironbar.clone(),
)?],

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,11 @@ 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 +272,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 {
@ -25,23 +23,16 @@ pub struct Item {
pub open_state: OpenState,
pub windows: IndexMap<usize, Window>,
pub name: String,
pub icon_override: String,
}
impl Item {
pub fn new(
app_id: String,
icon_override: String,
open_state: OpenState,
favorite: bool,
) -> Self {
pub fn new(app_id: String, open_state: OpenState, favorite: bool) -> Self {
Self {
app_id,
favorite,
open_state,
windows: IndexMap::new(),
name: String::new(),
icon_override,
}
}
@ -116,7 +107,6 @@ impl From<ToplevelInfo> for Item {
open_state,
windows,
name,
icon_override: String::new(),
}
}
}
@ -166,7 +156,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>,
@ -181,21 +171,18 @@ impl ItemButton {
}
if appearance.show_icons {
let input = if !item.icon_override.is_empty() {
item.icon_override.clone()
} else if item.app_id.is_empty() {
let input = if item.app_id.is_empty() {
item.name.clone()
} 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,26 +440,25 @@ 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,
self.layout.orientation(info),
IconContext {
icon_back: &self.icons.page_back,
icon_fwd: &self.icons.page_forward,
icon_size: self.pagination_icon_size,
icon_theme: info.icon_theme,
&IconContext {
back: &self.icons.page_back,
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();
@ -517,16 +499,16 @@ impl Module<gtk::Box> for LauncherModule {
let button = ItemButton::new(
&item,
appearance_options,
&icon_theme,
image_provider.clone(),
bar_position,
&tx,
&controller_tx,
);
if self.reversed {
container.pack_end(button.button.deref(), false, false, 0);
container.pack_end(&*button.button, false, false, 0);
} else {
container.add(button.button.deref());
container.add(&*button.button);
}
if buttons.len() + 1 >= pagination.offset() + page_size {
@ -599,7 +581,7 @@ impl Module<gtk::Box> for LauncherModule {
}
}
LauncherUpdate::Hover(_) => {}
};
}
};
rx.recv_glib(handle_event);

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;
@ -14,10 +15,9 @@ pub struct Pagination {
}
pub struct IconContext<'a> {
pub icon_back: &'a str,
pub icon_fwd: &'a str,
pub icon_size: i32,
pub icon_theme: &'a IconTheme,
pub back: &'a str,
pub fwd: &'a str,
pub size: i32,
}
impl Pagination {
@ -25,21 +25,16 @@ 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,
);
let scroll_back =
IconButton::new(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,
);
let scroll_fwd =
IconButton::new(icon_context.fwd, icon_context.size, image_provider.clone());
scroll_back.set_sensitive(false);
scroll_fwd.set_sensitive(false);
@ -86,8 +81,8 @@ impl Pagination {
if page_size < *offset {
*offset -= page_size;
} else {
*offset = 1
};
*offset = 1;
}
Self::update_page(&container, *offset, page_size);

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

@ -156,7 +156,7 @@ impl Module<Overlay> for NotificationsModule {
match initial_state {
Ok(ev) => tx.send_update(ev).await,
Err(err) => error!("{err:?}"),
};
}
while let Ok(ev) = rx.recv().await {
tx.send_update(ev).await;

View file

@ -251,7 +251,7 @@ impl Module<gtk::Box> for SysInfoModule {
RefreshType::Disks => client.refresh_disks(),
RefreshType::Network => client.refresh_network(),
RefreshType::System => client.refresh_load_average(),
};
}
for (i, token_set) in format_tokens.iter().enumerate() {
let is_affected = token_set

View file

@ -237,7 +237,7 @@ fn parse_formatting(chars: &mut Peekable<Chars>, mut formatting: Formatting) ->
formatting.align = Alignment::try_from(char)?;
}
(_, FormattingMode::WidthFillAlign) => formatting.fill = char,
};
}
next_char = chars.next();
}

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

@ -35,7 +35,7 @@ impl Popup {
/// This includes setting up gtk-layer-shell
/// and an empty `gtk::Box` container.
pub fn new(
ironbar: Rc<Ironbar>,
ironbar: &Ironbar,
module_info: &ModuleInfo,
output_size: (i32, i32),
gap: i32,

View file

@ -217,7 +217,7 @@ impl Script {
}
Err(err) => error!("{err:?}"),
},
};
}
sleep(tokio::time::Duration::from_millis(self.interval)).await;
}

View file

@ -35,7 +35,7 @@ pub fn load_css(style_path: PathBuf, application: Application) {
.suggestion("Check the CSS file for errors")
.suggestion("GTK CSS uses a subset of the full CSS spec and many properties are not available. Ensure you are not using any unsupported property.")
)
};
}
let screen = gdk::Screen::default().expect("Failed to get default GTK screen");
StyleContext::add_provider_for_screen(

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]=