Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -913,6 +913,8 @@ icon_style = "auto"

`WAYSCRIBER_TRAY_FORCE_PIXMAP=1` takes precedence over this setting and also disables named menu icons for compatibility with tray hosts that render them incorrectly.

With the symbolic icon, the tray also advertises the install prefix's `share/icons` directory as its icon theme path, so hosts can find the icon in a custom install such as `/opt/wayscriber`. It is left out when the host already searches that directory through `XDG_DATA_HOME` or `XDG_DATA_DIRS`, as with `/usr` and `/usr/local` installs. `WAYSCRIBER_ICON_THEME_PATH` replaces the advertised path; set it to an empty value to advertise none.

The tray menu reads the configuration to draw itself and changes none of it. Its **Session
persistence settings…** entry launches the configurator on the Session screen, where the change is
made and saved.
Expand Down Expand Up @@ -2684,6 +2686,7 @@ wayscriber --daemon &
These override behavior at runtime. Bool-ish values treat anything except `0`, `false`, or `off` as true.

- `WAYSCRIBER_NO_TRAY=1` disables the tray icon (default: tray enabled)
- `WAYSCRIBER_ICON_THEME_PATH=/path/to/icons` replaces the icon theme path the tray advertises; an empty value advertises none (default: the install prefix's `share/icons` with the symbolic tray icon, unless the XDG data directories already cover it)
- `WAYSCRIBER_RESUME_SESSION=1/0` forces session persistence on/off for the current run (default: unset; follows config)
- `WAYSCRIBER_CONFIGURATOR=/path/to/wayscriber-configurator` overrides the configurator executable path
- `WAYSCRIBER_DISABLE_UPDATE_CHECK=1` disables the background update check for this run (overrides `[updates] check`; `--check-update` still works)
Expand Down
104 changes: 104 additions & 0 deletions src/daemon/tray/icon_theme_path.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//! The `IconThemePath` the tray item advertises beside its icon name.
//!
//! The StatusNotifierItem spec makes this path an addition to the host's icon
//! theme search path. Hosts already search the XDG data directories, so a
//! package installed under one of them gains nothing by advertising its icon
//! directory, and some hosts index the advertised directory recursively:
//! `/usr/share/icons` made them walk every installed icon theme (#412).

use crate::env_vars::ICON_THEME_PATH_ENV;
use crate::paths;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};

/// The `IconThemePath` to advertise.
///
/// [`ICON_THEME_PATH_ENV`] wins whenever it is set, even to an empty value.
/// Otherwise the install prefix's icon directory is advertised, but only while
/// the tray names an icon for the host to find there, and only when the host
/// does not already search that directory.
pub(super) fn resolve_icon_theme_path(names_tray_icon: bool) -> String {
if let Ok(value) = env::var(ICON_THEME_PATH_ENV) {
return value;
}
if !names_tray_icon {
return String::new();
}

let Ok(exe_path) = env::current_exe() else {
return String::new();
};
installed_icon_theme_path(&exe_path, &standard_icon_dirs())
.map(|path| path.to_string_lossy().into_owned())
.unwrap_or_default()
}

/// `icons` under each data directory a spec-following host searches.
fn standard_icon_dirs() -> Vec<PathBuf> {
paths::searched_data_dirs()
.into_iter()
.map(|dir| dir.join("icons"))
.collect()
}

/// `<prefix>/share/icons` for the prefix `exe_path` is installed under, unless
/// that directory is missing or is one of `standard_icon_dirs`.
fn installed_icon_theme_path(exe_path: &Path, standard_icon_dirs: &[PathBuf]) -> Option<PathBuf> {
let install_root = exe_path.parent()?.parent()?;
let icon_theme_path = install_root.join("share/icons");
if !icon_theme_path.is_dir() {
return None;
}

// Compare resolved paths so a symlinked data directory still matches.
let resolved = fs::canonicalize(&icon_theme_path).ok()?;
let already_searched = standard_icon_dirs
.iter()
.filter_map(|dir| fs::canonicalize(dir).ok())
.any(|dir| dir == resolved);
(!already_searched).then_some(icon_theme_path)
}

