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
120 changes: 64 additions & 56 deletions configurator/src/app/daemon_setup/shortcut.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
use std::fs;

use crate::models::{DesktopEnvironment, ShortcutBackend};
use wayscriber::shortcut_hint::{
GNOME_MEDIA_KEYS_KEY, GNOME_MEDIA_KEYS_SCHEMA, GNOME_WAYSCRIBER_KEYBINDING_PATH,
gnome_effective_shortcut, gnome_shortcut_schema_with_path, normalize_shortcut_hint,
parse_gsettings_path_list,
};

use super::command::{command_available, run_command, run_command_checked};
use super::service::{
Expand All @@ -10,13 +15,6 @@ use super::service::{

const PORTAL_APP_ID: &str = "wayscriber";
const TOGGLE_COMMAND: &str = "pkill -SIGUSR1 wayscriber";

const GNOME_MEDIA_KEYS_SCHEMA: &str = "org.gnome.settings-daemon.plugins.media-keys";
const GNOME_MEDIA_KEYS_KEY: &str = "custom-keybindings";
const GNOME_CUSTOM_KEYBINDING_SCHEMA: &str =
"org.gnome.settings-daemon.plugins.media-keys.custom-keybinding";
const GNOME_WAYSCRIBER_KEYBINDING_PATH: &str =
"/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/wayscriber-toggle/";
const GNOME_SHORTCUT_NAME: &str = "Wayscriber Toggle";

pub(super) fn read_configured_shortcut(backend: ShortcutBackend) -> Option<String> {
Expand Down Expand Up @@ -80,8 +78,7 @@ fn apply_gnome_custom_shortcut(binding: &str) -> Result<(), String> {
&rendered_bindings,
])?;

let schema_with_path =
format!("{GNOME_CUSTOM_KEYBINDING_SCHEMA}:{GNOME_WAYSCRIBER_KEYBINDING_PATH}");
let schema_with_path = gnome_shortcut_schema_with_path();
run_gsettings_command(&[
"set",
&schema_with_path,
Expand Down Expand Up @@ -116,13 +113,21 @@ fn read_gnome_shortcut_binding() -> Option<String> {
if !command_available("gsettings") {
return None;
}
let schema_with_path =
format!("{GNOME_CUSTOM_KEYBINDING_SCHEMA}:{GNOME_WAYSCRIBER_KEYBINDING_PATH}");
let capture = run_command("gsettings", &["get", &schema_with_path, "binding"]).ok()?;
if !capture.success {
let custom_keybindings = run_command(
"gsettings",
&["get", GNOME_MEDIA_KEYS_SCHEMA, GNOME_MEDIA_KEYS_KEY],
)
.ok()?;
if !custom_keybindings.success {
return None;
}

let schema_with_path = gnome_shortcut_schema_with_path();
let binding = run_command("gsettings", &["get", &schema_with_path, "binding"]).ok()?;
if !binding.success {
return None;
}
parse_gsettings_string_value(capture.stdout.trim())
resolve_gnome_shortcut_from_gsettings(&custom_keybindings.stdout, &binding.stdout)
}

fn require_gsettings_available() -> Result<(), String> {
Expand Down Expand Up @@ -182,38 +187,11 @@ fn parse_portal_shortcut_from_dropin(content: &str) -> Option<String> {
return None;
}
let inner = &trimmed[prefix.len()..trimmed.len() - 1];
if inner.is_empty() {
return None;
}
Some(inner.replace("\\\"", "\"").replace("\\\\", "\\"))
let unescaped = inner.replace("\\\"", "\"").replace("\\\\", "\\");
normalize_shortcut_hint(Some(&unescaped))
})
}

fn parse_gsettings_path_list(raw: &str) -> Result<Vec<String>, String> {
let trimmed = raw.trim();
let list_literal = trimmed.strip_prefix("@as ").map_or(trimmed, str::trim);
if !list_literal.starts_with('[') || !list_literal.ends_with(']') {
return Err(format!(
"Unexpected gsettings list format: `{}`",
raw.trim()
));
}
let inner = list_literal[1..list_literal.len() - 1].trim();
if inner.is_empty() {
return Ok(Vec::new());
}

let mut values = Vec::new();
for chunk in inner.split(',') {
let value = chunk.trim().trim_matches('\'').trim_matches('"').trim();
if value.is_empty() {
continue;
}
values.push(value.to_string());
}
Ok(values)
}

fn serialize_gsettings_path_list(paths: &[String]) -> String {
let mut rendered = String::from("[");
for (index, path) in paths.iter().enumerate() {
Expand All @@ -233,19 +211,11 @@ fn gvariant_string_literal(value: &str) -> String {
format!("'{escaped}'")
}

fn parse_gsettings_string_value(raw: &str) -> Option<String> {
let trimmed = raw.trim();
if trimmed.is_empty() || trimmed == "''" {
return None;
}
let unquoted = trimmed
.strip_prefix('\'')
.and_then(|value| value.strip_suffix('\''))
.unwrap_or(trimmed);
if unquoted.is_empty() {
return None;
}
Some(unquoted.replace("\\'", "'").replace("\\\\", "\\"))
fn resolve_gnome_shortcut_from_gsettings(
custom_keybindings_output: &str,
binding_output: &str,
) -> Option<String> {
gnome_effective_shortcut(custom_keybindings_output, binding_output)
}

fn normalize_shortcut_for_gnome(input: &str) -> Result<String, String> {
Expand Down Expand Up @@ -367,6 +337,44 @@ mod tests {
);
}

#[test]
fn parse_portal_shortcut_ignores_blank_value() {
let content = "[Service]\nEnvironment=\"WAYSCRIBER_PORTAL_SHORTCUT= \"\n";
assert_eq!(parse_portal_shortcut_from_dropin(content), None);
}

#[test]
fn resolve_gnome_shortcut_requires_registered_path() {
let custom_keybindings =
"['/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/not-wayscriber/']";
assert_eq!(
resolve_gnome_shortcut_from_gsettings(custom_keybindings, "'<Super>g'"),
None
);
}

#[test]
fn resolve_gnome_shortcut_rejects_disabled_binding() {
let custom_keybindings = "['/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/wayscriber-toggle/']";
assert_eq!(
resolve_gnome_shortcut_from_gsettings(custom_keybindings, "'disabled'"),
None
);
assert_eq!(
resolve_gnome_shortcut_from_gsettings(custom_keybindings, "''"),
None
);
}

#[test]
fn resolve_gnome_shortcut_accepts_registered_binding() {
let custom_keybindings = "['/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/wayscriber-toggle/']";
assert_eq!(
resolve_gnome_shortcut_from_gsettings(custom_keybindings, "'<Super>g'"),
Some("<Super>g".to_string())
);
}

#[test]
fn normalize_shortcut_supports_human_readable_input() {
assert_eq!(
Expand Down
6 changes: 4 additions & 2 deletions configurator/src/models/daemon.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use wayscriber::shortcut_hint::is_gnome_desktop;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DesktopEnvironment {
Gnome,
Expand All @@ -13,10 +15,10 @@ impl DesktopEnvironment {
}

pub fn from_desktop_strings(current: &str, session: &str) -> Self {
let combined = format!("{current};{session}").to_lowercase();
if combined.contains("gnome") {
if is_gnome_desktop(current, session) {
return Self::Gnome;
}
let combined = format!("{current};{session}").to_lowercase();
if combined.contains("kde") || combined.contains("plasma") {
return Self::Kde;
}
Expand Down
43 changes: 2 additions & 41 deletions src/daemon/tray/ksni.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use super::WayscriberTray;
#[cfg(feature = "tray")]
use super::runtime::update_session_resume_in_config;
#[cfg(feature = "tray")]
use super::shortcut_hint_io::configured_toggle_shortcut_hint;
#[cfg(feature = "tray")]
use crate::config::{Action, action_label};
#[cfg(feature = "tray")]
use crate::label_format::format_binding_label;
Expand All @@ -15,8 +17,6 @@ use log::{info, warn};
#[cfg(feature = "tray")]
use std::env;
#[cfg(feature = "tray")]
use std::process::Command;
#[cfg(feature = "tray")]
use std::sync::atomic::Ordering;
#[cfg(feature = "tray")]
use std::time::{Duration, Instant};
Expand Down Expand Up @@ -316,42 +316,3 @@ fn toggle_overlay_menu_label() -> String {
None => base.to_string(),
}
}

#[cfg(feature = "tray")]
fn configured_toggle_shortcut_hint() -> Option<String> {
if let Ok(shortcut) = env::var("WAYSCRIBER_PORTAL_SHORTCUT")
&& !shortcut.trim().is_empty()
{
return Some(shortcut.trim().to_string());
}
read_gnome_toggle_shortcut_binding()
}

#[cfg(feature = "tray")]
fn read_gnome_toggle_shortcut_binding() -> Option<String> {
let schema = "org.gnome.settings-daemon.plugins.media-keys.custom-keybinding:/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/wayscriber-toggle/";
let output = Command::new("gsettings")
.args(["get", schema, "binding"])
.output()
.ok()?;
if !output.status.success() {
return None;
}
parse_gsettings_string_value(&String::from_utf8_lossy(&output.stdout))
}

#[cfg(feature = "tray")]
fn parse_gsettings_string_value(raw: &str) -> Option<String> {
let trimmed = raw.trim();
if trimmed.is_empty() || trimmed == "''" {
return None;
}
let unquoted = trimmed
.strip_prefix('\'')
.and_then(|value| value.strip_suffix('\''))
.unwrap_or(trimmed);
if unquoted.is_empty() {
return None;
}
Some(unquoted.replace("\\'", "'").replace("\\\\", "\\"))
}
2 changes: 2 additions & 0 deletions src/daemon/tray/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ mod helpers;
#[cfg(feature = "tray")]
mod ksni;
mod runtime;
#[cfg(feature = "tray")]
mod shortcut_hint_io;

pub(crate) use runtime::start_system_tray;

Expand Down
60 changes: 60 additions & 0 deletions src/daemon/tray/shortcut_hint_io.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#[cfg(feature = "tray")]
use std::env;
#[cfg(feature = "tray")]
use std::process::Command;
#[cfg(feature = "tray")]
use wayscriber::shortcut_hint::{
GNOME_MEDIA_KEYS_KEY, GNOME_MEDIA_KEYS_SCHEMA, gnome_shortcut_schema_with_path,
is_gnome_desktop, normalize_shortcut_hint, resolve_toggle_shortcut_hint,
};

#[cfg(feature = "tray")]
pub(super) fn configured_toggle_shortcut_hint() -> Option<String> {
let portal_shortcut_env = env::var("WAYSCRIBER_PORTAL_SHORTCUT").ok();
if let Some(shortcut) = normalize_shortcut_hint(portal_shortcut_env.as_deref()) {
return Some(shortcut);
}
let gnome_desktop = current_desktop_is_gnome();
let (custom_keybindings_raw, binding_raw) = if gnome_desktop {
match read_gnome_shortcut_outputs() {
Some((custom_keybindings, binding)) => (Some(custom_keybindings), Some(binding)),
None => (None, None),
}
} else {
(None, None)
};
resolve_toggle_shortcut_hint(
portal_shortcut_env.as_deref(),
gnome_desktop,
custom_keybindings_raw.as_deref(),
binding_raw.as_deref(),
)
}

#[cfg(feature = "tray")]
fn current_desktop_is_gnome() -> bool {
let current = env::var("XDG_CURRENT_DESKTOP").unwrap_or_default();
let session = env::var("XDG_SESSION_DESKTOP").unwrap_or_default();
is_gnome_desktop(&current, &session)
}

#[cfg(feature = "tray")]
fn read_gnome_shortcut_outputs() -> Option<(String, String)> {
let custom_keybindings_raw =
read_gsettings_value(GNOME_MEDIA_KEYS_SCHEMA, GNOME_MEDIA_KEYS_KEY)?;
let schema_with_path = gnome_shortcut_schema_with_path();
let binding_raw = read_gsettings_value(&schema_with_path, "binding")?;
Some((custom_keybindings_raw, binding_raw))
}

#[cfg(feature = "tray")]
fn read_gsettings_value(schema: &str, key: &str) -> Option<String> {
let output = Command::new("gsettings")
.args(["get", schema, key])
.output()
.ok()?;
if !output.status.success() {
return None;
}
Some(String::from_utf8_lossy(&output.stdout).to_string())
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub mod input;
mod label_format;
pub mod paths;
pub mod session;
pub mod shortcut_hint;
pub mod systemd_user_service;
pub mod time_utils;
pub mod toolbar_icons;
Expand Down
Loading