mirror of
https://github.com/Zedfrigg/ironbar.git
synced 2025-08-16 22:31:03 +02:00
refactor: overhaul .desktop
and image resolver systems
Rewrites the desktop file parser code and image resolver code to introduce caching system and make fully async. They should be much faster now. BREAKING CHANGE: The `icon_theme` setting has been moved from per-bar to top-level
This commit is contained in:
parent
ca524f19f6
commit
3e55d87c3a
26 changed files with 1840 additions and 600 deletions
|
@ -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"] }
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -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()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds directories that should contain `.desktop` files
|
||||
/// and exist on the filesystem.
|
||||
fn find_application_dirs() -> Vec<PathBuf> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
#[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| {
|
||||
/// 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))
|
||||
fn setup() {
|
||||
unsafe {
|
||||
let pwd = env::current_dir().unwrap();
|
||||
env::set_var("XDG_DATA_DIRS", format!("{}/test-configs", pwd.display()));
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<_>>();
|
||||
#[tokio::test]
|
||||
async fn find_by_filename() {
|
||||
setup();
|
||||
|
||||
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())
|
||||
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");
|
||||
}
|
||||
|
||||
/// 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());
|
||||
#[tokio::test]
|
||||
async fn find_by_file_contents() {
|
||||
setup();
|
||||
|
||||
let files = files
|
||||
.iter()
|
||||
.filter_map(|file| {
|
||||
let parsed_desktop_file = parse_desktop_file(file)?;
|
||||
let desktop_files = DesktopFiles::new();
|
||||
|
||||
desktop_files_cache.insert(file.clone(), parsed_desktop_file.clone());
|
||||
Some((file.clone(), parsed_desktop_file))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let file = desktop_files.find_by_file_contents("427520").await.unwrap();
|
||||
|
||||
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()
|
||||
assert!(file.is_some());
|
||||
assert_eq!(file.unwrap().file_name, "Factorio.desktop");
|
||||
}
|
||||
|
||||
/// 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;
|
||||
};
|
||||
#[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();
|
||||
|
||||
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
|
||||
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()));
|
||||
}
|
||||
})
|
||||
.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)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
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
|
||||
{
|
||||
Some(_) => {
|
||||
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();
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
|
||||
|
|
|
@ -16,4 +16,4 @@ mod provider;
|
|||
feature = "workspaces",
|
||||
))]
|
||||
pub use self::gtk::*;
|
||||
pub use provider::ImageProvider;
|
||||
pub use provider::{Provider, create_and_load_surface};
|
||||
|
|
|
@ -1,115 +1,198 @@
|
|||
use crate::channels::{AsyncSenderExt, MpscReceiverExt};
|
||||
use crate::desktop_file::get_desktop_icon_name;
|
||||
#[cfg(feature = "http")]
|
||||
use crate::spawn;
|
||||
use cfg_if::cfg_if;
|
||||
use crate::desktop_file::DesktopFiles;
|
||||
use crate::{arc_mut, lock};
|
||||
use color_eyre::{Help, Report, Result};
|
||||
use gtk::cairo::Surface;
|
||||
use gtk::gdk::ffi::gdk_cairo_surface_create_from_pixbuf;
|
||||
use gtk::gdk_pixbuf::Pixbuf;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{IconLookupFlags, IconTheme};
|
||||
use std::path::{Path, PathBuf};
|
||||
#[cfg(feature = "http")]
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
cfg_if!(
|
||||
if #[cfg(feature = "http")] {
|
||||
use gtk::gio::{Cancellable, MemoryInputStream};
|
||||
use tracing::error;
|
||||
}
|
||||
);
|
||||
use gtk::prelude::*;
|
||||
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};
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ImageLocation<'a> {
|
||||
Icon {
|
||||
name: String,
|
||||
theme: &'a IconTheme,
|
||||
},
|
||||
#[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, 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))
|
||||
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 {
|
||||
None
|
||||
}
|
||||
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,108 +200,150 @@ 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
|
||||
let location = self
|
||||
.desktop_files
|
||||
.find(input_name)
|
||||
.await?
|
||||
.and_then(|input| input.icon);
|
||||
|
||||
if let Some(location) = location {
|
||||
if location == input {
|
||||
None
|
||||
} else {
|
||||
warn!("Failed to find image: {input}");
|
||||
fallback!()
|
||||
}
|
||||
}
|
||||
None => {
|
||||
warn!("Failed to find image: {input}");
|
||||
fallback!()
|
||||
Box::pin(self.resolve_location(&location, size, recurse_depth + 1)).await?
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
Ok(location)
|
||||
}
|
||||
|
||||
/// Attempts to fetch the image from the location
|
||||
/// and load it into the provided `GTK::Image` widget.
|
||||
pub fn load_into_image(&self, image: >k::Image) -> Result<()> {
|
||||
// handle remote locations async to avoid blocking UI thread while downloading
|
||||
/// Attempts to load the provided `ImageRef` into a `Pixbuf`.
|
||||
///
|
||||
/// 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")]
|
||||
if let ImageLocation::Remote(url) = &self.location {
|
||||
let url = url.clone();
|
||||
let (tx, rx) = mpsc::channel(64);
|
||||
Some(ImageLocation::Remote(uri)) => {
|
||||
let res = reqwest::get(uri.clone()).await?;
|
||||
|
||||
spawn(async move {
|
||||
let bytes = Self::get_bytes_from_http(url).await;
|
||||
if let Ok(bytes) = bytes {
|
||||
tx.send_expect(bytes).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 size = self.size;
|
||||
let image = image.clone();
|
||||
rx.recv_glib(move |bytes| {
|
||||
let stream = MemoryInputStream::from_bytes(&bytes);
|
||||
let scaled_size = image_ref.size * scale;
|
||||
|
||||
let scale = image.scale_factor();
|
||||
let scaled_size = size * scale;
|
||||
|
||||
let pixbuf = Pixbuf::from_stream_at_scale(
|
||||
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:?}"),
|
||||
_ => {}
|
||||
)
|
||||
.map(Some)
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
self.load_into_image_sync(image)?;
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "http"))]
|
||||
self.load_into_image_sync(image)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempts to synchronously fetch an image from location
|
||||
/// and load into into the image.
|
||||
fn load_into_image_sync(&self, image: >k::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
|
||||
None if use_fallback => image_ref.theme.load_icon_for_scale(
|
||||
FALLBACK_ICON_NAME,
|
||||
image_ref.size,
|
||||
scale,
|
||||
IconLookupFlags::empty(),
|
||||
),
|
||||
None => Ok(None),
|
||||
}?;
|
||||
|
||||
Self::create_and_load_surface(&pixbuf, image)
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// Attempts to create a Cairo surface from the provided `Pixbuf`,
|
||||
/// 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('/')
|
||||
}
|
||||
|
||||
pub fn icon_theme(&self) -> IconTheme {
|
||||
self.icon_theme
|
||||
.borrow()
|
||||
.clone()
|
||||
.expect("theme should be set at startup")
|
||||
}
|
||||
|
||||
/// 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.icon_theme.borrow_mut() = if theme.is_some() {
|
||||
let icon_theme = IconTheme::new();
|
||||
icon_theme.set_custom_theme(theme);
|
||||
Some(icon_theme)
|
||||
} else {
|
||||
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: >k::Image) -> Result<()> {
|
||||
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)
|
||||
}?;
|
||||
|
||||
|
@ -226,64 +351,3 @@ impl<'a> ImageProvider<'a> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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,
|
||||
)
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// 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"
|
||||
)))
|
||||
},
|
||||
)?;
|
||||
|
||||
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))
|
||||
} 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
28
src/main.rs
28
src/main.rs
|
@ -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,
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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(>k_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, >k_image)
|
||||
.await;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,20 +152,21 @@ 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 {
|
||||
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 {
|
||||
if let Some(icon) = icon_overrides.get(&id) {
|
||||
id = icon.clone();
|
||||
}
|
||||
|
||||
match ImageProvider::parse(&id, &icon_theme, true, self.icon_size)
|
||||
.map(|image| image.load_into_image(&icon))
|
||||
match image_provider
|
||||
.load_into_image(&id, self.icon_size, true, &icon)
|
||||
.await
|
||||
{
|
||||
Some(Ok(())) => icon.show(),
|
||||
Ok(true) => icon.show(),
|
||||
_ => icon.hide(),
|
||||
}
|
||||
}
|
||||
|
@ -179,6 +179,7 @@ impl Module<gtk::Box> for FocusedModule {
|
|||
icon.hide();
|
||||
label.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -231,7 +231,7 @@ impl Module<gtk::Box> for KeyboardModule {
|
|||
tracing::error!("{err:?}");
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -257,9 +257,15 @@ impl Module<gtk::Box> for KeyboardModule {
|
|||
) -> Result<ModuleParts<gtk::Box>> {
|
||||
let container = gtk::Box::new(self.layout.orientation(info), 0);
|
||||
|
||||
let caps = IconLabel::new(&self.icons.caps_off, info.icon_theme, self.icon_size);
|
||||
let num = IconLabel::new(&self.icons.num_off, info.icon_theme, self.icon_size);
|
||||
let scroll = IconLabel::new(&self.icons.scroll_off, info.icon_theme, self.icon_size);
|
||||
let image_provider = context.ironbar.image_provider();
|
||||
|
||||
let caps = IconLabel::new(&self.icons.caps_off, self.icon_size, &image_provider);
|
||||
let num = IconLabel::new(&self.icons.num_off, self.icon_size, &image_provider);
|
||||
let scroll = IconLabel::new(
|
||||
&self.icons.scroll_off,
|
||||
self.icon_size,
|
||||
&image_provider,
|
||||
);
|
||||
|
||||
caps.label().set_angle(self.layout.angle(info));
|
||||
caps.label().set_justify(self.layout.justify.into());
|
||||
|
@ -270,7 +276,7 @@ impl Module<gtk::Box> for KeyboardModule {
|
|||
scroll.label().set_angle(self.layout.angle(info));
|
||||
scroll.label().set_justify(self.layout.justify.into());
|
||||
|
||||
let layout_button = IconButton::new("", info.icon_theme, self.icon_size);
|
||||
let layout_button = IconButton::new("", self.icon_size, image_provider);
|
||||
|
||||
if self.show_caps {
|
||||
caps.add_class("key");
|
||||
|
|
|
@ -3,20 +3,18 @@ use crate::channels::AsyncSenderExt;
|
|||
use crate::clients::wayland::ToplevelInfo;
|
||||
use crate::config::{BarPosition, TruncateMode};
|
||||
use crate::gtk_helpers::{IronbarGtkExt, IronbarLabelExt};
|
||||
use crate::image::ImageProvider;
|
||||
use crate::modules::ModuleUpdateEvent;
|
||||
use crate::modules::launcher::{ItemEvent, LauncherUpdate};
|
||||
use crate::read_lock;
|
||||
use crate::{image, read_lock};
|
||||
use glib::Propagation;
|
||||
use gtk::gdk::{BUTTON_MIDDLE, BUTTON_PRIMARY};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Align, Button, IconTheme, Image, Justification, Label, Orientation};
|
||||
use gtk::{Align, Button, Image, Justification, Label, Orientation};
|
||||
use indexmap::IndexMap;
|
||||
use std::ops::Deref;
|
||||
use std::rc::Rc;
|
||||
use std::sync::RwLock;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Item {
|
||||
|
@ -166,7 +164,7 @@ impl ItemButton {
|
|||
pub fn new(
|
||||
item: &Item,
|
||||
appearance: AppearanceOptions,
|
||||
icon_theme: &IconTheme,
|
||||
image_provider: image::Provider,
|
||||
bar_position: BarPosition,
|
||||
tx: &Sender<ModuleUpdateEvent<LauncherUpdate>>,
|
||||
controller_tx: &Sender<ItemEvent>,
|
||||
|
@ -188,14 +186,13 @@ impl ItemButton {
|
|||
} else {
|
||||
item.app_id.clone()
|
||||
};
|
||||
let image = ImageProvider::parse(&input, icon_theme, true, appearance.icon_size);
|
||||
if let Some(image) = image {
|
||||
button.set_always_show_image(true);
|
||||
|
||||
if let Err(err) = image.load_into_image(&button.image) {
|
||||
error!("{err:?}");
|
||||
}
|
||||
};
|
||||
let button = button.clone();
|
||||
glib::spawn_future_local(async move {
|
||||
image_provider
|
||||
.load_into_image_silent(&input, appearance.icon_size, true, &button.image)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
button.add_class("item");
|
||||
|
|
|
@ -8,7 +8,6 @@ use super::{Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, Wid
|
|||
use crate::channels::{AsyncSenderExt, BroadcastReceiverExt};
|
||||
use crate::clients::wayland::{self, ToplevelEvent};
|
||||
use crate::config::{CommonConfig, EllipsizeMode, LayoutConfig, TruncateMode};
|
||||
use crate::desktop_file::find_desktop_file;
|
||||
use crate::gtk_helpers::{IronbarGtkExt, IronbarLabelExt};
|
||||
use crate::modules::launcher::item::ImageTextButton;
|
||||
use crate::modules::launcher::pagination::{IconContext, Pagination};
|
||||
|
@ -18,11 +17,10 @@ use gtk::prelude::*;
|
|||
use gtk::{Button, Orientation};
|
||||
use indexmap::IndexMap;
|
||||
use serde::Deserialize;
|
||||
use std::ops::Deref;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, error, trace};
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
|
@ -212,7 +210,7 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
|
||||
fn spawn_controller(
|
||||
&self,
|
||||
info: &ModuleInfo,
|
||||
_info: &ModuleInfo,
|
||||
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
mut rx: mpsc::Receiver<Self::ReceiveMessage>,
|
||||
) -> crate::Result<()> {
|
||||
|
@ -223,14 +221,9 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
favorites
|
||||
.iter()
|
||||
.map(|app_id| {
|
||||
let icon_override = info
|
||||
.icon_overrides
|
||||
.get(app_id)
|
||||
.map_or_else(String::new, ToString::to_string);
|
||||
|
||||
(
|
||||
app_id.to_string(),
|
||||
Item::new(app_id.to_string(), icon_override, OpenState::Closed, true),
|
||||
Item::new(app_id.to_string(), OpenState::Closed, true),
|
||||
)
|
||||
})
|
||||
.collect::<IndexMap<_, _>>()
|
||||
|
@ -239,8 +232,6 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
let items = arc_mut!(items);
|
||||
let items2 = Arc::clone(&items);
|
||||
|
||||
let icon_overrides = info.icon_overrides.clone();
|
||||
|
||||
let tx = context.tx.clone();
|
||||
let tx2 = context.tx.clone();
|
||||
|
||||
|
@ -258,12 +249,7 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
if let Some(item) = item {
|
||||
item.merge_toplevel(info.clone());
|
||||
} else {
|
||||
let mut item = Item::from(info.clone());
|
||||
|
||||
if let Some(icon) = icon_overrides.get(&info.app_id) {
|
||||
item.icon_override.clone_from(icon);
|
||||
}
|
||||
|
||||
let item = Item::from(info.clone());
|
||||
items.insert(info.app_id.clone(), item);
|
||||
}
|
||||
}
|
||||
|
@ -271,6 +257,7 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
{
|
||||
let items = {
|
||||
let items = lock!(items);
|
||||
|
||||
items
|
||||
.iter()
|
||||
.map(|(_, item)| item.clone())
|
||||
|
@ -296,12 +283,7 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
let item = items.get_mut(&info.app_id);
|
||||
match item {
|
||||
None => {
|
||||
let mut item: Item = info.into();
|
||||
|
||||
if let Some(icon) = icon_overrides.get(&app_id) {
|
||||
item.icon_override.clone_from(icon);
|
||||
}
|
||||
|
||||
let item: Item = info.into();
|
||||
items.insert(app_id.clone(), item.clone());
|
||||
|
||||
ItemOrWindow::Item(item)
|
||||
|
@ -378,7 +360,7 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
.await?;
|
||||
}
|
||||
None => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -389,17 +371,16 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
// listen to ui events
|
||||
let minimize_focused = self.minimize_focused;
|
||||
let wl = context.client::<wayland::Client>();
|
||||
|
||||
let desktop_files = context.ironbar.desktop_files();
|
||||
|
||||
spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
if let ItemEvent::OpenItem(app_id) = event {
|
||||
find_desktop_file(&app_id).map_or_else(
|
||||
|| error!("Could not find desktop file for {}", app_id),
|
||||
|file| {
|
||||
match desktop_files.find(&app_id).await {
|
||||
Ok(Some(file)) => {
|
||||
if let Err(err) = Command::new("gtk-launch")
|
||||
.arg(
|
||||
file.file_name()
|
||||
.expect("File segment missing from path to desktop file"),
|
||||
)
|
||||
.arg(file.file_name)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
|
@ -408,11 +389,13 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
"{:?}",
|
||||
Report::new(err)
|
||||
.wrap_err("Failed to run gtk-launch command.")
|
||||
.suggestion("Perhaps the desktop file is invalid?")
|
||||
.suggestion("Perhaps the applications file is invalid?")
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
Ok(None) => warn!("Could not find applications file for {}", app_id),
|
||||
Err(err) => error!("Failed to find parse file for {}: {}", app_id, err),
|
||||
}
|
||||
} else {
|
||||
tx.send_expect(ModuleUpdateEvent::ClosePopup).await;
|
||||
|
||||
|
@ -457,11 +440,11 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> crate::Result<ModuleParts<gtk::Box>> {
|
||||
let icon_theme = info.icon_theme;
|
||||
|
||||
let container = gtk::Box::new(self.layout.orientation(info), 0);
|
||||
let page_size = self.page_size;
|
||||
|
||||
let image_provider = context.ironbar.image_provider();
|
||||
|
||||
let pagination = Pagination::new(
|
||||
&container,
|
||||
self.page_size,
|
||||
|
@ -471,11 +454,11 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
fwd: &self.icons.page_forward,
|
||||
size: self.pagination_icon_size,
|
||||
},
|
||||
&image_provider,
|
||||
);
|
||||
|
||||
{
|
||||
let container = container.clone();
|
||||
let icon_theme = icon_theme.clone();
|
||||
|
||||
let controller_tx = context.controller_tx.clone();
|
||||
|
||||
|
@ -516,7 +499,7 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
let button = ItemButton::new(
|
||||
&item,
|
||||
appearance_options,
|
||||
&icon_theme,
|
||||
image_provider.clone(),
|
||||
bar_position,
|
||||
&tx,
|
||||
&controller_tx,
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
use crate::gtk_helpers::IronbarGtkExt;
|
||||
use crate::image;
|
||||
use crate::image::IconButton;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, IconTheme, Orientation};
|
||||
use gtk::{Button, Orientation};
|
||||
use std::cell::RefCell;
|
||||
use std::ops::Deref;
|
||||
use std::rc::Rc;
|
||||
|
@ -24,20 +25,21 @@ impl Pagination {
|
|||
container: >k::Box,
|
||||
page_size: usize,
|
||||
orientation: Orientation,
|
||||
icon_context: IconContext,
|
||||
icon_context: &IconContext,
|
||||
image_provider: &image::Provider,
|
||||
) -> Self {
|
||||
let scroll_box = gtk::Box::new(orientation, 0);
|
||||
|
||||
let scroll_back = IconButton::new(
|
||||
icon_context.icon_back,
|
||||
icon_context.icon_theme,
|
||||
icon_context.icon_size,
|
||||
icon_context.back,
|
||||
icon_context.size,
|
||||
image_provider.clone(),
|
||||
);
|
||||
|
||||
let scroll_fwd = IconButton::new(
|
||||
icon_context.icon_fwd,
|
||||
icon_context.icon_theme,
|
||||
icon_context.icon_size,
|
||||
icon_context.fwd,
|
||||
icon_context.size,
|
||||
image_provider.clone(),
|
||||
);
|
||||
|
||||
scroll_back.set_sensitive(false);
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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)
|
||||
}) {
|
||||
|
||||
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();
|
||||
image.load_into_image(&album_image)
|
||||
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);
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
|
||||
|
|
8
test-configs/applications/Factorio.desktop
Normal file
8
test-configs/applications/Factorio.desktop
Normal 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;
|
902
test-configs/applications/firefox.desktop
Normal file
902
test-configs/applications/firefox.desktop
Normal 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]=Web’de 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]=Duyệt web trên toàn thế giới
|
||||
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 duyệt 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 duyệt;Web;Duyệt 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 duyệt 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]=Cửa sổ mới
|
||||
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]=Cửa sổ riêng tư mới
|
||||
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 mba’ete ñ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 quản lý hồ sơ
|
||||
Name[wo]=Open Profile Manager
|
||||
Name[xh]=Open Profile Manager
|
||||
Name[zh_CN]=打开配置文件管理器
|
||||
Name[zh_TW]=開啟設定檔管理員
|
Loading…
Add table
Add a link
Reference in a new issue