diff --git a/Cargo.lock b/Cargo.lock
index 9c55c19..3143076 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2,6 +2,15 @@
# It is not intended for manual editing.
version = 3
+[[package]]
+name = "aho-corasick"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
+dependencies = [
+ "memchr",
+]
+
[[package]]
name = "atty"
version = "0.2.14"
@@ -146,6 +155,12 @@ version = "0.2.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6"
+[[package]]
+name = "memchr"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
+
[[package]]
name = "os_str_bytes"
version = "2.4.0"
@@ -213,6 +228,23 @@ dependencies = [
"redox_syscall",
]
+[[package]]
+name = "regex"
+version = "1.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
+
[[package]]
name = "ryu"
version = "1.0.5"
@@ -284,6 +316,8 @@ version = "0.4.4"
dependencies = [
"clap",
"directories",
+ "lazy_static",
+ "regex",
"serde",
"serde_json",
"swayipc",
diff --git a/Cargo.toml b/Cargo.toml
index 99926c3..e9c9a21 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,3 +15,6 @@ clap = "3.0.0-beta.2"
swayipc = "3.0.0-alpha.3"
toml = "0.5.8"
directories = "3.0"
+regex = "1.5.4"
+lazy_static = "1.4.0"
+
diff --git a/README.md b/README.md
index e7415e2..2822285 100644
--- a/README.md
+++ b/README.md
@@ -148,6 +148,8 @@ window_format = '{urgency_start}“{title}”{urgency_end} — {app_na
workspace_format = 'Workspace {name} ({id})'
urgency_start = ''
urgency_end = ''
+icon_dirs = ['/usr/share/icons/Adwaita/48x48/apps', '/usr/share/icons/hicolor/48x48/apps', '/usr/share/pixmaps']
+fallback_icon = '/usr/share/icons/gnome/48x48/apps/kwin.png'
```
In the `[menu]` section, you can specify the menu program using the
@@ -162,6 +164,7 @@ to style the text using HTML and CSS. The following formats are supported
right now.
* `window_format` defines how windows are displayed. The placeholder `{title}`
is replaced with the window's title, `{app_name}` with the application name,
+ `{app_icon}` with the application's icon (a path to a PNG or SVG file),
`{workspace_name}` with the name or number of the workspace the window is
shown, and `{id}` is the window's sway-internal con id. There are also the
placeholders `{urcency_start}` and `{urgency_end}` which get replaced by the
@@ -175,6 +178,10 @@ right now.
in `window_format`.
* `urgency_end` is a string which replaces the `{urgency_end}` placeholder in
`window_format`.
+* `icon_dirs` is a vector of directories in which to look for application icons
+ in order to compute the `{app_icon}` replacement.
+* `fallback_icon` is a path to some PNG/SVG icon which will be used as
+ `{app_icon}` if no application-specific icon can be determined.
It is crucial that during selection (using wofi or some other menu program)
each window has a different display string. Therefore, it is highly
@@ -182,6 +189,10 @@ recommended to include the `{id}` placeholder at least in `window_format`.
Otherwise, e.g., two terminals (of the same terminal app) with the same working
directory (and therefore, the same title) wouldn't be distinguishable.
+Hint: `wofi` supports icons with the syntax `img::text:`, so
+a suitable `window_format` with application icon should start with
+`img:{app_icon}:text:`.
+
## Questions & Patches
For asking questions, sending feedback, or patches, refer to [my public inbox
diff --git a/src/con.rs b/src/con.rs
index 25b8cb6..a25666e 100644
--- a/src/con.rs
+++ b/src/con.rs
@@ -19,6 +19,7 @@ use crate::config as cfg;
use crate::ipc;
use crate::ipc::NodeMethods;
use crate::util;
+use lazy_static::lazy_static;
use std::cmp;
use std::collections::HashMap;
use std::fmt;
@@ -120,6 +121,11 @@ impl<'a> fmt::Display for Window<'a> {
}
}
+lazy_static! {
+ static ref APP_NAME_AND_VERSION_RX: regex::Regex =
+ regex::Regex::new("(.+)(-[0-9.]+)").unwrap();
+}
+
impl<'a> DisplayFormat for Window<'a> {
fn format_for_display(&self, cfg: &cfg::Config) -> String {
let default_format = cfg::Format::default();
@@ -138,6 +144,19 @@ impl<'a> DisplayFormat for Window<'a> {
.as_ref()
.and_then(|f| f.urgency_end.as_ref())
.unwrap_or_else(|| default_format.urgency_end.as_ref().unwrap());
+ let icon_dirs = cfg
+ .format
+ .as_ref()
+ .and_then(|f| f.icon_dirs.as_ref())
+ .unwrap_or_else(|| default_format.icon_dirs.as_ref().unwrap());
+ // fallback_icon has no default value.
+ let fallback_icon: Option<&String> =
+ cfg.format.as_ref().and_then(|f| f.fallback_icon.as_ref());
+
+ // Some apps report, e.g., Gimp-2.10 but the icon is still named
+ // gimp.png.
+ let app_name_no_version =
+ APP_NAME_AND_VERSION_RX.replace(self.get_app_name(), "$1");
fmt.replace("{id}", format!("{}", self.get_id()).as_str())
.replace(
@@ -161,6 +180,20 @@ impl<'a> DisplayFormat for Window<'a> {
"{workspace_name}",
self.workspace.name.as_ref().unwrap().as_str(),
)
+ .replace(
+ "{app_icon}",
+ util::get_icon(self.get_app_name(), icon_dirs)
+ .or_else(|| util::get_icon(&app_name_no_version, icon_dirs))
+ .or_else(|| {
+ util::get_icon(
+ &app_name_no_version.to_lowercase(),
+ icon_dirs,
+ )
+ })
+ .or_else(|| fallback_icon.map(|f| f.to_string()))
+ .unwrap_or_else(String::new)
+ .as_str(),
+ )
.replace("{title}", self.get_title())
}
}
@@ -280,9 +313,7 @@ impl DisplayFormat for WsOrWin<'_> {
fn format_for_display(&self, cfg: &cfg::Config) -> String {
match self {
WsOrWin::Ws { ws } => ws.format_for_display(cfg),
- WsOrWin::Win { win } => {
- "\t".to_owned() + &win.format_for_display(cfg)
- }
+ WsOrWin::Win { win } => win.format_for_display(cfg),
}
}
}
diff --git a/src/config.rs b/src/config.rs
index c8892aa..24491ae 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -40,6 +40,8 @@ pub struct Format {
pub workspace_format: Option,
pub urgency_start: Option,
pub urgency_end: Option,
+ pub icon_dirs: Option>,
+ pub fallback_icon: Option,
}
impl Default for Menu {
@@ -78,6 +80,11 @@ impl Default for Format {
.to_string(),
),
urgency_end: Some("".to_string()),
+ icon_dirs: Some(vec![
+ "/usr/share/icons/hicolor/48x48/apps".to_string(),
+ "/usr/share/pixmaps".to_string(),
+ ]),
+ fallback_icon: None,
}
}
}
diff --git a/src/util.rs b/src/util.rs
index 053c6a7..e69d08a 100644
--- a/src/util.rs
+++ b/src/util.rs
@@ -17,8 +17,9 @@
use crate::con::DisplayFormat;
use crate::config as cfg;
+use lazy_static::lazy_static;
use std::collections::HashMap;
-use std::io::Write;
+use std::io::{BufRead, Write};
use std::process as proc;
pub fn get_swayr_socket_path() -> String {
@@ -43,6 +44,137 @@ pub fn get_swayr_socket_path() -> String {
)
}
+fn desktop_entries() -> Vec {
+ let mut dirs = vec![];
+ if let Some(dd) = directories::BaseDirs::new()
+ .map(|b| b.data_local_dir().to_string_lossy().to_string())
+ {
+ dirs.push(dd);
+ }
+ dirs.push(String::from("/usr/share/applications/"));
+
+ let mut entries = vec![];
+ for dir in dirs {
+ if let Ok(readdir) = std::fs::read_dir(dir) {
+ for entry in readdir.flatten() {
+ let path = entry.path();
+ if path.is_file()
+ && path.extension().map(|ext| ext == "desktop")
+ == Some(true)
+ {
+ entries.push(path.as_path().to_string_lossy().to_string());
+ }
+ }
+ }
+ }
+ entries
+}
+
+fn find_icon(icon_name: &str, icon_dirs: &[String]) -> Option {
+ if std::path::Path::new(icon_name).is_file() {
+ return Some(String::from(icon_name));
+ }
+
+ for dir in icon_dirs {
+ for ext in &["png", "svg"] {
+ let mut pb = std::path::PathBuf::from(dir);
+ pb.push(icon_name);
+ pb.set_extension(ext);
+ let icon_file = pb.as_path();
+ if icon_file.is_file() {
+ return Some(String::from(icon_file.to_str().unwrap()));
+ }
+ }
+ }
+
+ None
+}
+
+lazy_static! {
+ static ref WM_CLASS_OR_ICON_RX: regex::Regex =
+ regex::Regex::new("(StartupWMClass|Icon)=(.+)").unwrap();
+}
+
+fn get_app_id_to_icon_map(icon_dirs: &[String]) -> HashMap {
+ let mut map = HashMap::new();
+
+ for e in desktop_entries() {
+ if let Ok(f) = std::fs::File::open(&e) {
+ let buf = std::io::BufReader::new(f);
+ let mut wm_class: Option = None;
+ let mut icon: Option = None;
+
+ // Get App-Id and Icon from desktop file.
+ for line in buf.lines() {
+ if wm_class.is_some() && icon.is_some() {
+ break;
+ }
+ if let Ok(line) = line {
+ if let Some(cap) = WM_CLASS_OR_ICON_RX.captures(&line) {
+ if "StartupWMClass" == cap.get(1).unwrap().as_str() {
+ wm_class.replace(
+ cap.get(2).unwrap().as_str().to_string(),
+ );
+ } else if let Some(icon_file) =
+ find_icon(cap.get(2).unwrap().as_str(), icon_dirs)
+ {
+ icon.replace(icon_file);
+ }
+ }
+ }
+ }
+
+ if let Some(icon) = icon {
+ if let Some(wm_class) = wm_class {
+ map.insert(wm_class, icon.clone());
+ }
+ map.insert(
+ String::from(
+ std::path::Path::new(&e)
+ .with_extension("")
+ .file_name()
+ .unwrap()
+ .to_string_lossy(),
+ ),
+ icon,
+ );
+ }
+ }
+ }
+
+ map
+}
+
+lazy_static! {
+ static ref APP_ID_TO_ICON_MAP: std::sync::Mutex