diff --git a/.idea/runConfigurations/Test.xml b/.idea/runConfigurations/Test.xml
new file mode 100644
index 0000000..1659764
--- /dev/null
+++ b/.idea/runConfigurations/Test.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Cargo.toml b/Cargo.toml
index 204ea5d..f94d8c3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"] }
diff --git a/docs/Configuration guide.md b/docs/Configuration guide.md
index a93b7c6..fd92b50 100644
--- a/docs/Configuration guide.md
+++ b/docs/Configuration guide.md
@@ -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` | `{}` | Map of [ironvar](ironvars) keys against their default values. |
-| `monitors` | `Map` | `null` | Map of monitor names against bar configs. |
-| `icon_overrides` | `Map` | `{}` | Map of app IDs (or classes) to icon names, overriding the app's default icon. |
+| Name | Type | Default | Description |
+|--------------------|-----------------------------------------|---------|--------------------------------------------------------------------------------------------------------------------------------|
+| `ironvar_defaults` | `Map` | `{}` | Map of [ironvar](ironvars) keys against their default values. |
+| `monitors` | `Map` | `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` | `{}` | 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. |
diff --git a/src/bar.rs b/src/bar.rs
index 8ec6ae2..1056f46 100644
--- a/src/bar.rs
+++ b/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>,
position: BarPosition,
ironbar: Rc,
@@ -46,7 +43,6 @@ impl Bar {
app: &Application,
monitor_name: String,
monitor_size: (i32, i32),
- icon_overrides: Arc>,
config: BarConfig,
ironbar: Rc,
) -> 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 {
- 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>,
config: BarConfig,
ironbar: Rc,
) -> Result {
- 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)
}
diff --git a/src/channels.rs b/src/channels.rs
index 69317d5..0d48625 100644
--- a/src/channels.rs
+++ b/src/channels.rs
@@ -125,6 +125,12 @@ where
fn recv_glib(self, f: F)
where
F: FnMut(T) + 'static;
+
+ /// Like [`BroadcastReceiverExt::recv_glib`], but the closure must return a [`Future`].
+ fn recv_glib_async(self, f: Fn)
+ where
+ Fn: FnMut(T) -> F + 'static,
+ F: Future;
}
impl BroadcastReceiverExt for broadcast::Receiver
@@ -152,4 +158,29 @@ where
}
});
}
+
+ fn recv_glib_async(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;
+ }
+ }
+ }
+ });
+ }
}
diff --git a/src/clients/networkmanager.rs b/src/clients/networkmanager.rs
index 02ce6b5..f81ca45 100644
--- a/src/clients/networkmanager.rs
+++ b/src/clients/networkmanager.rs
@@ -140,7 +140,7 @@ pub async fn create_client() -> Result> {
spawn(async move {
if let Err(error) = client.run().await {
error!("{}", error);
- };
+ }
});
}
Ok(client)
diff --git a/src/clients/wayland/mod.rs b/src/clients/wayland/mod.rs
index ca41926..299b087 100644
--- a/src/clients/wayland/mod.rs
+++ b/src/clients/wayland/mod.rs
@@ -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),
- };
+ }
}
});
}
diff --git a/src/config/layout.rs b/src/config/layout.rs
index bc2601c..e9149a2 100644
--- a/src/config/layout.rs
+++ b/src/config/layout.rs
@@ -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)
}
}
diff --git a/src/config/mod.rs b/src/config/mod.rs
index 39d2713..088fdb3 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -295,12 +295,6 @@ pub struct BarConfig {
#[serde(default)]
pub autohide: Option,
- /// The name of the GTK icon theme to use.
- /// Leave unset to use the default Adwaita theme.
- ///
- /// **Default**: `null`
- pub icon_theme: Option,
-
/// 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>,
+ /// The name of the GTK icon theme to use.
+ /// Leave unset to use the default Adwaita theme.
+ ///
+ /// **Default**: `null`
+ pub icon_theme: Option,
+
/// Map of app IDs (or classes) to icon names,
/// overriding the app's default icon.
///
diff --git a/src/desktop_file.rs b/src/desktop_file.rs
index 558743e..2d788df 100644
--- a/src/desktop_file.rs
+++ b/src/desktop_file.rs
@@ -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>;
-
-fn desktop_files() -> &'static Mutex> {
- static DESKTOP_FILES: OnceLock>> = 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> = OnceLock::new();
- DESKTOP_FILES_LOOK_OUT_KEYS
- .get_or_init(|| HashSet::from(["Name", "StartupWMClass", "Exec", "Icon"]))
+impl DesktopFileRef {
+ async fn get(&mut self) -> Result {
+ 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 {
+ 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 {
+#[derive(Debug, Clone)]
+pub struct DesktopFile {
+ pub file_name: String,
+ pub name: Option,
+ pub app_type: Option,
+ pub startup_wm_class: Option,
+ pub exec: Option,
+ pub icon: Option,
+}
+
+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, DesktopFileRef>;
+
+/// Desktop file cache and resolver.
+///
+/// Files are lazy-loaded as required on resolution.
+#[derive(Debug, Clone)]
+pub struct DesktopFiles {
+ files: Arc>,
+}
+
+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