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.
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",

@ -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"

@ -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>'
urgency_start = '<span background="darkred" foreground="yellow">'
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
@ -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:<image-file>:text:<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

@ -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),
}
}
}

@ -40,6 +40,8 @@ pub struct Format {
pub workspace_format: Option<String>,
pub urgency_start: Option<String>,
pub urgency_end: Option<String>,
pub icon_dirs: Option<Vec<String>>,
pub fallback_icon: Option<String>,
}
impl Default for Menu {
@ -78,6 +80,11 @@ impl Default for Format {
.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::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<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>(
prompt: &'a str,
choices: &'b [TS],

Loading…
Cancel
Save