diff --git a/README.md b/README.md
index 7f9f79b7..41558240 100644
--- a/README.md
+++ b/README.md
@@ -178,7 +178,7 @@ Press F1 or F10 for help; Shift+F1 for quick re
**3. Daemon mode (preferred for daily use, optional)**
Daemon mode is faster and keeps session state between toggles.
-For setup steps, see [Usage](#daemon-mode-preferred).
+For setup steps, see [Usage](#daemon-mode-preferred). Ubuntu GNOME users: Super+G is a good default because Super+D is often reserved.
Alternative (one-shot mode):
```bash
@@ -359,7 +359,9 @@ Run wayscriber in the background and toggle with a keybind:
systemctl --user enable --now wayscriber.service
```
-Add keybinding (Hyprland):
+Add keybinding:
+
+Hyprland:
```conf
bind = SUPER, D, exec, pkill -SIGUSR1 wayscriber
```
@@ -369,6 +371,23 @@ Reload your config:
hyprctl reload
```
+GNOME (Ubuntu/Debian/Fedora):
+1. Open `Settings -> Keyboard -> Keyboard Shortcuts`.
+2. Scroll down to `Custom Shortcuts`, click `+`.
+3. Name: `Wayscriber Toggle`.
+4. Command: `pkill -SIGUSR1 wayscriber`.
+5. Set a shortcut key (recommended on Ubuntu GNOME: Super+G; Super+D is often already in use).
+
+KDE Plasma:
+1. Open `Settings -> Keyboard -> Shortcuts`.
+2. Add new `Command or Script`.
+3. Name: `Wayscriber Toggle`.
+4. Command: `pkill -SIGUSR1 wayscriber`.
+5. Assign a key (for example Meta+Shift+D).
+
+Other desktops/window managers:
+- Bind `pkill -SIGUSR1 wayscriber` to any global shortcut key you prefer.
+
Use `--no-tray` or `WAYSCRIBER_NO_TRAY=1` if you don't have a system tray; otherwise right-click the tray icon for options:
- Toggle overlay visibility
- Freeze/unfreeze the current overlay
diff --git a/config.example.toml b/config.example.toml
index ab42ade0..9c60ab88 100644
--- a/config.example.toml
+++ b/config.example.toml
@@ -262,6 +262,10 @@ active_output_badge = true
# Request fullscreen for the GNOME fallback overlay. Disable if fullscreen appears opaque.
#xdg_fullscreen = false
+# Behavior when GNOME fallback (xdg-shell) loses keyboard focus.
+# "stay" (default on Ubuntu/GNOME) keeps it open. "exit" closes the overlay.
+#xdg_focus_loss_behavior = "exit"
+
# Mouse button that toggles radial menu
# Options: "middle", "right", "disabled"
radial_menu_mouse_binding = "middle"
diff --git a/configurator/src/app/entry.rs b/configurator/src/app/entry.rs
index ef750490..d72e7ecc 100644
--- a/configurator/src/app/entry.rs
+++ b/configurator/src/app/entry.rs
@@ -3,10 +3,17 @@ use iced::{Application, Settings, Size};
use super::state::ConfiguratorApp;
pub fn run() -> iced::Result {
- let mut settings = Settings::default();
+ let mut settings = Settings {
+ id: Some("wayscriber-configurator".to_string()),
+ ..Settings::default()
+ };
settings.window.size = Size::new(960.0, 640.0);
settings.window.resizable = true;
settings.window.decorations = true;
+ #[cfg(target_os = "linux")]
+ {
+ settings.window.platform_specific.application_id = "wayscriber-configurator".to_string();
+ }
if std::env::var_os("ICED_BACKEND").is_none() && should_force_tiny_skia() {
// GNOME Wayland + wgpu can crash on dma-buf/present mode selection; tiny-skia avoids this.
// SAFETY: setting a process-local env var before initializing iced is safe here.
diff --git a/configurator/src/app/view/ui/mod.rs b/configurator/src/app/view/ui/mod.rs
index 23349993..2a2c6f84 100644
--- a/configurator/src/app/view/ui/mod.rs
+++ b/configurator/src/app/view/ui/mod.rs
@@ -57,6 +57,12 @@ impl ConfiguratorApp {
self.defaults.ui_xdg_fullscreen,
ToggleField::UiXdgFullscreen,
),
+ toggle_row(
+ "Keep open on xdg focus loss",
+ self.draft.ui_xdg_keep_on_focus_loss,
+ self.defaults.ui_xdg_keep_on_focus_loss,
+ ToggleField::UiXdgKeepOnFocusLoss,
+ ),
toggle_row(
"Enable context menu",
self.draft.ui_context_menu_enabled,
diff --git a/configurator/src/models/config/draft/from_config.rs b/configurator/src/models/config/draft/from_config.rs
index 9b7cc651..50c67afd 100644
--- a/configurator/src/models/config/draft/from_config.rs
+++ b/configurator/src/models/config/draft/from_config.rs
@@ -14,7 +14,7 @@ use super::super::boards::BoardsDraft;
use super::super::presets::PresetsDraft;
use super::super::toolbar_overrides::ToolbarModeOverridesDraft;
use super::ConfigDraft;
-use wayscriber::config::Config;
+use wayscriber::config::{Config, XdgFocusLossBehavior};
impl ConfigDraft {
pub fn from_config(config: &Config) -> Self {
@@ -73,6 +73,10 @@ impl ConfigDraft {
ui_context_menu_enabled: config.ui.context_menu.enabled,
ui_preferred_output: config.ui.preferred_output.clone().unwrap_or_default(),
ui_xdg_fullscreen: config.ui.xdg_fullscreen,
+ ui_xdg_keep_on_focus_loss: matches!(
+ config.ui.xdg_focus_loss_behavior,
+ XdgFocusLossBehavior::Stay
+ ),
ui_command_palette_toast_duration_ms: config
.ui
.command_palette_toast_duration_ms
diff --git a/configurator/src/models/config/draft/mod.rs b/configurator/src/models/config/draft/mod.rs
index 64fc0e48..46bf23d8 100644
--- a/configurator/src/models/config/draft/mod.rs
+++ b/configurator/src/models/config/draft/mod.rs
@@ -62,6 +62,7 @@ pub struct ConfigDraft {
pub ui_context_menu_enabled: bool,
pub ui_preferred_output: String,
pub ui_xdg_fullscreen: bool,
+ pub ui_xdg_keep_on_focus_loss: bool,
pub ui_command_palette_toast_duration_ms: String,
pub ui_toolbar_top_pinned: bool,
pub ui_toolbar_side_pinned: bool,
diff --git a/configurator/src/models/config/setters.rs b/configurator/src/models/config/setters.rs
index dabaff87..6c6bc764 100644
--- a/configurator/src/models/config/setters.rs
+++ b/configurator/src/models/config/setters.rs
@@ -59,6 +59,7 @@ impl ConfigDraft {
ToggleField::UiHelpOverlayContextFilter => self.help_context_filter = value,
ToggleField::UiContextMenuEnabled => self.ui_context_menu_enabled = value,
ToggleField::UiXdgFullscreen => self.ui_xdg_fullscreen = value,
+ ToggleField::UiXdgKeepOnFocusLoss => self.ui_xdg_keep_on_focus_loss = value,
ToggleField::UiToolbarTopPinned => self.ui_toolbar_top_pinned = value,
ToggleField::UiToolbarSidePinned => self.ui_toolbar_side_pinned = value,
ToggleField::UiToolbarUseIcons => self.ui_toolbar_use_icons = value,
diff --git a/configurator/src/models/config/tests.rs b/configurator/src/models/config/tests.rs
index 42666cf2..5edbab0b 100644
--- a/configurator/src/models/config/tests.rs
+++ b/configurator/src/models/config/tests.rs
@@ -5,7 +5,7 @@ use super::super::fields::{
};
use super::super::{ColorMode, NamedColorOption};
use super::ConfigDraft;
-use wayscriber::config::{ColorSpec, Config, ToolPresetConfig};
+use wayscriber::config::{ColorSpec, Config, ToolPresetConfig, XdgFocusLossBehavior};
use wayscriber::input::Tool;
#[test]
@@ -170,3 +170,20 @@ fn config_draft_round_trips_drag_tool_mapping() {
config.drawing.tab_drag_tool
);
}
+
+#[test]
+fn config_draft_round_trips_xdg_focus_loss_behavior() {
+ let mut config = Config::default();
+ config.ui.xdg_focus_loss_behavior = XdgFocusLossBehavior::Stay;
+
+ let draft = ConfigDraft::from_config(&config);
+ assert!(draft.ui_xdg_keep_on_focus_loss);
+
+ let round_trip = draft
+ .to_config(&config)
+ .expect("expected config to round trip");
+ assert_eq!(
+ round_trip.ui.xdg_focus_loss_behavior,
+ XdgFocusLossBehavior::Stay
+ );
+}
diff --git a/configurator/src/models/config/to_config/ui.rs b/configurator/src/models/config/to_config/ui.rs
index 5206a792..2217992b 100644
--- a/configurator/src/models/config/to_config/ui.rs
+++ b/configurator/src/models/config/to_config/ui.rs
@@ -1,7 +1,7 @@
use super::super::draft::ConfigDraft;
use super::super::parse::{parse_field, parse_u64_field};
use crate::models::error::FormError;
-use wayscriber::config::Config;
+use wayscriber::config::{Config, XdgFocusLossBehavior};
impl ConfigDraft {
pub(super) fn apply_ui(&self, config: &mut Config, errors: &mut Vec) {
@@ -18,6 +18,11 @@ impl ConfigDraft {
Some(preferred_output.to_string())
};
config.ui.xdg_fullscreen = self.ui_xdg_fullscreen;
+ config.ui.xdg_focus_loss_behavior = if self.ui_xdg_keep_on_focus_loss {
+ XdgFocusLossBehavior::Stay
+ } else {
+ XdgFocusLossBehavior::Exit
+ };
parse_u64_field(
&self.ui_command_palette_toast_duration_ms,
"ui.command_palette_toast_duration_ms",
diff --git a/configurator/src/models/fields/toggles.rs b/configurator/src/models/fields/toggles.rs
index 13048539..c410e43b 100644
--- a/configurator/src/models/fields/toggles.rs
+++ b/configurator/src/models/fields/toggles.rs
@@ -8,6 +8,7 @@ pub enum ToggleField {
UiHelpOverlayContextFilter,
UiContextMenuEnabled,
UiXdgFullscreen,
+ UiXdgKeepOnFocusLoss,
UiToolbarTopPinned,
UiToolbarSidePinned,
UiToolbarUseIcons,
diff --git a/docs/CONFIG.md b/docs/CONFIG.md
index 0310dd32..aaaf91cf 100644
--- a/docs/CONFIG.md
+++ b/docs/CONFIG.md
@@ -262,6 +262,10 @@ active_output_badge = true
# Request fullscreen for the GNOME fallback overlay (disable if opaque)
#xdg_fullscreen = false
+# Behavior when GNOME fallback (xdg-shell) loses keyboard focus
+# Options: "exit", "stay" (default on Ubuntu/GNOME)
+#xdg_focus_loss_behavior = "exit"
+
# Mouse button that toggles radial menu
# Options: "middle", "right", "disabled"
radial_menu_mouse_binding = "middle"
@@ -320,7 +324,7 @@ enabled = true
- **Highlight tool ring**: `show_on_highlight_tool = true` keeps a persistent halo visible while the highlight tool is active
- **Context menu**: `ui.context_menu.enabled` toggles right-click / keyboard menus
- **Output focus**: `multi_monitor_enabled` controls output-cycling shortcuts; `active_output_badge` shows the current monitor in the status bar
-- **GNOME fallback**: `preferred_output` pins the xdg-shell overlay to a specific monitor; `xdg_fullscreen` requests fullscreen instead of maximized
+- **GNOME fallback**: `preferred_output` pins the xdg-shell overlay to a specific monitor; `xdg_fullscreen` requests fullscreen instead of maximized; `xdg_focus_loss_behavior` controls whether losing focus closes (`exit`) or keeps (`stay`) the overlay
- **Radial menu trigger**: `radial_menu_mouse_binding` selects which mouse button opens radial menu (`middle` default, `right`, or `disabled`)
**Multi-monitor behavior:**
diff --git a/packaging/wayscriber.desktop b/packaging/wayscriber.desktop
index c19a9ec6..e99e739a 100644
--- a/packaging/wayscriber.desktop
+++ b/packaging/wayscriber.desktop
@@ -9,4 +9,4 @@ Icon=wayscriber
Terminal=false
Categories=Graphics;
Keywords=draw;annotate;screenshot;whiteboard;wayland;
-StartupNotify=false
+StartupNotify=true
diff --git a/src/about_window/mod.rs b/src/about_window/mod.rs
index 33545317..ee3b4129 100644
--- a/src/about_window/mod.rs
+++ b/src/about_window/mod.rs
@@ -10,6 +10,8 @@ use smithay_client_toolkit::shm::{Shm, slot::SlotPool};
use wayland_client::Connection;
use wayland_client::globals::registry_queue_init;
+use crate::app_id::runtime_app_id;
+
mod clipboard;
mod handlers;
mod render;
@@ -37,7 +39,8 @@ pub fn run_about_window() -> Result<()> {
let wl_surface = compositor_state.create_surface(&qh);
let window = xdg_shell.create_window(wl_surface, WindowDecorations::None, &qh);
window.set_title("Wayscriber About");
- window.set_app_id("com.devmobasa.wayscriber");
+ let app_id = runtime_app_id();
+ window.set_app_id(&app_id);
window.set_min_size(Some((ABOUT_WIDTH, ABOUT_HEIGHT)));
window.set_max_size(Some((ABOUT_WIDTH, ABOUT_HEIGHT)));
window.commit();
diff --git a/src/app/mod.rs b/src/app/mod.rs
index 4be504e8..4c7d219f 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -4,12 +4,39 @@ mod usage;
use crate::backend::ExitAfterCaptureMode;
use crate::cli::Cli;
+use crate::paths::overlay_lock_file;
+use crate::session::try_lock_exclusive;
use crate::session_override::set_runtime_session_override;
use env::env_flag_enabled;
use session::run_session_cli_commands;
+use std::fs::{File, OpenOptions};
+use std::io::ErrorKind;
use std::process::{Command, Stdio};
use usage::{log_overlay_controls, print_usage};
+fn acquire_overlay_lock() -> anyhow::Result