New {app_icon} replacement for window_format

timeout_old
Tassilo Horn 3 years ago
parent 467a6fc74d
commit daac2691bc
  1. 34
      Cargo.lock
  2. 3
      Cargo.toml
  3. 11
      README.md
  4. 37
      src/con.rs
  5. 7
      src/config.rs
  6. 134
      src/util.rs

34
Cargo.lock generated

@ -2,6 +2,15 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 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]] [[package]]
name = "atty" name = "atty"
version = "0.2.14" version = "0.2.14"
@ -146,6 +155,12 @@ version = "0.2.97"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6"
[[package]]
name = "memchr"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
[[package]] [[package]]
name = "os_str_bytes" name = "os_str_bytes"
version = "2.4.0" version = "2.4.0"
@ -213,6 +228,23 @@ dependencies = [
"redox_syscall", "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]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.5" version = "1.0.5"
@ -284,6 +316,8 @@ version = "0.4.4"
dependencies = [ dependencies = [
"clap", "clap",
"directories", "directories",
"lazy_static",
"regex",
"serde", "serde",
"serde_json", "serde_json",
"swayipc", "swayipc",

@ -15,3 +15,6 @@ clap = "3.0.0-beta.2"
swayipc = "3.0.0-alpha.3" swayipc = "3.0.0-alpha.3"
toml = "0.5.8" toml = "0.5.8"
directories = "3.0" directories = "3.0"
regex = "1.5.4"
lazy_static = "1.4.0"

@ -148,6 +148,8 @@ window_format = '{urgency_start}<b>“{title}”</b>{urgency_end} — <i>{app_na
workspace_format = '<b>Workspace {name}</b> <span alpha="20000">({id})</span>' workspace_format = '<b>Workspace {name}</b> <span alpha="20000">({id})</span>'
urgency_start = '<span background="darkred" foreground="yellow">' urgency_start = '<span background="darkred" foreground="yellow">'
urgency_end = '</span>' urgency_end = '</span>'
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 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. right now.
* `window_format` defines how windows are displayed. The placeholder `{title}` * `window_format` defines how windows are displayed. The placeholder `{title}`
is replaced with the window's title, `{app_name}` with the application name, 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 `{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 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 placeholders `{urcency_start}` and `{urgency_end}` which get replaced by the
@ -175,6 +178,10 @@ right now.
in `window_format`. in `window_format`.
* `urgency_end` is a string which replaces the `{urgency_end}` placeholder in * `urgency_end` is a string which replaces the `{urgency_end}` placeholder in
`window_format`. `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) It is crucial that during selection (using wofi or some other menu program)
each window has a different display string. Therefore, it is highly 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 Otherwise, e.g., two terminals (of the same terminal app) with the same working
directory (and therefore, the same title) wouldn't be distinguishable. directory (and therefore, the same title) wouldn't be distinguishable.
Hint: `wofi` supports icons with the syntax `img:<image-file>:text:<text>`, so
a suitable `window_format` with application icon should start with
`img:{app_icon}:text:`.
## Questions & Patches ## Questions & Patches
For asking questions, sending feedback, or patches, refer to [my public inbox For asking questions, sending feedback, or patches, refer to [my public inbox

@ -19,6 +19,7 @@ use crate::config as cfg;
use crate::ipc; use crate::ipc;
use crate::ipc::NodeMethods; use crate::ipc::NodeMethods;
use crate::util; use crate::util;
use lazy_static::lazy_static;
use std::cmp; use std::cmp;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt; 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> { impl<'a> DisplayFormat for Window<'a> {
fn format_for_display(&self, cfg: &cfg::Config) -> String { fn format_for_display(&self, cfg: &cfg::Config) -> String {
let default_format = cfg::Format::default(); let default_format = cfg::Format::default();
@ -138,6 +144,19 @@ impl<'a> DisplayFormat for Window<'a> {
.as_ref() .as_ref()
.and_then(|f| f.urgency_end.as_ref()) .and_then(|f| f.urgency_end.as_ref())
.unwrap_or_else(|| default_format.urgency_end.as_ref().unwrap()); .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()) fmt.replace("{id}", format!("{}", self.get_id()).as_str())
.replace( .replace(
@ -161,6 +180,20 @@ impl<'a> DisplayFormat for Window<'a> {
"{workspace_name}", "{workspace_name}",
self.workspace.name.as_ref().unwrap().as_str(), 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()) .replace("{title}", self.get_title())
} }
} }
@ -280,9 +313,7 @@ impl DisplayFormat for WsOrWin<'_> {
fn format_for_display(&self, cfg: &cfg::Config) -> String { fn format_for_display(&self, cfg: &cfg::Config) -> String {
match self { match self {
WsOrWin::Ws { ws } => ws.format_for_display(cfg), WsOrWin::Ws { ws } => ws.format_for_display(cfg),
WsOrWin::Win { win } => { WsOrWin::Win { win } => win.format_for_display(cfg),
"\t".to_owned() + &win.format_for_display(cfg)
}
} }
} }
} }

@ -40,6 +40,8 @@ pub struct Format {
pub workspace_format: Option<String>, pub workspace_format: Option<String>,
pub urgency_start: Option<String>, pub urgency_start: Option<String>,
pub urgency_end: Option<String>, pub urgency_end: Option<String>,
pub icon_dirs: Option<Vec<String>>,
pub fallback_icon: Option<String>,
} }
impl Default for Menu { impl Default for Menu {
@ -78,6 +80,11 @@ impl Default for Format {
.to_string(), .to_string(),
), ),
urgency_end: Some("</span>".to_string()), urgency_end: Some("</span>".to_string()),
icon_dirs: Some(vec![
"/usr/share/icons/hicolor/48x48/apps".to_string(),
"/usr/share/pixmaps".to_string(),
]),
fallback_icon: None,
} }
} }
} }

@ -17,8 +17,9 @@
use crate::con::DisplayFormat; use crate::con::DisplayFormat;
use crate::config as cfg; use crate::config as cfg;
use lazy_static::lazy_static;
use std::collections::HashMap; use std::collections::HashMap;
use std::io::Write; use std::io::{BufRead, Write};
use std::process as proc; use std::process as proc;
pub fn get_swayr_socket_path() -> String { pub fn get_swayr_socket_path() -> String {
@ -43,6 +44,137 @@ pub fn get_swayr_socket_path() -> String {
) )
} }
fn desktop_entries() -> Vec<String> {
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<String> {
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<String, String> {
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<String> = None;
let mut icon: Option<String> = 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<Option<HashMap<String, String>>> =
std::sync::Mutex::new(None);
}
pub fn get_icon(app_id: &str, icon_dirs: &[String]) -> Option<String> {
let mut opt = APP_ID_TO_ICON_MAP.lock().unwrap();
if opt.is_none() {
opt.replace(get_app_id_to_icon_map(icon_dirs));
}
opt.as_ref().unwrap().get(app_id).map(String::from)
}
#[test]
fn test_desktop_entries() {
let icon_dirs = vec![
String::from("/usr/share/icons/hicolor/48x48/apps"),
String::from("/usr/share/pixmaps"),
];
let m = get_app_id_to_icon_map(&icon_dirs);
println!("Found {} icon entries:\n{:#?}", m.len(), m);
let apps = vec!["Emacs", "Alacritty", "firefoxdeveloperedition", "gimp"];
for app in apps {
println!("Icon for {}: {:?}", app, get_icon(app, &icon_dirs))
}
}
pub fn select_from_menu<'a, 'b, TS>( pub fn select_from_menu<'a, 'b, TS>(
prompt: &'a str, prompt: &'a str,
choices: &'b [TS], choices: &'b [TS],

Loading…
Cancel
Save