mirror of
https://github.com/Zedfrigg/ironbar.git
synced 2025-08-16 22:31:03 +02:00
Merge pull request #1003 from JakeStanger/refactor/image
Overhaul `.desktop` and image resolver code
This commit is contained in:
commit
e99a04923d
40 changed files with 1891 additions and 682 deletions
20
.idea/runConfigurations/Test.xml
generated
Normal file
20
.idea/runConfigurations/Test.xml
generated
Normal file
|
@ -0,0 +1,20 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Test" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
||||
<option name="buildProfileId" value="test" />
|
||||
<option name="command" value="test" />
|
||||
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
|
||||
<envs />
|
||||
<option name="emulateTerminal" value="true" />
|
||||
<option name="channel" value="DEFAULT" />
|
||||
<option name="requiredFeatures" value="true" />
|
||||
<option name="allFeatures" value="false" />
|
||||
<option name="withSudo" value="false" />
|
||||
<option name="buildTarget" value="REMOTE" />
|
||||
<option name="backtrace" value="SHORT" />
|
||||
<option name="isRedirectInput" value="false" />
|
||||
<option name="redirectInputPath" value="" />
|
||||
<method v="2">
|
||||
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
|
@ -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"] }
|
||||
|
|
|
@ -280,11 +280,12 @@ Check [here](config) for an example config file for a fully configured bar in ea
|
|||
|
||||
The following table lists each of the top-level bar config options:
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|--------------------|-----------------------------------------|---------|-------------------------------------------------------------------------------|
|
||||
| `ironvar_defaults` | `Map<string, string>` | `{}` | Map of [ironvar](ironvars) keys against their default values. |
|
||||
| `monitors` | `Map<string, BarConfig or BarConfig[]>` | `null` | Map of monitor names against bar configs. |
|
||||
| `icon_overrides` | `Map<string, string>` | `{}` | Map of app IDs (or classes) to icon names, overriding the app's default icon. |
|
||||
| Name | Type | Default | Description |
|
||||
|--------------------|-----------------------------------------|---------|--------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `ironvar_defaults` | `Map<string, string>` | `{}` | Map of [ironvar](ironvars) keys against their default values. |
|
||||
| `monitors` | `Map<string, BarConfig or BarConfig[]>` | `null` | Map of monitor names against bar configs. |
|
||||
| `icon_theme` | `string` | `null` | Name of the GTK icon theme to use. Leave blank to use default. |
|
||||
| `icon_overrides` | `Map<string, string>` | `{}` | Map of image inputs to override names. Usually used for app IDs (or classes) to icon names, overriding the app's default icon. |
|
||||
|
||||
> [!TIP]
|
||||
> `monitors` is only required if you are following **2b** or **2c** (ie not the same bar across all monitors).
|
||||
|
@ -309,7 +310,6 @@ The following table lists each of the bar-level bar config options:
|
|||
| `layer` | `background` or `bottom` or `top` or `overlay` | `top` | The layer-shell layer to place the bar on. |
|
||||
| `exclusive_zone` | `boolean` | `true` unless `start_hidden` is enabled. | Whether the bar should reserve an exclusive zone around it. |
|
||||
| `popup_gap` | `integer` | `5` | The gap between the bar and popup window. |
|
||||
| `icon_theme` | `string` | `null` | Name of the GTK icon theme to use. Leave blank to use default. |
|
||||
| `start_hidden` | `boolean` | `false`, or `true` if `autohide` set | Whether the bar should be hidden when the application starts. Enabled by default when `autohide` is set. |
|
||||
| `autohide` | `integer` | `null` | The duration in milliseconds before the bar is hidden after the cursor leaves. Leave unset to disable auto-hide behaviour. |
|
||||
| `start` | `Module[]` | `[]` | Array of left or top modules. |
|
||||
|
|
26
src/bar.rs
26
src/bar.rs
|
@ -6,11 +6,9 @@ use color_eyre::Result;
|
|||
use glib::Propagation;
|
||||
use gtk::gdk::Monitor;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Application, ApplicationWindow, IconTheme, Orientation, Window, WindowType};
|
||||
use gtk::{Application, ApplicationWindow, Orientation, Window, WindowType};
|
||||
use gtk_layer_shell::LayerShell;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tracing::{debug, info};
|
||||
|
||||
|
@ -25,7 +23,6 @@ pub struct Bar {
|
|||
name: String,
|
||||
monitor_name: String,
|
||||
monitor_size: (i32, i32),
|
||||
icon_overrides: Arc<HashMap<String, String>>,
|
||||
position: BarPosition,
|
||||
|
||||
ironbar: Rc<Ironbar>,
|
||||
|
@ -46,7 +43,6 @@ impl Bar {
|
|||
app: &Application,
|
||||
monitor_name: String,
|
||||
monitor_size: (i32, i32),
|
||||
icon_overrides: Arc<HashMap<String, String>>,
|
||||
config: BarConfig,
|
||||
ironbar: Rc<Ironbar>,
|
||||
) -> Self {
|
||||
|
@ -96,7 +92,6 @@ impl Bar {
|
|||
name,
|
||||
monitor_name,
|
||||
monitor_size,
|
||||
icon_overrides,
|
||||
position,
|
||||
ironbar,
|
||||
window,
|
||||
|
@ -257,11 +252,6 @@ impl Bar {
|
|||
monitor: &Monitor,
|
||||
output_size: (i32, i32),
|
||||
) -> Result<BarLoadResult> {
|
||||
let icon_theme = IconTheme::new();
|
||||
if let Some(ref theme) = config.icon_theme {
|
||||
icon_theme.set_custom_theme(Some(theme));
|
||||
}
|
||||
|
||||
let app = &self.window.application().expect("to exist");
|
||||
|
||||
macro_rules! info {
|
||||
|
@ -272,15 +262,13 @@ impl Bar {
|
|||
monitor,
|
||||
output_name: &self.monitor_name,
|
||||
location: $location,
|
||||
icon_theme: &icon_theme,
|
||||
icon_overrides: self.icon_overrides.clone(),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// popup ignores module location so can bodge this for now
|
||||
let popup = Popup::new(
|
||||
self.ironbar.clone(),
|
||||
&self.ironbar,
|
||||
&info!(ModuleLocation::Left),
|
||||
output_size,
|
||||
config.popup_gap,
|
||||
|
@ -404,17 +392,9 @@ pub fn create_bar(
|
|||
monitor: &Monitor,
|
||||
monitor_name: String,
|
||||
monitor_size: (i32, i32),
|
||||
icon_overrides: Arc<HashMap<String, String>>,
|
||||
config: BarConfig,
|
||||
ironbar: Rc<Ironbar>,
|
||||
) -> Result<Bar> {
|
||||
let bar = Bar::new(
|
||||
app,
|
||||
monitor_name,
|
||||
monitor_size,
|
||||
icon_overrides,
|
||||
config,
|
||||
ironbar,
|
||||
);
|
||||
let bar = Bar::new(app, monitor_name, monitor_size, config, ironbar);
|
||||
bar.init(monitor)
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -140,7 +140,7 @@ pub async fn create_client() -> Result<Arc<Client>> {
|
|||
spawn(async move {
|
||||
if let Err(error) = client.run().await {
|
||||
error!("{}", error);
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(client)
|
||||
|
|
|
@ -157,7 +157,7 @@ impl Client {
|
|||
Event::Toplevel(event) => toplevel_tx.send_expect(event),
|
||||
#[cfg(feature = "clipboard")]
|
||||
Event::Clipboard(item) => clipboard_tx.send_expect(item),
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -25,13 +25,11 @@ pub struct LayoutConfig {
|
|||
impl LayoutConfig {
|
||||
pub fn orientation(&self, info: &ModuleInfo) -> gtk::Orientation {
|
||||
self.orientation
|
||||
.map(ModuleOrientation::into)
|
||||
.unwrap_or(info.bar_position.orientation())
|
||||
.map_or(info.bar_position.orientation(), ModuleOrientation::into)
|
||||
}
|
||||
|
||||
pub fn angle(&self, info: &ModuleInfo) -> f64 {
|
||||
self.orientation
|
||||
.map(ModuleOrientation::to_angle)
|
||||
.unwrap_or(info.bar_position.angle())
|
||||
.map_or(info.bar_position.angle(), ModuleOrientation::to_angle)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn load(file_path: &Path) -> Result<DesktopFile> {
|
||||
debug!("loading applications file: {}", file_path.display());
|
||||
|
||||
let file = tokio::fs::File::open(file_path).await?;
|
||||
|
||||
let mut desktop_file = DesktopFile::new(
|
||||
file_path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
let mut lines = BufReader::new(file).lines();
|
||||
|
||||
let mut has_name = false;
|
||||
let mut has_type = false;
|
||||
let mut has_wm_class = false;
|
||||
let mut has_exec = false;
|
||||
let mut has_icon = false;
|
||||
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
let Some((key, value)) = line.split_once('=') else {
|
||||
continue;
|
||||
};
|
||||
|
||||
match key {
|
||||
"Name" => {
|
||||
desktop_file.name = Some(value.to_string());
|
||||
has_name = true;
|
||||
}
|
||||
"Type" => {
|
||||
desktop_file.app_type = Some(value.to_string());
|
||||
has_type = true;
|
||||
}
|
||||
"StartupWMClass" => {
|
||||
desktop_file.startup_wm_class = Some(value.to_string());
|
||||
has_wm_class = true;
|
||||
}
|
||||
"Exec" => {
|
||||
desktop_file.exec = Some(value.to_string());
|
||||
has_exec = true;
|
||||
}
|
||||
"Icon" => {
|
||||
desktop_file.icon = Some(value.to_string());
|
||||
has_icon = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// parsing complete - don't bother with the rest of the lines
|
||||
if has_name && has_type && has_wm_class && has_exec && has_icon {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(desktop_file)
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds directories that should contain `.desktop` files
|
||||
/// and exist on the filesystem.
|
||||
fn find_application_dirs() -> Vec<PathBuf> {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DesktopFile {
|
||||
pub file_name: String,
|
||||
pub name: Option<String>,
|
||||
pub app_type: Option<String>,
|
||||
pub startup_wm_class: Option<String>,
|
||||
pub exec: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
}
|
||||
|
||||
impl DesktopFile {
|
||||
fn new(file_name: String) -> Self {
|
||||
Self {
|
||||
file_name,
|
||||
name: None,
|
||||
app_type: None,
|
||||
startup_wm_class: None,
|
||||
exec: None,
|
||||
icon: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type FileMap = HashMap<Box<str>, DesktopFileRef>;
|
||||
|
||||
/// Desktop file cache and resolver.
|
||||
///
|
||||
/// Files are lazy-loaded as required on resolution.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DesktopFiles {
|
||||
files: Arc<Mutex<FileMap>>,
|
||||
}
|
||||
|
||||
impl Default for DesktopFiles {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl DesktopFiles {
|
||||
/// Creates a new instance,
|
||||
/// scanning disk to generate a list of (unloaded) file refs in the process.
|
||||
pub fn new() -> Self {
|
||||
let desktop_files: FileMap = dirs()
|
||||
.iter()
|
||||
.flat_map(|path| files(path))
|
||||
.map(|file| {
|
||||
(
|
||||
file.file_stem()
|
||||
.unwrap_or_default()
|
||||
.to_str()
|
||||
.unwrap_or_default()
|
||||
.to_string()
|
||||
.into(),
|
||||
DesktopFileRef::Unloaded(file),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
debug!("resolved {} files", desktop_files.len());
|
||||
|
||||
Self {
|
||||
files: Arc::new(Mutex::new(desktop_files)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to locate a applications file by file name or contents.
|
||||
///
|
||||
/// Input should typically be the app id, app name or icon.
|
||||
pub async fn find(&self, input: &str) -> Result<Option<DesktopFile>> {
|
||||
let mut res = self.find_by_file_name(input).await?;
|
||||
if res.is_none() {
|
||||
res = self.find_by_file_contents(input).await?;
|
||||
}
|
||||
|
||||
debug!("found match for app_id {input}: {}", res.is_some());
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Checks file names for an exact or partial match of the provided input.
|
||||
async fn find_by_file_name(&self, input: &str) -> Result<Option<DesktopFile>> {
|
||||
let mut files = self.files.lock().await;
|
||||
|
||||
let mut file_ref = files
|
||||
.iter_mut()
|
||||
.find(|&(name, _)| name.eq_ignore_ascii_case(input));
|
||||
|
||||
if file_ref.is_none() {
|
||||
file_ref = files.iter_mut().find(
|
||||
|&(name, _)| // this will attempt to find flatpak apps that are in the format
|
||||
// `com.company.app` or `com.app.something`
|
||||
input
|
||||
.split(&[' ', ':', '@', '.', '_'][..])
|
||||
.any(|part| name.eq_ignore_ascii_case(part)),
|
||||
);
|
||||
}
|
||||
|
||||
let file_ref = file_ref.map(|(_, file)| file);
|
||||
|
||||
if let Some(file_ref) = file_ref {
|
||||
let file = file_ref.get().await?;
|
||||
Ok(Some(file))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks file contents for an exact or partial match of the provided input.
|
||||
async fn find_by_file_contents(&self, app_id: &str) -> Result<Option<DesktopFile>> {
|
||||
let mut files = self.files.lock().await;
|
||||
|
||||
// first pass - check name for exact match
|
||||
for (_, file_ref) in files.iter_mut() {
|
||||
let file = file_ref.get().await?;
|
||||
if let Some(name) = &file.name {
|
||||
if name.eq_ignore_ascii_case(app_id) {
|
||||
return Ok(Some(file));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// second pass - check name for partial match
|
||||
for (_, file_ref) in files.iter_mut() {
|
||||
let file = file_ref.get().await?;
|
||||
if let Some(name) = &file.name {
|
||||
if name.to_lowercase().contains(app_id) {
|
||||
return Ok(Some(file));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// third pass - check remaining fields for partial match
|
||||
for (_, file_ref) in files.iter_mut() {
|
||||
let file = file_ref.get().await?;
|
||||
|
||||
if let Some(name) = &file.exec {
|
||||
if name.to_lowercase().contains(app_id) {
|
||||
return Ok(Some(file));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(name) = &file.startup_wm_class {
|
||||
if name.to_lowercase().contains(app_id) {
|
||||
return Ok(Some(file));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(name) = &file.icon {
|
||||
if name.to_lowercase().contains(app_id) {
|
||||
return Ok(Some(file));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a list of paths to all directories
|
||||
/// containing `.applications` files.
|
||||
fn dirs() -> Vec<PathBuf> {
|
||||
let mut dirs = vec![
|
||||
PathBuf::from("/usr/share/applications"), // system installed apps
|
||||
PathBuf::from("/var/lib/flatpak/exports/share/applications"), // flatpak apps
|
||||
];
|
||||
|
||||
let xdg_dirs = env::var_os("XDG_DATA_DIRS");
|
||||
if let Some(xdg_dirs) = xdg_dirs {
|
||||
let xdg_dirs = env::var("XDG_DATA_DIRS");
|
||||
if let Ok(xdg_dirs) = xdg_dirs {
|
||||
for mut xdg_dir in env::split_paths(&xdg_dirs) {
|
||||
xdg_dir.push("applications");
|
||||
dirs.push(xdg_dir);
|
||||
|
@ -43,157 +274,66 @@ fn find_application_dirs() -> Vec<PathBuf> {
|
|||
dirs.push(user_dir);
|
||||
}
|
||||
|
||||
dirs.into_iter().filter(|dir| dir.exists()).collect()
|
||||
dirs.into_iter().filter(|dir| dir.exists()).rev().collect()
|
||||
}
|
||||
|
||||
/// Finds all the desktop files
|
||||
fn find_desktop_files() -> Vec<PathBuf> {
|
||||
let dirs = find_application_dirs();
|
||||
dirs.into_iter()
|
||||
.flat_map(|dir| {
|
||||
WalkDir::new(dir)
|
||||
.max_depth(5)
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
.map(DirEntry::into_path)
|
||||
.filter(|file| file.is_file() && file.extension().unwrap_or_default() == "desktop")
|
||||
})
|
||||
/// Gets a list of all `.applications` files in the provided directory.
|
||||
///
|
||||
/// The directory is recursed to a maximum depth of 5.
|
||||
fn files(dir: &Path) -> Vec<PathBuf> {
|
||||
WalkDir::new(dir)
|
||||
.max_depth(5)
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
.map(DirEntry::into_path)
|
||||
.filter(|file| file.is_file() && file.extension().unwrap_or_default() == "desktop")
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Attempts to locate a `.desktop` file for an app id
|
||||
pub fn find_desktop_file(app_id: &str) -> Option<PathBuf> {
|
||||
// this is necessary to invalidate the cache
|
||||
let files = find_desktop_files();
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
find_desktop_file_by_filename(app_id, &files)
|
||||
.or_else(|| find_desktop_file_by_filedata(app_id, &files))
|
||||
}
|
||||
|
||||
/// Finds the correct desktop file using a simple condition check
|
||||
fn find_desktop_file_by_filename(app_id: &str, files: &[PathBuf]) -> Option<PathBuf> {
|
||||
let with_names = files
|
||||
.iter()
|
||||
.map(|f| {
|
||||
(
|
||||
f,
|
||||
f.file_stem()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_lowercase(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
with_names
|
||||
.iter()
|
||||
// first pass - check for exact match
|
||||
.find(|(_, name)| name.eq_ignore_ascii_case(app_id))
|
||||
// second pass - check for substring
|
||||
.or_else(|| {
|
||||
with_names.iter().find(|(_, name)| {
|
||||
// this will attempt to find flatpak apps that are in the format
|
||||
// `com.company.app` or `com.app.something`
|
||||
app_id
|
||||
.split(&[' ', ':', '@', '.', '_'][..])
|
||||
.any(|part| name.eq_ignore_ascii_case(part))
|
||||
})
|
||||
})
|
||||
.map(|(file, _)| file.into())
|
||||
}
|
||||
|
||||
/// Finds the correct desktop file using the keys in `DESKTOP_FILES_LOOK_OUT_KEYS`
|
||||
fn find_desktop_file_by_filedata(app_id: &str, files: &[PathBuf]) -> Option<PathBuf> {
|
||||
let app_id = &app_id.to_lowercase();
|
||||
let mut desktop_files_cache = lock!(desktop_files());
|
||||
|
||||
let files = files
|
||||
.iter()
|
||||
.filter_map(|file| {
|
||||
let parsed_desktop_file = parse_desktop_file(file)?;
|
||||
|
||||
desktop_files_cache.insert(file.clone(), parsed_desktop_file.clone());
|
||||
Some((file.clone(), parsed_desktop_file))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let file = files
|
||||
.iter()
|
||||
// first pass - check name key for exact match
|
||||
.find(|(_, desktop_file)| {
|
||||
desktop_file
|
||||
.get("Name")
|
||||
.is_some_and(|names| names.iter().any(|name| name.eq_ignore_ascii_case(app_id)))
|
||||
})
|
||||
// second pass - check name key for substring
|
||||
.or_else(|| {
|
||||
files.iter().find(|(_, desktop_file)| {
|
||||
desktop_file.get("Name").is_some_and(|names| {
|
||||
names
|
||||
.iter()
|
||||
.any(|name| name.to_lowercase().contains(app_id))
|
||||
})
|
||||
})
|
||||
})
|
||||
// third pass - check all keys for substring
|
||||
.or_else(|| {
|
||||
files.iter().find(|(_, desktop_file)| {
|
||||
desktop_file
|
||||
.values()
|
||||
.flatten()
|
||||
.any(|value| value.to_lowercase().contains(app_id))
|
||||
})
|
||||
});
|
||||
|
||||
file.map(|(path, _)| path).cloned()
|
||||
}
|
||||
|
||||
/// Parses a desktop file into a hashmap of keys/vector(values).
|
||||
fn parse_desktop_file(path: &Path) -> Option<DesktopFile> {
|
||||
let Ok(file) = fs::read_to_string(path) else {
|
||||
warn!("Couldn't Open File: {}", path.display());
|
||||
return None;
|
||||
};
|
||||
|
||||
let mut desktop_file: DesktopFile = DesktopFile::new();
|
||||
|
||||
file.lines()
|
||||
.filter_map(|line| {
|
||||
let (key, value) = line.split_once('=')?;
|
||||
|
||||
let key = key.trim();
|
||||
let value = value.trim();
|
||||
|
||||
if desktop_files_look_out_keys().contains(key) {
|
||||
Some((key, value))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.for_each(|(key, value)| {
|
||||
desktop_file
|
||||
.entry(key.to_string())
|
||||
.or_default()
|
||||
.push(value.to_string());
|
||||
});
|
||||
|
||||
Some(desktop_file)
|
||||
}
|
||||
|
||||
/// Attempts to get the icon name from the app's `.desktop` file.
|
||||
pub fn get_desktop_icon_name(app_id: &str) -> Option<String> {
|
||||
let path = find_desktop_file(app_id)?;
|
||||
|
||||
let mut desktop_files_cache = lock!(desktop_files());
|
||||
|
||||
let desktop_file = match desktop_files_cache.get(&path) {
|
||||
Some(desktop_file) => desktop_file,
|
||||
_ => desktop_files_cache
|
||||
.entry(path.clone())
|
||||
.or_insert_with(|| parse_desktop_file(&path).expect("desktop_file")),
|
||||
};
|
||||
|
||||
let mut icons = desktop_file.get("Icon").into_iter().flatten();
|
||||
|
||||
icons.next().map(std::string::ToString::to_string)
|
||||
fn setup() {
|
||||
unsafe {
|
||||
let pwd = env::current_dir().unwrap();
|
||||
env::set_var("XDG_DATA_DIRS", format!("{}/test-configs", pwd.display()));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_by_filename() {
|
||||
setup();
|
||||
|
||||
let desktop_files = DesktopFiles::new();
|
||||
let file = desktop_files.find_by_file_name("firefox").await.unwrap();
|
||||
|
||||
assert!(file.is_some());
|
||||
assert_eq!(file.unwrap().file_name, "firefox.desktop");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_by_file_contents() {
|
||||
setup();
|
||||
|
||||
let desktop_files = DesktopFiles::new();
|
||||
|
||||
let file = desktop_files.find_by_file_contents("427520").await.unwrap();
|
||||
|
||||
assert!(file.is_some());
|
||||
assert_eq!(file.unwrap().file_name, "Factorio.desktop");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn parser() {
|
||||
let mut file_ref =
|
||||
DesktopFileRef::Unloaded(PathBuf::from("test-configs/applications/firefox.desktop"));
|
||||
let file = file_ref.get().await.unwrap();
|
||||
|
||||
assert_eq!(file.name, Some("Firefox".to_string()));
|
||||
assert_eq!(file.icon, Some("firefox".to_string()));
|
||||
assert_eq!(file.exec, Some("/usr/lib/firefox/firefox %u".to_string()));
|
||||
assert_eq!(file.startup_wm_class, Some("firefox".to_string()));
|
||||
assert_eq!(file.app_type, Some("Application".to_string()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use super::ImageProvider;
|
||||
use crate::gtk_helpers::{IronbarGtkExt, IronbarLabelExt};
|
||||
use crate::image;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, IconTheme, Image, Label, Orientation};
|
||||
use gtk::{Button, Image, Label, Orientation};
|
||||
use std::ops::Deref;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -29,27 +29,33 @@ pub struct IconButton {
|
|||
feature = "workspaces",
|
||||
))]
|
||||
impl IconButton {
|
||||
pub fn new(input: &str, icon_theme: &IconTheme, size: i32) -> Self {
|
||||
pub fn new(input: &str, size: i32, image_provider: image::Provider) -> Self {
|
||||
let button = Button::new();
|
||||
let image = Image::new();
|
||||
let label = Label::new(Some(input));
|
||||
|
||||
if ImageProvider::is_definitely_image_input(input) {
|
||||
if image::Provider::is_explicit_input(input) {
|
||||
image.add_class("image");
|
||||
image.add_class("icon");
|
||||
|
||||
match ImageProvider::parse(input, icon_theme, false, size)
|
||||
.map(|provider| provider.load_into_image(&image))
|
||||
{
|
||||
Some(_) => {
|
||||
let image = image.clone();
|
||||
let label = label.clone();
|
||||
let button = button.clone();
|
||||
|
||||
let input = input.to_string(); // ew
|
||||
|
||||
glib::spawn_future_local(async move {
|
||||
if let Ok(true) = image_provider
|
||||
.load_into_image(&input, size, false, &image)
|
||||
.await
|
||||
{
|
||||
button.set_image(Some(&image));
|
||||
button.set_always_show_image(true);
|
||||
}
|
||||
None => {
|
||||
} else {
|
||||
button.set_child(Some(&label));
|
||||
label.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
button.set_child(Some(&label));
|
||||
label.show();
|
||||
|
@ -82,17 +88,17 @@ impl Deref for IconButton {
|
|||
|
||||
#[cfg(any(feature = "keyboard", feature = "music", feature = "workspaces"))]
|
||||
pub struct IconLabel {
|
||||
provider: image::Provider,
|
||||
container: gtk::Box,
|
||||
label: Label,
|
||||
image: Image,
|
||||
|
||||
icon_theme: IconTheme,
|
||||
size: i32,
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "keyboard", feature = "music", feature = "workspaces"))]
|
||||
impl IconLabel {
|
||||
pub fn new(input: &str, icon_theme: &IconTheme, size: i32) -> Self {
|
||||
pub fn new(input: &str, size: i32, image_provider: &image::Provider) -> Self {
|
||||
let container = gtk::Box::new(Orientation::Horizontal, 0);
|
||||
|
||||
let label = Label::builder().use_markup(true).build();
|
||||
|
@ -106,21 +112,34 @@ impl IconLabel {
|
|||
container.add(&image);
|
||||
container.add(&label);
|
||||
|
||||
if ImageProvider::is_definitely_image_input(input) {
|
||||
ImageProvider::parse(input, icon_theme, false, size)
|
||||
.map(|provider| provider.load_into_image(&image));
|
||||
if image::Provider::is_explicit_input(input) {
|
||||
let image = image.clone();
|
||||
let label = label.clone();
|
||||
let image_provider = image_provider.clone();
|
||||
|
||||
image.show();
|
||||
let input = input.to_string();
|
||||
|
||||
glib::spawn_future_local(async move {
|
||||
let res = image_provider
|
||||
.load_into_image(&input, size, false, &image)
|
||||
.await;
|
||||
if matches!(res, Ok(true)) {
|
||||
image.show();
|
||||
} else {
|
||||
label.set_text(&input);
|
||||
label.show();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
label.set_text(input);
|
||||
label.show();
|
||||
}
|
||||
|
||||
Self {
|
||||
provider: image_provider.clone(),
|
||||
container,
|
||||
label,
|
||||
image,
|
||||
icon_theme: icon_theme.clone(),
|
||||
size,
|
||||
}
|
||||
}
|
||||
|
@ -130,12 +149,26 @@ impl IconLabel {
|
|||
let image = &self.image;
|
||||
|
||||
if let Some(input) = input {
|
||||
if ImageProvider::is_definitely_image_input(input) {
|
||||
ImageProvider::parse(input, &self.icon_theme, false, self.size)
|
||||
.map(|provider| provider.load_into_image(image));
|
||||
if image::Provider::is_explicit_input(input) {
|
||||
let provider = self.provider.clone();
|
||||
let size = self.size;
|
||||
|
||||
label.hide();
|
||||
image.show();
|
||||
let label = label.clone();
|
||||
let image = image.clone();
|
||||
let input = input.to_string();
|
||||
|
||||
glib::spawn_future_local(async move {
|
||||
let res = provider.load_into_image(&input, size, false, &image).await;
|
||||
if matches!(res, Ok(true)) {
|
||||
label.hide();
|
||||
image.show();
|
||||
} else {
|
||||
label.set_label_escaped(&input);
|
||||
|
||||
image.hide();
|
||||
label.show();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
label.set_label_escaped(input);
|
||||
|
||||
|
|
|
@ -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,195 @@
|
|||
use crate::channels::{AsyncSenderExt, MpscReceiverExt};
|
||||
use crate::desktop_file::get_desktop_icon_name;
|
||||
#[cfg(feature = "http")]
|
||||
use crate::spawn;
|
||||
use cfg_if::cfg_if;
|
||||
use crate::desktop_file::DesktopFiles;
|
||||
use crate::{arc_mut, lock};
|
||||
use color_eyre::{Help, Report, Result};
|
||||
use gtk::cairo::Surface;
|
||||
use gtk::gdk::ffi::gdk_cairo_surface_create_from_pixbuf;
|
||||
use gtk::gdk_pixbuf::Pixbuf;
|
||||
use gtk::gio::{Cancellable, MemoryInputStream};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{IconLookupFlags, IconTheme};
|
||||
use std::path::{Path, PathBuf};
|
||||
#[cfg(feature = "http")]
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, warn};
|
||||
use gtk::{IconLookupFlags, IconTheme, Image};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tracing::{debug, trace, warn};
|
||||
|
||||
cfg_if!(
|
||||
if #[cfg(feature = "http")] {
|
||||
use gtk::gio::{Cancellable, MemoryInputStream};
|
||||
use tracing::error;
|
||||
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
|
||||
struct ImageRef {
|
||||
size: i32,
|
||||
location: Option<ImageLocation>,
|
||||
theme: IconTheme,
|
||||
}
|
||||
|
||||
impl ImageRef {
|
||||
fn new(size: i32, location: Option<ImageLocation>, theme: IconTheme) -> Self {
|
||||
Self {
|
||||
size,
|
||||
location,
|
||||
theme,
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ImageLocation<'a> {
|
||||
Icon {
|
||||
name: String,
|
||||
theme: &'a IconTheme,
|
||||
},
|
||||
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
|
||||
enum ImageLocation {
|
||||
Icon(String),
|
||||
Local(PathBuf),
|
||||
Steam(String),
|
||||
#[cfg(feature = "http")]
|
||||
Remote(reqwest::Url),
|
||||
}
|
||||
|
||||
pub struct ImageProvider<'a> {
|
||||
location: ImageLocation<'a>,
|
||||
size: i32,
|
||||
#[derive(Debug)]
|
||||
struct Cache {
|
||||
location_cache: HashMap<(Box<str>, i32), ImageRef>,
|
||||
pixbuf_cache: HashMap<ImageRef, Option<Pixbuf>>,
|
||||
}
|
||||
|
||||
impl<'a> ImageProvider<'a> {
|
||||
/// Attempts to parse the image input to find its location.
|
||||
/// Errors if no valid location type can be found.
|
||||
impl Cache {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
location_cache: HashMap::new(),
|
||||
pixbuf_cache: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Provider {
|
||||
desktop_files: DesktopFiles,
|
||||
icon_theme: RefCell<Option<IconTheme>>,
|
||||
overrides: HashMap<String, String>,
|
||||
cache: Arc<Mutex<Cache>>,
|
||||
}
|
||||
|
||||
impl Provider {
|
||||
pub fn new(desktop_files: DesktopFiles, overrides: &mut HashMap<String, String>) -> Self {
|
||||
let mut overrides_map = HashMap::with_capacity(overrides.len());
|
||||
overrides_map.extend(overrides.drain());
|
||||
|
||||
Self {
|
||||
desktop_files,
|
||||
icon_theme: RefCell::new(None),
|
||||
overrides: overrides_map,
|
||||
cache: arc_mut!(Cache::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to resolve the provided input into a `Pixbuf`,
|
||||
/// and load that `Pixbuf` into the provided `Image` widget.
|
||||
///
|
||||
/// Note this checks that icons exist in theme, or files exist on disk
|
||||
/// but no other check is performed.
|
||||
pub fn parse(input: &str, theme: &'a IconTheme, use_fallback: bool, size: i32) -> Option<Self> {
|
||||
let location = Self::get_location(input, theme, size, use_fallback, 0)?;
|
||||
debug!("Resolved {input} --> {location:?} (size: {size})");
|
||||
|
||||
Some(Self { location, size })
|
||||
}
|
||||
|
||||
/// Returns true if the input starts with a prefix
|
||||
/// that is supported by the parser
|
||||
/// (ie the parser would not fallback to checking the input).
|
||||
pub fn is_definitely_image_input(input: &str) -> bool {
|
||||
input.starts_with("icon:")
|
||||
|| input.starts_with("file://")
|
||||
|| input.starts_with("http://")
|
||||
|| input.starts_with("https://")
|
||||
|| input.starts_with('/')
|
||||
}
|
||||
|
||||
fn get_location(
|
||||
/// If `use_fallback` is `true`, a fallback icon will be used
|
||||
/// where an image cannot be found.
|
||||
///
|
||||
/// Returns `true` if the image was successfully loaded,
|
||||
/// or `false` if the image could not be found.
|
||||
/// May also return an error if the resolution or loading process failed.
|
||||
pub async fn load_into_image(
|
||||
&self,
|
||||
input: &str,
|
||||
theme: &'a IconTheme,
|
||||
size: i32,
|
||||
use_fallback: bool,
|
||||
recurse_depth: usize,
|
||||
) -> Option<ImageLocation<'a>> {
|
||||
macro_rules! fallback {
|
||||
() => {
|
||||
if use_fallback {
|
||||
Some(Self::get_fallback_icon(theme))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
image: &Image,
|
||||
) -> Result<bool> {
|
||||
let image_ref = self.get_ref(input, size).await?;
|
||||
debug!("image ref for {input}: {:?}", image_ref);
|
||||
|
||||
let pixbuf = if let Some(pixbuf) = lock!(self.cache).pixbuf_cache.get(&image_ref) {
|
||||
pixbuf.clone()
|
||||
} else {
|
||||
let pixbuf = Self::get_pixbuf(&image_ref, image.scale_factor(), use_fallback).await?;
|
||||
|
||||
lock!(self.cache)
|
||||
.pixbuf_cache
|
||||
.insert(image_ref, pixbuf.clone());
|
||||
|
||||
pixbuf
|
||||
};
|
||||
|
||||
if let Some(ref pixbuf) = pixbuf {
|
||||
create_and_load_surface(pixbuf, image)?;
|
||||
}
|
||||
|
||||
const MAX_RECURSE_DEPTH: usize = 2;
|
||||
Ok(pixbuf.is_some())
|
||||
}
|
||||
|
||||
let should_parse_desktop_file = !Self::is_definitely_image_input(input);
|
||||
/// Like [`Provider::load_into_image`], but does not return an error if the image could not be found.
|
||||
///
|
||||
/// If an image is not resolved, a warning is logged. Errors are also logged.
|
||||
pub async fn load_into_image_silent(
|
||||
&self,
|
||||
input: &str,
|
||||
size: i32,
|
||||
use_fallback: bool,
|
||||
image: &Image,
|
||||
) {
|
||||
match self.load_into_image(input, size, use_fallback, image).await {
|
||||
Ok(true) => {}
|
||||
Ok(false) => warn!("failed to resolve image: {input}"),
|
||||
Err(e) => warn!("failed to load image: {input}: {e:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the `ImageRef` for the provided input.
|
||||
///
|
||||
/// This contains the location of the image if it can be resolved.
|
||||
/// The ref will be loaded from cache if present.
|
||||
async fn get_ref(&self, input: &str, size: i32) -> Result<ImageRef> {
|
||||
let key = (input.into(), size);
|
||||
|
||||
if let Some(location) = lock!(self.cache).location_cache.get(&key) {
|
||||
Ok(location.clone())
|
||||
} else {
|
||||
let location = self.resolve_location(input, size, 0).await?;
|
||||
let image_ref = ImageRef::new(size, location, self.icon_theme());
|
||||
|
||||
lock!(self.cache)
|
||||
.location_cache
|
||||
.insert(key, image_ref.clone());
|
||||
Ok(image_ref)
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to resolve the provided input into an `ImageLocation`.
|
||||
///
|
||||
/// This will resolve all of:
|
||||
/// - The current icon theme
|
||||
/// - The file on disk
|
||||
/// - Steam icons
|
||||
/// - Desktop files (`Icon` keys)
|
||||
/// - HTTP(S) URLs
|
||||
async fn resolve_location(
|
||||
&self,
|
||||
input: &str,
|
||||
size: i32,
|
||||
recurse_depth: u8,
|
||||
) -> Result<Option<ImageLocation>> {
|
||||
const MAX_RECURSE_DEPTH: u8 = 2;
|
||||
|
||||
let input = self.overrides.get(input).map_or(input, String::as_str);
|
||||
|
||||
let should_parse_desktop_file = !Self::is_explicit_input(input);
|
||||
|
||||
let (input_type, input_name) = input
|
||||
.split_once(':')
|
||||
.map_or((None, input), |(t, n)| (Some(t), n));
|
||||
|
||||
match input_type {
|
||||
Some(input_type) if input_type == "icon" => Some(ImageLocation::Icon {
|
||||
name: input_name.to_string(),
|
||||
theme,
|
||||
}),
|
||||
Some(input_type) if input_type == "file" => Some(ImageLocation::Local(PathBuf::from(
|
||||
let location = match input_type {
|
||||
Some(_t @ "icon") => Some(ImageLocation::Icon(input.to_string())),
|
||||
Some(_t @ "file") => Some(ImageLocation::Local(PathBuf::from(
|
||||
input_name[2..].to_string(),
|
||||
))),
|
||||
#[cfg(feature = "http")]
|
||||
Some(input_type) if input_type == "http" || input_type == "https" => {
|
||||
input.parse().ok().map(ImageLocation::Remote)
|
||||
}
|
||||
Some(_t @ ("http" | "https")) => input.parse().ok().map(ImageLocation::Remote),
|
||||
None if input.starts_with("steam_app_") => Some(ImageLocation::Steam(
|
||||
input_name.chars().skip("steam_app_".len()).collect(),
|
||||
)),
|
||||
None if theme
|
||||
None if self
|
||||
.icon_theme()
|
||||
.lookup_icon(input, size, IconLookupFlags::empty())
|
||||
.is_some() =>
|
||||
{
|
||||
Some(ImageLocation::Icon {
|
||||
name: input_name.to_string(),
|
||||
theme,
|
||||
})
|
||||
Some(ImageLocation::Icon(input.to_string()))
|
||||
}
|
||||
Some(input_type) => {
|
||||
warn!(
|
||||
|
@ -117,173 +197,154 @@ impl<'a> ImageProvider<'a> {
|
|||
Report::msg(format!("Unsupported image type: {input_type}"))
|
||||
.note("You may need to recompile with support if available")
|
||||
);
|
||||
fallback!()
|
||||
None
|
||||
}
|
||||
None if PathBuf::from(input_name).is_file() => {
|
||||
Some(ImageLocation::Local(PathBuf::from(input_name)))
|
||||
}
|
||||
None if recurse_depth == MAX_RECURSE_DEPTH => fallback!(),
|
||||
None if recurse_depth == MAX_RECURSE_DEPTH => None,
|
||||
None if should_parse_desktop_file => {
|
||||
if let Some(location) = get_desktop_icon_name(input_name).map(|input| {
|
||||
Self::get_location(&input, theme, size, use_fallback, recurse_depth + 1)
|
||||
}) {
|
||||
location
|
||||
} else {
|
||||
warn!("Failed to find image: {input}");
|
||||
fallback!()
|
||||
}
|
||||
}
|
||||
None => {
|
||||
warn!("Failed to find image: {input}");
|
||||
fallback!()
|
||||
}
|
||||
}
|
||||
}
|
||||
let location = self
|
||||
.desktop_files
|
||||
.find(input_name)
|
||||
.await?
|
||||
.and_then(|input| input.icon);
|
||||
|
||||
/// Attempts to fetch the image from the location
|
||||
/// and load it into the provided `GTK::Image` widget.
|
||||
pub fn load_into_image(&self, image: >k::Image) -> Result<()> {
|
||||
// handle remote locations async to avoid blocking UI thread while downloading
|
||||
#[cfg(feature = "http")]
|
||||
if let ImageLocation::Remote(url) = &self.location {
|
||||
let url = url.clone();
|
||||
let (tx, rx) = mpsc::channel(64);
|
||||
|
||||
spawn(async move {
|
||||
let bytes = Self::get_bytes_from_http(url).await;
|
||||
if let Ok(bytes) = bytes {
|
||||
tx.send_expect(bytes).await;
|
||||
}
|
||||
});
|
||||
|
||||
{
|
||||
let size = self.size;
|
||||
let image = image.clone();
|
||||
rx.recv_glib(move |bytes| {
|
||||
let stream = MemoryInputStream::from_bytes(&bytes);
|
||||
|
||||
let scale = image.scale_factor();
|
||||
let scaled_size = size * scale;
|
||||
|
||||
let pixbuf = Pixbuf::from_stream_at_scale(
|
||||
&stream,
|
||||
scaled_size,
|
||||
scaled_size,
|
||||
true,
|
||||
Some(&Cancellable::new()),
|
||||
);
|
||||
|
||||
// Different error types makes this a bit awkward
|
||||
match pixbuf.map(|pixbuf| Self::create_and_load_surface(&pixbuf, &image)) {
|
||||
Ok(Err(err)) => error!("{err:?}"),
|
||||
Err(err) => error!("{err:?}"),
|
||||
_ => {}
|
||||
if let Some(location) = location {
|
||||
if location == input {
|
||||
None
|
||||
} else {
|
||||
Box::pin(self.resolve_location(&location, size, recurse_depth + 1)).await?
|
||||
}
|
||||
});
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.load_into_image_sync(image)?;
|
||||
None => None,
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "http"))]
|
||||
self.load_into_image_sync(image)?;
|
||||
|
||||
Ok(())
|
||||
Ok(location)
|
||||
}
|
||||
|
||||
/// Attempts to synchronously fetch an image from location
|
||||
/// and load into into the image.
|
||||
fn load_into_image_sync(&self, image: >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
|
||||
}?;
|
||||
|
||||
Self::create_and_load_surface(&pixbuf, image)
|
||||
}
|
||||
|
||||
/// Attempts to create a Cairo surface from the provided `Pixbuf`,
|
||||
/// using the provided scaling factor.
|
||||
/// The surface is then loaded into the provided image.
|
||||
/// Attempts to load the provided `ImageRef` into a `Pixbuf`.
|
||||
///
|
||||
/// This is necessary for HiDPI since `Pixbuf`s are always treated as scale factor 1.
|
||||
pub fn create_and_load_surface(pixbuf: &Pixbuf, image: >k::Image) -> Result<()> {
|
||||
let surface = unsafe {
|
||||
let ptr = gdk_cairo_surface_create_from_pixbuf(
|
||||
pixbuf.as_ptr(),
|
||||
image.scale_factor(),
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
Surface::from_raw_full(ptr)
|
||||
/// If `use_fallback` is `true`, a fallback icon will be used
|
||||
/// where an image cannot be found.
|
||||
async fn get_pixbuf(
|
||||
image_ref: &ImageRef,
|
||||
scale: i32,
|
||||
use_fallback: bool,
|
||||
) -> Result<Option<Pixbuf>> {
|
||||
const FALLBACK_ICON_NAME: &str = "dialog-question-symbolic";
|
||||
|
||||
let buf = match &image_ref.location {
|
||||
Some(ImageLocation::Icon(name)) => image_ref.theme.load_icon_for_scale(
|
||||
name,
|
||||
image_ref.size,
|
||||
scale,
|
||||
IconLookupFlags::FORCE_SIZE,
|
||||
),
|
||||
Some(ImageLocation::Local(path)) => {
|
||||
let scaled_size = image_ref.size * scale;
|
||||
Pixbuf::from_file_at_scale(path, scaled_size, scaled_size, true).map(Some)
|
||||
}
|
||||
Some(ImageLocation::Steam(app_id)) => {
|
||||
let path = dirs::data_dir().map_or_else(
|
||||
|| Err(Report::msg("Missing XDG data dir")),
|
||||
|dir| Ok(dir.join(format!("icons/hicolor/32x32/apps/steam_icon_{app_id}.png"))),
|
||||
)?;
|
||||
|
||||
let scaled_size = image_ref.size * scale;
|
||||
Pixbuf::from_file_at_scale(path, scaled_size, scaled_size, true).map(Some)
|
||||
}
|
||||
#[cfg(feature = "http")]
|
||||
Some(ImageLocation::Remote(uri)) => {
|
||||
let res = reqwest::get(uri.clone()).await?;
|
||||
|
||||
let status = res.status();
|
||||
let bytes = if status.is_success() {
|
||||
let bytes = res.bytes().await?;
|
||||
Ok(glib::Bytes::from_owned(bytes))
|
||||
} else {
|
||||
Err(Report::msg(format!(
|
||||
"Received non-success HTTP code ({status})"
|
||||
)))
|
||||
}?;
|
||||
|
||||
let stream = MemoryInputStream::from_bytes(&bytes);
|
||||
let scaled_size = image_ref.size * scale;
|
||||
|
||||
Pixbuf::from_stream_at_scale(
|
||||
&stream,
|
||||
scaled_size,
|
||||
scaled_size,
|
||||
true,
|
||||
Some(&Cancellable::new()),
|
||||
)
|
||||
.map(Some)
|
||||
}
|
||||
None if use_fallback => image_ref.theme.load_icon_for_scale(
|
||||
FALLBACK_ICON_NAME,
|
||||
image_ref.size,
|
||||
scale,
|
||||
IconLookupFlags::empty(),
|
||||
),
|
||||
None => Ok(None),
|
||||
}?;
|
||||
|
||||
image.set_from_surface(Some(&surface));
|
||||
|
||||
Ok(())
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// Attempts to get a `Pixbuf` from the GTK icon theme.
|
||||
fn get_from_icon(&self, name: &str, theme: &IconTheme, scale: i32) -> Result<Pixbuf> {
|
||||
let pixbuf =
|
||||
match theme.lookup_icon_for_scale(name, self.size, scale, IconLookupFlags::empty()) {
|
||||
Some(_) => theme.load_icon(name, self.size * scale, IconLookupFlags::FORCE_SIZE),
|
||||
None => Ok(None),
|
||||
}?;
|
||||
|
||||
pixbuf.map_or_else(
|
||||
|| Err(Report::msg("Icon theme does not contain icon '{name}'")),
|
||||
Ok,
|
||||
)
|
||||
/// Returns true if the input starts with a prefix
|
||||
/// that is supported by the parser
|
||||
/// (i.e. the parser would not fall back to checking the input).
|
||||
pub fn is_explicit_input(input: &str) -> bool {
|
||||
input.starts_with("icon:")
|
||||
|| input.starts_with("file://")
|
||||
|| input.starts_with("http://")
|
||||
|| input.starts_with("https://")
|
||||
|| input.starts_with('/')
|
||||
}
|
||||
|
||||
/// Attempts to get a `Pixbuf` from a local file.
|
||||
fn get_from_file(&self, path: &Path, scale: i32) -> Result<Pixbuf> {
|
||||
let scaled_size = self.size * scale;
|
||||
let pixbuf = Pixbuf::from_file_at_scale(path, scaled_size, scaled_size, true)?;
|
||||
Ok(pixbuf)
|
||||
pub fn icon_theme(&self) -> IconTheme {
|
||||
self.icon_theme
|
||||
.borrow()
|
||||
.clone()
|
||||
.expect("theme should be set at startup")
|
||||
}
|
||||
|
||||
/// Attempts to get a `Pixbuf` from a local file,
|
||||
/// using the Steam game ID to look it up.
|
||||
fn get_from_steam_id(&self, steam_id: &str, scale: i32) -> Result<Pixbuf> {
|
||||
// TODO: Can we load this from icon theme with app id `steam_icon_{}`?
|
||||
let path = dirs::data_dir().map_or_else(
|
||||
|| Err(Report::msg("Missing XDG data dir")),
|
||||
|dir| {
|
||||
Ok(dir.join(format!(
|
||||
"icons/hicolor/32x32/apps/steam_icon_{steam_id}.png"
|
||||
)))
|
||||
},
|
||||
)?;
|
||||
/// Sets the custom icon theme name.
|
||||
/// If no name is provided, the system default is used.
|
||||
pub fn set_icon_theme(&self, theme: Option<&str>) {
|
||||
trace!("Setting icon theme to {:?}", theme);
|
||||
|
||||
self.get_from_file(&path, scale)
|
||||
}
|
||||
|
||||
/// Attempts to get `Bytes` from an HTTP resource asynchronously.
|
||||
#[cfg(feature = "http")]
|
||||
async fn get_bytes_from_http(url: reqwest::Url) -> Result<glib::Bytes> {
|
||||
let res = reqwest::get(url).await?;
|
||||
|
||||
let status = res.status();
|
||||
if status.is_success() {
|
||||
let bytes = res.bytes().await?;
|
||||
Ok(glib::Bytes::from_owned(bytes))
|
||||
*self.icon_theme.borrow_mut() = if theme.is_some() {
|
||||
let icon_theme = IconTheme::new();
|
||||
icon_theme.set_custom_theme(theme);
|
||||
Some(icon_theme)
|
||||
} else {
|
||||
Err(Report::msg(format!(
|
||||
"Received non-success HTTP code ({status})"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_fallback_icon(theme: &'a IconTheme) -> ImageLocation<'a> {
|
||||
ImageLocation::Icon {
|
||||
name: "dialog-question-symbolic".to_string(),
|
||||
theme,
|
||||
}
|
||||
IconTheme::default()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to create a Cairo `Surface` from the provided `Pixbuf`,
|
||||
/// using the provided scaling factor.
|
||||
/// The surface is then loaded into the provided image.
|
||||
///
|
||||
/// This is necessary for HiDPI since `Pixbuf`s are always treated as scale factor 1.
|
||||
pub fn create_and_load_surface(pixbuf: &Pixbuf, image: &Image) -> Result<()> {
|
||||
let surface = unsafe {
|
||||
let ptr = gdk_cairo_surface_create_from_pixbuf(
|
||||
pixbuf.as_ptr(),
|
||||
image.scale_factor(),
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
|
||||
Surface::from_raw_full(ptr)
|
||||
}?;
|
||||
|
||||
image.set_from_surface(Some(&surface));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -18,12 +18,11 @@ pub fn handle_command(command: IronvarCommand) -> Response {
|
|||
|
||||
if key.contains('.') {
|
||||
for part in key.split('.') {
|
||||
ns = match ns.get_namespace(part) {
|
||||
Some(ns) => ns.clone(),
|
||||
None => {
|
||||
key = part.into();
|
||||
break;
|
||||
}
|
||||
ns = if let Some(ns) = ns.get_namespace(part) {
|
||||
ns.clone()
|
||||
} else {
|
||||
key = part.into();
|
||||
break;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,7 +80,7 @@ impl Namespace for VariableManager {
|
|||
let namespaces = read_lock!(self.namespaces);
|
||||
let ns = namespaces.get(ns)?;
|
||||
|
||||
ns.get(key).map(|v| v.to_owned())
|
||||
ns.get(key).as_deref().map(ToOwned::to_owned)
|
||||
} else {
|
||||
read_lock!(self.variables).get(key).and_then(IronVar::get)
|
||||
}
|
||||
|
|
33
src/main.rs
33
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,
|
||||
|
@ -370,7 +394,7 @@ fn load_output_bars(
|
|||
};
|
||||
|
||||
let config = ironbar.config.borrow();
|
||||
let icon_overrides = Arc::new(config.icon_overrides.clone());
|
||||
|
||||
let display = get_display();
|
||||
|
||||
let pos = output.logical_position.unwrap_or_default();
|
||||
|
@ -392,7 +416,6 @@ fn load_output_bars(
|
|||
&monitor,
|
||||
monitor_name.to_string(),
|
||||
output_size,
|
||||
icon_overrides,
|
||||
config.clone(),
|
||||
ironbar.clone(),
|
||||
)?]
|
||||
|
@ -405,7 +428,6 @@ fn load_output_bars(
|
|||
&monitor,
|
||||
monitor_name.to_string(),
|
||||
output_size,
|
||||
icon_overrides.clone(),
|
||||
config.clone(),
|
||||
ironbar.clone(),
|
||||
)
|
||||
|
@ -416,7 +438,6 @@ fn load_output_bars(
|
|||
&monitor,
|
||||
monitor_name.to_string(),
|
||||
output_size,
|
||||
icon_overrides,
|
||||
config.bar.clone(),
|
||||
ironbar.clone(),
|
||||
)?],
|
||||
|
|
|
@ -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,31 +152,33 @@ impl Module<gtk::Box> for FocusedModule {
|
|||
container.add(&label);
|
||||
|
||||
{
|
||||
let icon_overrides = info.icon_overrides.clone();
|
||||
let icon_theme = info.icon_theme.clone();
|
||||
let image_provider = context.ironbar.image_provider();
|
||||
|
||||
context.subscribe().recv_glib(move |data| {
|
||||
if let Some((name, mut id)) = data {
|
||||
if self.show_icon {
|
||||
if let Some(icon) = icon_overrides.get(&id) {
|
||||
id = icon.clone();
|
||||
context.subscribe().recv_glib_async(move |data| {
|
||||
let icon = icon.clone();
|
||||
let label = label.clone();
|
||||
let image_provider = image_provider.clone();
|
||||
|
||||
async move {
|
||||
if let Some((name, id)) = data {
|
||||
if self.show_icon {
|
||||
match image_provider
|
||||
.load_into_image(&id, self.icon_size, true, &icon)
|
||||
.await
|
||||
{
|
||||
Ok(true) => icon.show(),
|
||||
_ => icon.hide(),
|
||||
}
|
||||
}
|
||||
|
||||
match ImageProvider::parse(&id, &icon_theme, true, self.icon_size)
|
||||
.map(|image| image.load_into_image(&icon))
|
||||
{
|
||||
Some(Ok(())) => icon.show(),
|
||||
_ => icon.hide(),
|
||||
if self.show_title {
|
||||
label.show();
|
||||
label.set_label(&name);
|
||||
}
|
||||
} else {
|
||||
icon.hide();
|
||||
label.hide();
|
||||
}
|
||||
|
||||
if self.show_title {
|
||||
label.show();
|
||||
label.set_label(&name);
|
||||
}
|
||||
} else {
|
||||
icon.hide();
|
||||
label.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -231,7 +231,7 @@ impl Module<gtk::Box> for KeyboardModule {
|
|||
tracing::error!("{err:?}");
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -257,9 +257,11 @@ impl Module<gtk::Box> for KeyboardModule {
|
|||
) -> Result<ModuleParts<gtk::Box>> {
|
||||
let container = gtk::Box::new(self.layout.orientation(info), 0);
|
||||
|
||||
let caps = IconLabel::new(&self.icons.caps_off, info.icon_theme, self.icon_size);
|
||||
let num = IconLabel::new(&self.icons.num_off, info.icon_theme, self.icon_size);
|
||||
let scroll = IconLabel::new(&self.icons.scroll_off, info.icon_theme, self.icon_size);
|
||||
let image_provider = context.ironbar.image_provider();
|
||||
|
||||
let caps = IconLabel::new(&self.icons.caps_off, self.icon_size, &image_provider);
|
||||
let num = IconLabel::new(&self.icons.num_off, self.icon_size, &image_provider);
|
||||
let scroll = IconLabel::new(&self.icons.scroll_off, self.icon_size, &image_provider);
|
||||
|
||||
caps.label().set_angle(self.layout.angle(info));
|
||||
caps.label().set_justify(self.layout.justify.into());
|
||||
|
@ -270,7 +272,7 @@ impl Module<gtk::Box> for KeyboardModule {
|
|||
scroll.label().set_angle(self.layout.angle(info));
|
||||
scroll.label().set_justify(self.layout.justify.into());
|
||||
|
||||
let layout_button = IconButton::new("", info.icon_theme, self.icon_size);
|
||||
let layout_button = IconButton::new("", self.icon_size, image_provider);
|
||||
|
||||
if self.show_caps {
|
||||
caps.add_class("key");
|
||||
|
|
|
@ -3,20 +3,18 @@ use crate::channels::AsyncSenderExt;
|
|||
use crate::clients::wayland::ToplevelInfo;
|
||||
use crate::config::{BarPosition, TruncateMode};
|
||||
use crate::gtk_helpers::{IronbarGtkExt, IronbarLabelExt};
|
||||
use crate::image::ImageProvider;
|
||||
use crate::modules::ModuleUpdateEvent;
|
||||
use crate::modules::launcher::{ItemEvent, LauncherUpdate};
|
||||
use crate::read_lock;
|
||||
use crate::{image, read_lock};
|
||||
use glib::Propagation;
|
||||
use gtk::gdk::{BUTTON_MIDDLE, BUTTON_PRIMARY};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Align, Button, IconTheme, Image, Justification, Label, Orientation};
|
||||
use gtk::{Align, Button, Image, Justification, Label, Orientation};
|
||||
use indexmap::IndexMap;
|
||||
use std::ops::Deref;
|
||||
use std::rc::Rc;
|
||||
use std::sync::RwLock;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Item {
|
||||
|
@ -25,23 +23,16 @@ pub struct Item {
|
|||
pub open_state: OpenState,
|
||||
pub windows: IndexMap<usize, Window>,
|
||||
pub name: String,
|
||||
pub icon_override: String,
|
||||
}
|
||||
|
||||
impl Item {
|
||||
pub fn new(
|
||||
app_id: String,
|
||||
icon_override: String,
|
||||
open_state: OpenState,
|
||||
favorite: bool,
|
||||
) -> Self {
|
||||
pub fn new(app_id: String, open_state: OpenState, favorite: bool) -> Self {
|
||||
Self {
|
||||
app_id,
|
||||
favorite,
|
||||
open_state,
|
||||
windows: IndexMap::new(),
|
||||
name: String::new(),
|
||||
icon_override,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -116,7 +107,6 @@ impl From<ToplevelInfo> for Item {
|
|||
open_state,
|
||||
windows,
|
||||
name,
|
||||
icon_override: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -166,7 +156,7 @@ impl ItemButton {
|
|||
pub fn new(
|
||||
item: &Item,
|
||||
appearance: AppearanceOptions,
|
||||
icon_theme: &IconTheme,
|
||||
image_provider: image::Provider,
|
||||
bar_position: BarPosition,
|
||||
tx: &Sender<ModuleUpdateEvent<LauncherUpdate>>,
|
||||
controller_tx: &Sender<ItemEvent>,
|
||||
|
@ -181,21 +171,18 @@ impl ItemButton {
|
|||
}
|
||||
|
||||
if appearance.show_icons {
|
||||
let input = if !item.icon_override.is_empty() {
|
||||
item.icon_override.clone()
|
||||
} else if item.app_id.is_empty() {
|
||||
let input = if item.app_id.is_empty() {
|
||||
item.name.clone()
|
||||
} else {
|
||||
item.app_id.clone()
|
||||
};
|
||||
let image = ImageProvider::parse(&input, icon_theme, true, appearance.icon_size);
|
||||
if let Some(image) = image {
|
||||
button.set_always_show_image(true);
|
||||
|
||||
if let Err(err) = image.load_into_image(&button.image) {
|
||||
error!("{err:?}");
|
||||
}
|
||||
};
|
||||
let button = button.clone();
|
||||
glib::spawn_future_local(async move {
|
||||
image_provider
|
||||
.load_into_image_silent(&input, appearance.icon_size, true, &button.image)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
button.add_class("item");
|
||||
|
|
|
@ -8,7 +8,6 @@ use super::{Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, Wid
|
|||
use crate::channels::{AsyncSenderExt, BroadcastReceiverExt};
|
||||
use crate::clients::wayland::{self, ToplevelEvent};
|
||||
use crate::config::{CommonConfig, EllipsizeMode, LayoutConfig, TruncateMode};
|
||||
use crate::desktop_file::find_desktop_file;
|
||||
use crate::gtk_helpers::{IronbarGtkExt, IronbarLabelExt};
|
||||
use crate::modules::launcher::item::ImageTextButton;
|
||||
use crate::modules::launcher::pagination::{IconContext, Pagination};
|
||||
|
@ -18,11 +17,10 @@ use gtk::prelude::*;
|
|||
use gtk::{Button, Orientation};
|
||||
use indexmap::IndexMap;
|
||||
use serde::Deserialize;
|
||||
use std::ops::Deref;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, error, trace};
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
|
@ -212,7 +210,7 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
|
||||
fn spawn_controller(
|
||||
&self,
|
||||
info: &ModuleInfo,
|
||||
_info: &ModuleInfo,
|
||||
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
mut rx: mpsc::Receiver<Self::ReceiveMessage>,
|
||||
) -> crate::Result<()> {
|
||||
|
@ -223,14 +221,9 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
favorites
|
||||
.iter()
|
||||
.map(|app_id| {
|
||||
let icon_override = info
|
||||
.icon_overrides
|
||||
.get(app_id)
|
||||
.map_or_else(String::new, ToString::to_string);
|
||||
|
||||
(
|
||||
app_id.to_string(),
|
||||
Item::new(app_id.to_string(), icon_override, OpenState::Closed, true),
|
||||
Item::new(app_id.to_string(), OpenState::Closed, true),
|
||||
)
|
||||
})
|
||||
.collect::<IndexMap<_, _>>()
|
||||
|
@ -239,8 +232,6 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
let items = arc_mut!(items);
|
||||
let items2 = Arc::clone(&items);
|
||||
|
||||
let icon_overrides = info.icon_overrides.clone();
|
||||
|
||||
let tx = context.tx.clone();
|
||||
let tx2 = context.tx.clone();
|
||||
|
||||
|
@ -258,12 +249,7 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
if let Some(item) = item {
|
||||
item.merge_toplevel(info.clone());
|
||||
} else {
|
||||
let mut item = Item::from(info.clone());
|
||||
|
||||
if let Some(icon) = icon_overrides.get(&info.app_id) {
|
||||
item.icon_override.clone_from(icon);
|
||||
}
|
||||
|
||||
let item = Item::from(info.clone());
|
||||
items.insert(info.app_id.clone(), item);
|
||||
}
|
||||
}
|
||||
|
@ -271,6 +257,7 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
{
|
||||
let items = {
|
||||
let items = lock!(items);
|
||||
|
||||
items
|
||||
.iter()
|
||||
.map(|(_, item)| item.clone())
|
||||
|
@ -296,12 +283,7 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
let item = items.get_mut(&info.app_id);
|
||||
match item {
|
||||
None => {
|
||||
let mut item: Item = info.into();
|
||||
|
||||
if let Some(icon) = icon_overrides.get(&app_id) {
|
||||
item.icon_override.clone_from(icon);
|
||||
}
|
||||
|
||||
let item: Item = info.into();
|
||||
items.insert(app_id.clone(), item.clone());
|
||||
|
||||
ItemOrWindow::Item(item)
|
||||
|
@ -378,7 +360,7 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
.await?;
|
||||
}
|
||||
None => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -389,17 +371,16 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
// listen to ui events
|
||||
let minimize_focused = self.minimize_focused;
|
||||
let wl = context.client::<wayland::Client>();
|
||||
|
||||
let desktop_files = context.ironbar.desktop_files();
|
||||
|
||||
spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
if let ItemEvent::OpenItem(app_id) = event {
|
||||
find_desktop_file(&app_id).map_or_else(
|
||||
|| error!("Could not find desktop file for {}", app_id),
|
||||
|file| {
|
||||
match desktop_files.find(&app_id).await {
|
||||
Ok(Some(file)) => {
|
||||
if let Err(err) = Command::new("gtk-launch")
|
||||
.arg(
|
||||
file.file_name()
|
||||
.expect("File segment missing from path to desktop file"),
|
||||
)
|
||||
.arg(file.file_name)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
|
@ -408,11 +389,13 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
"{:?}",
|
||||
Report::new(err)
|
||||
.wrap_err("Failed to run gtk-launch command.")
|
||||
.suggestion("Perhaps the desktop file is invalid?")
|
||||
.suggestion("Perhaps the applications file is invalid?")
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
Ok(None) => warn!("Could not find applications file for {}", app_id),
|
||||
Err(err) => error!("Failed to find parse file for {}: {}", app_id, err),
|
||||
}
|
||||
} else {
|
||||
tx.send_expect(ModuleUpdateEvent::ClosePopup).await;
|
||||
|
||||
|
@ -457,26 +440,25 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> crate::Result<ModuleParts<gtk::Box>> {
|
||||
let icon_theme = info.icon_theme;
|
||||
|
||||
let container = gtk::Box::new(self.layout.orientation(info), 0);
|
||||
let page_size = self.page_size;
|
||||
|
||||
let image_provider = context.ironbar.image_provider();
|
||||
|
||||
let pagination = Pagination::new(
|
||||
&container,
|
||||
self.page_size,
|
||||
self.layout.orientation(info),
|
||||
IconContext {
|
||||
icon_back: &self.icons.page_back,
|
||||
icon_fwd: &self.icons.page_forward,
|
||||
icon_size: self.pagination_icon_size,
|
||||
icon_theme: info.icon_theme,
|
||||
&IconContext {
|
||||
back: &self.icons.page_back,
|
||||
fwd: &self.icons.page_forward,
|
||||
size: self.pagination_icon_size,
|
||||
},
|
||||
&image_provider,
|
||||
);
|
||||
|
||||
{
|
||||
let container = container.clone();
|
||||
let icon_theme = icon_theme.clone();
|
||||
|
||||
let controller_tx = context.controller_tx.clone();
|
||||
|
||||
|
@ -517,16 +499,16 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
let button = ItemButton::new(
|
||||
&item,
|
||||
appearance_options,
|
||||
&icon_theme,
|
||||
image_provider.clone(),
|
||||
bar_position,
|
||||
&tx,
|
||||
&controller_tx,
|
||||
);
|
||||
|
||||
if self.reversed {
|
||||
container.pack_end(button.button.deref(), false, false, 0);
|
||||
container.pack_end(&*button.button, false, false, 0);
|
||||
} else {
|
||||
container.add(button.button.deref());
|
||||
container.add(&*button.button);
|
||||
}
|
||||
|
||||
if buttons.len() + 1 >= pagination.offset() + page_size {
|
||||
|
@ -599,7 +581,7 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
}
|
||||
}
|
||||
LauncherUpdate::Hover(_) => {}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
rx.recv_glib(handle_event);
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
use crate::gtk_helpers::IronbarGtkExt;
|
||||
use crate::image;
|
||||
use crate::image::IconButton;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, IconTheme, Orientation};
|
||||
use gtk::{Button, Orientation};
|
||||
use std::cell::RefCell;
|
||||
use std::ops::Deref;
|
||||
use std::rc::Rc;
|
||||
|
@ -14,10 +15,9 @@ pub struct Pagination {
|
|||
}
|
||||
|
||||
pub struct IconContext<'a> {
|
||||
pub icon_back: &'a str,
|
||||
pub icon_fwd: &'a str,
|
||||
pub icon_size: i32,
|
||||
pub icon_theme: &'a IconTheme,
|
||||
pub back: &'a str,
|
||||
pub fwd: &'a str,
|
||||
pub size: i32,
|
||||
}
|
||||
|
||||
impl Pagination {
|
||||
|
@ -25,21 +25,16 @@ impl Pagination {
|
|||
container: >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,
|
||||
);
|
||||
let scroll_back =
|
||||
IconButton::new(icon_context.back, icon_context.size, image_provider.clone());
|
||||
|
||||
let scroll_fwd = IconButton::new(
|
||||
icon_context.icon_fwd,
|
||||
icon_context.icon_theme,
|
||||
icon_context.icon_size,
|
||||
);
|
||||
let scroll_fwd =
|
||||
IconButton::new(icon_context.fwd, icon_context.size, image_provider.clone());
|
||||
|
||||
scroll_back.set_sensitive(false);
|
||||
scroll_fwd.set_sensitive(false);
|
||||
|
@ -86,8 +81,8 @@ impl Pagination {
|
|||
if page_size < *offset {
|
||||
*offset -= page_size;
|
||||
} else {
|
||||
*offset = 1
|
||||
};
|
||||
*offset = 1;
|
||||
}
|
||||
|
||||
Self::update_page(&container, *offset, page_size);
|
||||
|
||||
|
|
|
@ -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)
|
||||
}) {
|
||||
album_image.show();
|
||||
image.load_into_image(&album_image)
|
||||
|
||||
if let Some(cover_path) = new_cover {
|
||||
let image_provider = image_provider.clone();
|
||||
let album_image = album_image.clone();
|
||||
|
||||
glib::spawn_future_local(async move {
|
||||
let success = match image_provider
|
||||
.load_into_image(
|
||||
&cover_path,
|
||||
image_size,
|
||||
false,
|
||||
&album_image,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(true) => {
|
||||
album_image.show();
|
||||
true
|
||||
}
|
||||
Ok(false) => {
|
||||
warn!("failed to parse image: {}", cover_path);
|
||||
false
|
||||
}
|
||||
Err(err) => {
|
||||
error!("failed to load image: {}", err);
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if !success {
|
||||
album_image.set_from_pixbuf(None);
|
||||
album_image.hide();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
album_image.set_from_pixbuf(None);
|
||||
album_image.hide();
|
||||
Ok(())
|
||||
};
|
||||
|
||||
if let Err(err) = res {
|
||||
error!("{err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -490,7 +515,7 @@ impl Module<Button> for MusicModule {
|
|||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -544,10 +569,10 @@ struct IconPrefixedLabel {
|
|||
}
|
||||
|
||||
impl IconPrefixedLabel {
|
||||
fn new(icon_input: &str, label: Option<&str>, icon_theme: &IconTheme) -> Self {
|
||||
fn new(icon_input: &str, label: Option<&str>, image_provider: &image::Provider) -> Self {
|
||||
let container = gtk::Box::new(Orientation::Horizontal, 5);
|
||||
|
||||
let icon = IconLabel::new(icon_input, icon_theme, 24);
|
||||
let icon = IconLabel::new(icon_input, 24, image_provider);
|
||||
|
||||
let mut builder = Label::builder().use_markup(true);
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -156,7 +156,7 @@ impl Module<Overlay> for NotificationsModule {
|
|||
match initial_state {
|
||||
Ok(ev) => tx.send_update(ev).await,
|
||||
Err(err) => error!("{err:?}"),
|
||||
};
|
||||
}
|
||||
|
||||
while let Ok(ev) = rx.recv().await {
|
||||
tx.send_update(ev).await;
|
||||
|
|
|
@ -251,7 +251,7 @@ impl Module<gtk::Box> for SysInfoModule {
|
|||
RefreshType::Disks => client.refresh_disks(),
|
||||
RefreshType::Network => client.refresh_network(),
|
||||
RefreshType::System => client.refresh_load_average(),
|
||||
};
|
||||
}
|
||||
|
||||
for (i, token_set) in format_tokens.iter().enumerate() {
|
||||
let is_affected = token_set
|
||||
|
|
|
@ -237,7 +237,7 @@ fn parse_formatting(chars: &mut Peekable<Chars>, mut formatting: Formatting) ->
|
|||
formatting.align = Alignment::try_from(char)?;
|
||||
}
|
||||
(_, FormattingMode::WidthFillAlign) => formatting.fill = char,
|
||||
};
|
||||
}
|
||||
|
||||
next_char = chars.next();
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ impl Popup {
|
|||
/// This includes setting up gtk-layer-shell
|
||||
/// and an empty `gtk::Box` container.
|
||||
pub fn new(
|
||||
ironbar: Rc<Ironbar>,
|
||||
ironbar: &Ironbar,
|
||||
module_info: &ModuleInfo,
|
||||
output_size: (i32, i32),
|
||||
gap: i32,
|
||||
|
|
|
@ -217,7 +217,7 @@ impl Script {
|
|||
}
|
||||
Err(err) => error!("{err:?}"),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
sleep(tokio::time::Duration::from_millis(self.interval)).await;
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ pub fn load_css(style_path: PathBuf, application: Application) {
|
|||
.suggestion("Check the CSS file for errors")
|
||||
.suggestion("GTK CSS uses a subset of the full CSS spec and many properties are not available. Ensure you are not using any unsupported property.")
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
let screen = gdk::Screen::default().expect("Failed to get default GTK screen");
|
||||
StyleContext::add_provider_for_screen(
|
||||
|
|
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