#[cfg(test)]
mod tests {
use super::installed_icon_theme_path;
use std::fs;

#[test]
fn installed_icon_dir_is_withheld_when_hosts_already_search_it() {
let root = crate::test_temp::tempdir().unwrap();
let share = root.path().join("usr/share");
fs::create_dir_all(share.join("icons")).unwrap();
let linked_share = root.path().join("linked-share");
std::os::unix::fs::symlink(&share, &linked_share).unwrap();
let exe = root.path().join("usr/bin/wayscriber");

assert_eq!(
installed_icon_theme_path(&exe, &[share.join("icons")]),
None
);
assert_eq!(
installed_icon_theme_path(&exe, &[linked_share.join("icons")]),
None
);
}

#[test]
fn installed_icon_dir_is_advertised_for_a_private_prefix() {
let root = crate::test_temp::tempdir().unwrap();
let private_icons = root.path().join("opt/wayscriber/share/icons");
let system_icons = root.path().join("usr/share/icons");
fs::create_dir_all(&private_icons).unwrap();
fs::create_dir_all(&system_icons).unwrap();
let exe = root.path().join("opt/wayscriber/bin/wayscriber");

assert_eq!(
installed_icon_theme_path(
&exe,
&[system_icons, root.path().join("missing/share/icons")]
),
Some(private_icons)
);
}
}
27 changes: 4 additions & 23 deletions src/daemon/tray/ksni.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ use super::super::types::TrayStatus;
#[cfg(feature = "tray")]
use super::WayscriberTray;
#[cfg(feature = "tray")]
use super::icon_theme_path::resolve_icon_theme_path;
#[cfg(feature = "tray")]
use super::shortcut_hint_io::configured_toggle_shortcut_hint;
#[cfg(feature = "tray")]
use crate::config::{Action, TrayIconStyle, action_label};
#[cfg(feature = "tray")]
use crate::configurator_destination::{ConfiguratorDestination, ConfiguratorScreen};
#[cfg(feature = "tray")]
use crate::env_vars::{
DESKTOP_SESSION_ENV, ICON_THEME_PATH_ENV, TRAY_FORCE_PIXMAP_ENV, XDG_CURRENT_DESKTOP_ENV,
XDG_SESSION_DESKTOP_ENV,
DESKTOP_SESSION_ENV, TRAY_FORCE_PIXMAP_ENV, XDG_CURRENT_DESKTOP_ENV, XDG_SESSION_DESKTOP_ENV,
};
#[cfg(feature = "tray")]
use crate::label_format::format_binding_label;
Expand All @@ -22,8 +23,6 @@ use log::{info, warn};
#[cfg(feature = "tray")]
use std::env;
#[cfg(feature = "tray")]
use std::path::PathBuf;
#[cfg(feature = "tray")]
use std::sync::atomic::Ordering;
#[cfg(feature = "tray")]
use std::time::{Duration, Instant};
Expand Down Expand Up @@ -60,7 +59,7 @@ impl ksni::Tray for WayscriberTray {
}

fn icon_theme_path(&self) -> String {
resolve_icon_theme_path()
resolve_icon_theme_path(tray_symbolic_icon_enabled(self.icon_style))
}

fn tool_tip(&self) -> ksni::ToolTip {
Expand Down Expand Up @@ -423,16 +422,6 @@ fn menu_icon_name(name: &str, use_theme_icons: bool) -> String {
}
}

#[cfg(feature = "tray")]
fn resolve_icon_theme_path() -> String {
if let Ok(value) = env::var(ICON_THEME_PATH_ENV) {
return value;
}
installed_icon_theme_path()
.map(|path| path.to_string_lossy().into_owned())
.unwrap_or_default()
}

