Skip to content

Commit 3afb8e5

Browse files
authored
Merge pull request #168 from devmobasa/fix/gnome-logout-portal-race
Fix GNOME portal shortcut shutdown handling
2 parents 1d2799b + 2cc9349 commit 3afb8e5

13 files changed

Lines changed: 485 additions & 81 deletions

File tree

configurator/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ version = "0.9.13"
44
edition = "2024"
55

66
[dependencies]
7-
wayscriber = { path = "..", default-features = false }
7+
# Keep configurator-side runtime/status logic aligned with the daemon's portal-capable builds.
8+
wayscriber = { path = "..", default-features = false, features = ["portal"] }
89
iced = { version = "0.12", features = ["tokio", "canvas"] }
910

1011
[features]

configurator/src/app/daemon_setup/mod.rs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ use crate::models::{
99
use command::command_available;
1010
use service::{
1111
SERVICE_NAME, detect_service_unit_path, install_or_update_user_service, query_service_active,
12-
query_service_enabled, require_systemctl_available, run_systemctl_user,
12+
query_service_enabled, remove_portal_shortcut_dropin_if_gnome, require_systemctl_available,
13+
run_systemctl_user,
1314
};
14-
use shortcut::{apply_shortcut, read_configured_shortcut};
15+
use shortcut::{apply_shortcut, read_configured_shortcut, read_portal_shortcut_dropin_state};
1516

1617
pub(super) async fn load_daemon_runtime_status() -> Result<DaemonRuntimeStatus, String> {
1718
load_daemon_runtime_status_sync()
@@ -33,10 +34,23 @@ fn perform_daemon_action_sync(
3334
match action {
3435
DaemonAction::RefreshStatus => Ok("Daemon status refreshed.".to_string()),
3536
DaemonAction::InstallOrUpdateService => {
37+
let desktop = DesktopEnvironment::detect_current();
3638
let service_path = install_or_update_user_service()?;
39+
let removed_dropin = remove_portal_shortcut_dropin_if_gnome(desktop)?;
40+
if command_available("systemctl") && removed_dropin {
41+
run_systemctl_user(&["daemon-reload"])?;
42+
if query_service_active() {
43+
run_systemctl_user(&["restart", SERVICE_NAME])?;
44+
}
45+
}
3746
Ok(format!(
38-
"Installed/updated user service at {}",
39-
service_path.display()
47+
"Installed/updated user service at {}{}",
48+
service_path.display(),
49+
if removed_dropin {
50+
"; removed stale GNOME portal shortcut drop-in"
51+
} else {
52+
""
53+
}
4054
))
4155
}
4256
DaemonAction::EnableAndStartService => {
@@ -64,7 +78,12 @@ fn load_daemon_runtime_status_sync() -> Result<DaemonRuntimeStatus, String> {
6478
let systemctl_available = command_available("systemctl");
6579
let gsettings_available = command_available("gsettings");
6680
let shortcut_backend =
67-
ShortcutBackend::from_environment(desktop, gsettings_available, systemctl_available);
81+
ShortcutBackend::from_runtime_inputs(desktop, read_portal_shortcut_dropin_state());
82+
let shortcut_apply_capability = crate::models::ShortcutApplyCapability::from_environment(
83+
desktop,
84+
gsettings_available,
85+
systemctl_available,
86+
);
6887
let service_unit_path = detect_service_unit_path(systemctl_available);
6988
let service_installed = service_unit_path.is_some();
7089
let service_enabled = if systemctl_available {
@@ -82,6 +101,7 @@ fn load_daemon_runtime_status_sync() -> Result<DaemonRuntimeStatus, String> {
82101
Ok(DaemonRuntimeStatus {
83102
desktop,
84103
shortcut_backend,
104+
shortcut_apply_capability,
85105
systemctl_available,
86106
gsettings_available,
87107
service_installed,

configurator/src/app/daemon_setup/service.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::env;
22
use std::fs;
33
use std::path::{Path, PathBuf};
44

5+
use crate::models::DesktopEnvironment;
56
use wayscriber::systemd_user_service::{
67
USER_SERVICE_NAME, escape_systemd_env_value as shared_escape_systemd_env_value,
78
portal_shortcut_dropin_path as shared_portal_shortcut_dropin_path, render_user_service_unit,
@@ -121,6 +122,32 @@ pub(super) fn install_or_update_user_service() -> Result<PathBuf, String> {
121122
Ok(service_path)
122123
}
123124

125+
pub(super) fn remove_portal_shortcut_dropin_if_gnome(
126+
desktop: DesktopEnvironment,
127+
) -> Result<bool, String> {
128+
if desktop != DesktopEnvironment::Gnome {
129+
return Ok(false);
130+
}
131+
remove_portal_shortcut_dropin()
132+
}
133+
134+
pub(super) fn remove_portal_shortcut_dropin() -> Result<bool, String> {
135+
let Some(path) = portal_shortcut_dropin_path() else {
136+
return Ok(false);
137+
};
138+
if !path.exists() {
139+
return Ok(false);
140+
}
141+
fs::remove_file(&path).map_err(|err| {
142+
format!(
143+
"Failed to remove portal shortcut drop-in {}: {}",
144+
path.display(),
145+
err
146+
)
147+
})?;
148+
Ok(true)
149+
}
150+
124151
fn package_service_paths() -> Vec<PathBuf> {
125152
vec![
126153
PathBuf::from("/usr/lib/systemd/user").join(SERVICE_NAME),

configurator/src/app/daemon_setup/shortcut.rs

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
use std::fs;
22

3-
use crate::models::{DesktopEnvironment, ShortcutBackend};
3+
use crate::models::{DesktopEnvironment, ShortcutApplyCapability, ShortcutBackend};
44
use wayscriber::shortcut_hint::{
55
GNOME_MEDIA_KEYS_KEY, GNOME_MEDIA_KEYS_SCHEMA, GNOME_WAYSCRIBER_KEYBINDING_PATH,
6-
gnome_effective_shortcut, gnome_shortcut_schema_with_path, normalize_shortcut_hint,
7-
parse_gsettings_path_list,
6+
PORTAL_APP_ID_ENV, PORTAL_SHORTCUT_ENV, PORTAL_SHORTCUT_OPT_IN_ENV, PortalShortcutDropInState,
7+
gnome_effective_shortcut, gnome_shortcut_schema_with_path, parse_gsettings_path_list,
8+
parse_portal_shortcut_dropin_state,
9+
parse_portal_shortcut_from_dropin as shared_parse_portal_shortcut_from_dropin,
810
};
911

1012
use super::command::{command_available, run_command, run_command_checked};
1113
use super::service::{
1214
escape_systemd_env_value, portal_shortcut_dropin_path, query_service_active,
13-
require_systemctl_available, run_systemctl_user,
15+
remove_portal_shortcut_dropin_if_gnome, require_systemctl_available, run_systemctl_user,
1416
};
1517

1618
const PORTAL_APP_ID: &str = "wayscriber";
@@ -25,21 +27,38 @@ pub(super) fn read_configured_shortcut(backend: ShortcutBackend) -> Option<Strin
2527
}
2628
}
2729

30+
pub(super) fn read_portal_shortcut_dropin_state() -> PortalShortcutDropInState {
31+
let Some(path) = portal_shortcut_dropin_path() else {
32+
return PortalShortcutDropInState::default();
33+
};
34+
let Ok(content) = fs::read_to_string(path) else {
35+
return PortalShortcutDropInState::default();
36+
};
37+
parse_portal_shortcut_dropin_state(&content)
38+
}
39+
2840
pub(super) fn apply_shortcut(shortcut_input: &str) -> Result<String, String> {
2941
let desktop = DesktopEnvironment::detect_current();
30-
let backend = ShortcutBackend::from_environment(
42+
let apply_capability = ShortcutApplyCapability::from_environment(
3143
desktop,
3244
command_available("gsettings"),
3345
command_available("systemctl"),
3446
);
3547

36-
match backend {
37-
ShortcutBackend::GnomeCustomShortcut => {
48+
match apply_capability {
49+
ShortcutApplyCapability::GnomeCustomShortcut => {
3850
let normalized = normalize_shortcut_for_gnome(shortcut_input)?;
3951
apply_gnome_custom_shortcut(&normalized)?;
52+
let removed_dropin = remove_portal_shortcut_dropin_if_gnome(desktop)?;
53+
if command_available("systemctl") {
54+
run_systemctl_user(&["daemon-reload"])?;
55+
if removed_dropin && query_service_active() {
56+
run_systemctl_user(&["restart", "wayscriber.service"])?;
57+
}
58+
}
4059
Ok(format!("Configured GNOME shortcut: {normalized}"))
4160
}
42-
ShortcutBackend::PortalServiceDropIn => {
61+
ShortcutApplyCapability::PortalServiceDropIn => {
4362
require_systemctl_available()?;
4463
let normalized = normalize_shortcut_for_portal(shortcut_input)?;
4564
let dropin_path = write_portal_shortcut_dropin(&normalized)?;
@@ -52,7 +71,7 @@ pub(super) fn apply_shortcut(shortcut_input: &str) -> Result<String, String> {
5271
dropin_path.display()
5372
))
5473
}
55-
ShortcutBackend::Manual => Err(
74+
ShortcutApplyCapability::Manual => Err(
5675
"Automatic shortcut setup is not available in this desktop session; bind `pkill -SIGUSR1 wayscriber` manually."
5776
.to_string(),
5877
),
@@ -160,9 +179,7 @@ fn write_portal_shortcut_dropin(shortcut: &str) -> Result<std::path::PathBuf, St
160179

161180
let escaped_shortcut = escape_systemd_env_value(shortcut);
162181
let escaped_app_id = escape_systemd_env_value(PORTAL_APP_ID);
163-
let contents = format!(
164-
"[Service]\nEnvironment=\"WAYSCRIBER_PORTAL_SHORTCUT={escaped_shortcut}\"\nEnvironment=\"WAYSCRIBER_PORTAL_APP_ID={escaped_app_id}\"\n"
165-
);
182+
let contents = render_portal_shortcut_dropin(&escaped_shortcut, &escaped_app_id);
166183
fs::write(&dropin_path, contents).map_err(|err| {
167184
format!(
168185
"Failed to write portal shortcut drop-in {}: {}",
@@ -173,23 +190,20 @@ fn write_portal_shortcut_dropin(shortcut: &str) -> Result<std::path::PathBuf, St
173190
Ok(dropin_path)
174191
}
175192

193+
fn render_portal_shortcut_dropin(escaped_shortcut: &str, escaped_app_id: &str) -> String {
194+
format!(
195+
"[Service]\nEnvironment=\"{PORTAL_SHORTCUT_OPT_IN_ENV}=1\"\nEnvironment=\"{PORTAL_SHORTCUT_ENV}={escaped_shortcut}\"\nEnvironment=\"{PORTAL_APP_ID_ENV}={escaped_app_id}\"\n"
196+
)
197+
}
198+
176199
fn read_portal_shortcut_from_dropin() -> Option<String> {
177200
let path = portal_shortcut_dropin_path()?;
178201
let content = fs::read_to_string(path).ok()?;
179202
parse_portal_shortcut_from_dropin(&content)
180203
}
181204

182205
fn parse_portal_shortcut_from_dropin(content: &str) -> Option<String> {
183-
content.lines().find_map(|line| {
184-
let trimmed = line.trim();
185-
let prefix = "Environment=\"WAYSCRIBER_PORTAL_SHORTCUT=";
186-
if !trimmed.starts_with(prefix) || !trimmed.ends_with('"') {
187-
return None;
188-
}
189-
let inner = &trimmed[prefix.len()..trimmed.len() - 1];
190-
let unescaped = inner.replace("\\\"", "\"").replace("\\\\", "\\");
191-
normalize_shortcut_hint(Some(&unescaped))
192-
})
206+
shared_parse_portal_shortcut_from_dropin(content)
193207
}
194208

195209
fn serialize_gsettings_path_list(paths: &[String]) -> String {
@@ -343,6 +357,14 @@ mod tests {
343357
assert_eq!(parse_portal_shortcut_from_dropin(content), None);
344358
}
345359

360+
#[test]
361+
fn render_portal_shortcut_dropin_includes_explicit_opt_in_marker() {
362+
let rendered = render_portal_shortcut_dropin("<Ctrl><Shift>g", PORTAL_APP_ID);
363+
assert!(rendered.contains("Environment=\"WAYSCRIBER_ENABLE_PORTAL_SHORTCUTS=1\""));
364+
assert!(rendered.contains("Environment=\"WAYSCRIBER_PORTAL_SHORTCUT=<Ctrl><Shift>g\""));
365+
assert!(rendered.contains("Environment=\"WAYSCRIBER_PORTAL_APP_ID=wayscriber\""));
366+
}
367+
346368
#[test]
347369
fn resolve_gnome_shortcut_requires_registered_path() {
348370
let custom_keybindings =

configurator/src/app/update/daemon.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ fn action_pending_message(action: DaemonAction) -> String {
159159
#[cfg(test)]
160160
mod tests {
161161
use super::*;
162-
use crate::models::{DesktopEnvironment, ShortcutBackend};
162+
use crate::models::{DesktopEnvironment, ShortcutApplyCapability, ShortcutBackend};
163163

164164
#[test]
165165
fn daemon_status_loaded_sets_default_shortcut_when_missing() {
@@ -168,6 +168,7 @@ mod tests {
168168
let status = DaemonRuntimeStatus {
169169
desktop: DesktopEnvironment::Kde,
170170
shortcut_backend: ShortcutBackend::PortalServiceDropIn,
171+
shortcut_apply_capability: ShortcutApplyCapability::PortalServiceDropIn,
171172
systemctl_available: true,
172173
gsettings_available: false,
173174
service_installed: false,
@@ -208,6 +209,7 @@ mod tests {
208209
let status = DaemonRuntimeStatus {
209210
desktop: DesktopEnvironment::Kde,
210211
shortcut_backend: ShortcutBackend::PortalServiceDropIn,
212+
shortcut_apply_capability: ShortcutApplyCapability::PortalServiceDropIn,
211213
systemctl_available: true,
212214
gsettings_available: false,
213215
service_installed: false,
@@ -235,6 +237,7 @@ mod tests {
235237
let status = DaemonRuntimeStatus {
236238
desktop: DesktopEnvironment::Kde,
237239
shortcut_backend: ShortcutBackend::PortalServiceDropIn,
240+
shortcut_apply_capability: ShortcutApplyCapability::PortalServiceDropIn,
238241
systemctl_available: true,
239242
gsettings_available: false,
240243
service_installed: false,
@@ -264,6 +267,7 @@ mod tests {
264267
let stale_status = DaemonRuntimeStatus {
265268
desktop: DesktopEnvironment::Kde,
266269
shortcut_backend: ShortcutBackend::PortalServiceDropIn,
270+
shortcut_apply_capability: ShortcutApplyCapability::PortalServiceDropIn,
267271
systemctl_available: true,
268272
gsettings_available: false,
269273
service_installed: false,
@@ -305,6 +309,7 @@ mod tests {
305309
let old_status = DaemonRuntimeStatus {
306310
desktop: DesktopEnvironment::Kde,
307311
shortcut_backend: ShortcutBackend::PortalServiceDropIn,
312+
shortcut_apply_capability: ShortcutApplyCapability::PortalServiceDropIn,
308313
systemctl_available: true,
309314
gsettings_available: false,
310315
service_installed: false,
@@ -316,6 +321,7 @@ mod tests {
316321
let new_status = DaemonRuntimeStatus {
317322
desktop: DesktopEnvironment::Kde,
318323
shortcut_backend: ShortcutBackend::PortalServiceDropIn,
324+
shortcut_apply_capability: ShortcutApplyCapability::PortalServiceDropIn,
319325
systemctl_available: true,
320326
gsettings_available: false,
321327
service_installed: true,

configurator/src/app/view/daemon.rs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use iced::theme;
33
use iced::widget::{button, column, horizontal_rule, row, scrollable, text, text_input};
44

55
use crate::messages::Message;
6-
use crate::models::{DaemonAction, ShortcutBackend};
6+
use crate::models::{DaemonAction, ShortcutApplyCapability};
77

88
use super::super::state::ConfiguratorApp;
99

@@ -157,14 +157,20 @@ impl ConfiguratorApp {
157157
.into();
158158
}
159159

160-
let placeholder = match self.daemon_status.as_ref().map(|s| s.shortcut_backend) {
161-
Some(ShortcutBackend::GnomeCustomShortcut) => "e.g. Super+G or <Super>g",
162-
Some(ShortcutBackend::PortalServiceDropIn) => "e.g. Ctrl+Shift+G or <Ctrl><Shift>g",
160+
let apply_capability = self
161+
.daemon_status
162+
.as_ref()
163+
.map(|s| s.shortcut_apply_capability);
164+
let placeholder = match apply_capability {
165+
Some(ShortcutApplyCapability::GnomeCustomShortcut) => "e.g. Super+G or <Super>g",
166+
Some(ShortcutApplyCapability::PortalServiceDropIn) => {
167+
"e.g. Ctrl+Shift+G or <Ctrl><Shift>g"
168+
}
163169
_ => "e.g. Ctrl+Shift+G",
164170
};
165171

166172
let mut shortcut_button = button("Apply Shortcut").style(theme::Button::Primary);
167-
if !busy {
173+
if !busy && apply_capability != Some(ShortcutApplyCapability::Manual) {
168174
shortcut_button = shortcut_button
169175
.on_press(Message::DaemonActionRequested(DaemonAction::ApplyShortcut));
170176
}
@@ -192,6 +198,14 @@ impl ConfiguratorApp {
192198
);
193199
}
194200

201+
if apply_capability == Some(ShortcutApplyCapability::Manual) {
202+
step = step.push(
203+
text("Automatic shortcut setup is unavailable here. Add a manual keybind for `pkill -SIGUSR1 wayscriber`.")
204+
.size(12)
205+
.style(theme::Text::Color(iced::Color::from_rgb(0.95, 0.8, 0.3))),
206+
);
207+
}
208+
195209
step = step.push(
196210
text_input(placeholder, &self.daemon_shortcut_input)
197211
.on_input(Message::DaemonShortcutInputChanged)
@@ -293,6 +307,11 @@ impl ConfiguratorApp {
293307
.size(12)
294308
.style(theme::Text::Color(iced::Color::from_rgb(0.6, 0.6, 0.6))),
295309
);
310+
details = details.push(
311+
text(status.shortcut_apply_capability.friendly_label())
312+
.size(12)
313+
.style(theme::Text::Color(iced::Color::from_rgb(0.6, 0.6, 0.6))),
314+
);
296315

297316
if let Some(path) = status.service_unit_path.as_deref() {
298317
details = details.push(

0 commit comments

Comments
 (0)