#[cfg(feature = "tray")]
fn tray_theme_icons_supported(desktop_env: &str, session_env: &str, desktop_session: &str) -> bool {
!tray_theme_icons_blocked_by_desktop(desktop_env)
Expand All @@ -448,14 +437,6 @@ fn tray_theme_icons_blocked_by_desktop(value: &str) -> bool {
|| value.contains("cosmic")
}

#[cfg(feature = "tray")]
fn installed_icon_theme_path() -> Option<PathBuf> {
let exe_path = env::current_exe().ok()?;
let install_root = exe_path.parent()?.parent()?;
let icon_theme_path = install_root.join("share/icons");
icon_theme_path.is_dir().then_some(icon_theme_path)
}

#[cfg(feature = "tray")]
fn toggle_overlay_menu_label() -> String {
let base = "Toggle Overlay";
Expand Down
2 changes: 2 additions & 0 deletions src/daemon/tray/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
mod helpers;
#[cfg(feature = "tray")]
mod icon_theme_path;
#[cfg(feature = "tray")]
mod ksni;
mod runtime;
#[cfg(feature = "tray")]
Expand Down
1 change: 1 addition & 0 deletions src/env_vars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ pub const XDG_ACTIVATION_TOKEN_ENV: &str = "XDG_ACTIVATION_TOKEN";
pub const XDG_CACHE_HOME_ENV: &str = "XDG_CACHE_HOME";
pub const XDG_CONFIG_HOME_ENV: &str = "XDG_CONFIG_HOME";
pub const XDG_DATA_HOME_ENV: &str = "XDG_DATA_HOME";
pub const XDG_DATA_DIRS_ENV: &str = "XDG_DATA_DIRS";
pub const XDG_PICTURES_DIR_ENV: &str = "XDG_PICTURES_DIR";
pub const XDG_RUNTIME_DIR_ENV: &str = "XDG_RUNTIME_DIR";
/// Seat this session belongs to; the input HUD's system capture follows it so
Expand Down
52 changes: 50 additions & 2 deletions src/paths/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use std::env;
use std::ffi::OsString;
use std::path::PathBuf;

use crate::env_vars::{
HOME_ENV, USERPROFILE_ENV, XDG_CACHE_HOME_ENV, XDG_CONFIG_HOME_ENV, XDG_DATA_HOME_ENV,
XDG_PICTURES_DIR_ENV, XDG_RUNTIME_DIR_ENV,
HOME_ENV, USERPROFILE_ENV, XDG_CACHE_HOME_ENV, XDG_CONFIG_HOME_ENV, XDG_DATA_DIRS_ENV,
XDG_DATA_HOME_ENV, XDG_PICTURES_DIR_ENV, XDG_RUNTIME_DIR_ENV,
};

/// Resolve the user's home directory.
Expand Down Expand Up @@ -53,6 +54,53 @@ pub fn data_dir() -> Option<PathBuf> {
home_dir().map(|home| home.join(".local").join("share"))
}

/// The data directories a program following the XDG base directory spec
/// searches, most important first: the data home, then the system data
/// directories.
///
/// Unlike [`data_dir`], this ignores relative values, as the spec requires, so
/// the result does not depend on the current directory.
pub fn searched_data_dirs() -> Vec<PathBuf> {
searched_data_dirs_from(
env::var_os(XDG_DATA_HOME_ENV),
home_dir(),
env::var_os(XDG_DATA_DIRS_ENV),
)
}

/// Reads `XDG_DATA_HOME` and `XDG_DATA_DIRS` values as the base directory spec
/// does: an unset, empty, or relative data home means `~/.local/share`.
fn searched_data_dirs_from(
data_home: Option<OsString>,
home: Option<PathBuf>,
data_dirs: Option<OsString>,
) -> Vec<PathBuf> {
let data_home = data_home
.map(PathBuf::from)
.filter(|dir| dir.is_absolute())
.or_else(|| home.map(|home| home.join(".local").join("share")))
.filter(|dir| dir.is_absolute());

data_home
.into_iter()
.chain(system_data_dirs_from(data_dirs))
.collect()
}

/// Parses an `XDG_DATA_DIRS` value as the base directory spec reads it: an
/// unset or empty value means the defaults, and relative entries are ignored.
fn system_data_dirs_from(value: Option<OsString>) -> Vec<PathBuf> {
let Some(value) = value.filter(|value| !value.is_empty()) else {
return vec![
PathBuf::from("/usr/local/share"),
PathBuf::from("/usr/share"),
];
};
env::split_paths(&value)
.filter(|dir| dir.is_absolute())
.collect()
}

/// Location for generated, persistent runtime UI preferences. This is kept
/// separate from the authored configuration and drawing-session stores.
pub(crate) fn runtime_ui_state_file() -> PathBuf {
Expand Down
45 changes: 45 additions & 0 deletions src/paths/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,51 @@ fn data_dir_falls_back_to_home_share() {
}
}

#[test]
fn searched_data_dirs_follow_the_base_directory_spec() {
let home = || Some(PathBuf::from("/home/user"));
let default_home = PathBuf::from("/home/user/.local/share");
let system_defaults = [
PathBuf::from("/usr/local/share"),
PathBuf::from("/usr/share"),
];
let with_defaults = |data_home: PathBuf| {
let mut dirs = vec![data_home];
dirs.extend(system_defaults.clone());
dirs
};

assert_eq!(
searched_data_dirs_from(None, home(), None),
with_defaults(default_home.clone())
);
assert_eq!(
searched_data_dirs_from(Some(OsString::new()), home(), Some(OsString::new())),
with_defaults(default_home.clone())
);
// A relative data home would resolve against the current directory.
assert_eq!(
searched_data_dirs_from(Some("share".into()), home(), None),
with_defaults(default_home)
);
assert_eq!(
searched_data_dirs_from(Some("/data".into()), home(), None),
with_defaults(PathBuf::from("/data"))
);
assert_eq!(
searched_data_dirs_from(
Some("/data".into()),
home(),
Some("/opt/share::relative/share:/usr/share/".into())
),
vec![
PathBuf::from("/data"),
PathBuf::from("/opt/share"),
PathBuf::from("/usr/share/")
]
);
}

#[test]
fn pictures_dir_prefers_xdg_when_set() {
let _guard = crate::test_env::lock();
Expand Down
Loading