Popup auf SNI-Desktops: Activate-Koordinaten (Anchor::Click) + Cinnamon-Extension

- SNI-Activate-Koordinaten gehen als Anchor::Click durch; Cinnamon liefert
  echte Werte, bei (0,0) bleibt der Cursor-Fallback (KDE/XFCE).
- Neue Cinnamon-Extension ai-control-popup@local: platziert das Popup unter
  Wayland am Zeiger (Work-Area-geklemmt, set_position ist dort wirkungslos)
  und gibt ihm unter X11 den initialen Fokus (Muffins Focus-Stealing-
  Prevention verwirft das present() der App).
- Extension in deb/rpm nach /usr/share/cinnamon/extensions paketiert.
- README: Cinnamon-Abschnitte und Installer-Teststand aktualisiert.

Verifiziert in der VM-Flotte: Cinnamon Wayland + X11, XFCE, KDE (Wayland),
GNOME (Wayland).
This commit is contained in:
marcus hinz
2026-07-21 11:23:45 +02:00
parent 1fa9ffcf1b
commit bbb06e0814
7 changed files with 105 additions and 16 deletions
+3 -2
View File
@@ -64,11 +64,11 @@ Releases carry a `.deb`, an `.rpm`, and an AppImage.
- **deb / rpm** — recommended. Both also install the GNOME Shell extension and the KWin script that provide the tray/popup integration on GNOME and KDE Wayland (into `/usr/share/gnome-shell/extensions/` and `/usr/share/kwin/scripts/`).
- **AppImage** — runs on desktops with a standard tray (KDE, XFCE, Cinnamon). On GNOME it refuses to start and points to the deb/rpm: the tray there needs the shell extension, which an installation-free AppImage cannot provide.
Tray integration by desktop: GNOME via the bundled shell extension, KDE Wayland via StatusNotifierItem plus the bundled KWin script for popup placement, KDE X11/XFCE/Cinnamon via StatusNotifierItem directly.
Tray integration by desktop: GNOME via the bundled shell extension, KDE Wayland via StatusNotifierItem plus the bundled KWin script for popup placement, KDE X11/XFCE/Cinnamon via StatusNotifierItem directly. On Cinnamon **Wayland** sessions a bundled Cinnamon extension additionally places the popup at the tray icon (a Wayland client cannot position its own window); Cinnamon X11 needs no extension.
### Installer tests
The installers are hand-tested per release. Current state: Fedora 44 via rpm on GNOME, KDE Plasma, Cinnamon, and XFCE (VM test fleet); Ubuntu 26.04 (GNOME), Kubuntu (KDE Plasma, Wayland) and Linux Mint (Cinnamon) via deb in earlier rounds. Two known issues from the current round are open: on Cinnamon the tray popup opens at the top-left corner instead of at the tray icon, and on the X11 desktops (Cinnamon, XFCE) clicking outside the popup does not close it. macOS re-test of the current build is in progress.
The installers are hand-tested per release. Current state: Fedora 44 via rpm on GNOME, KDE Plasma, Cinnamon, and XFCE (VM test fleet); Ubuntu 26.04 (GNOME), Kubuntu (KDE Plasma, Wayland) and Linux Mint (Cinnamon) via deb in earlier rounds. The two open findings from that round are resolved: popup placement on Cinnamon Wayland now comes from the bundled Cinnamon extension, and click-outside-to-close verified fine on XFCE and Cinnamon. macOS re-test of the current build is in progress.
### Windows
@@ -109,6 +109,7 @@ Deleting a project is scoped in three stages, each preceded by a preview of the
│ ├── capabilities/ per-window ACL (main/popup/terminal/panel)
│ └── icons/ app and tray icons
├── gnome-shell-extension/ tray/popup integration for GNOME
├── cinnamon-extension/ popup placement for Cinnamon Wayland
├── kwin-script/ popup placement for KDE Wayland
├── dev.sh development mode (tauri dev)
├── build.sh release build for the current OS (see below)
@@ -0,0 +1,69 @@
/* Cinnamon-Extension (CJS, Muffin/Meta-0)
* ai-control-popup@local
*
* Das Tray-Icon kommt unter Cinnamon über SNI (xapp), die App zeigt das
* Popup selbst. Die Extension ergänzt, was der Client nicht kann:
* - Wayland: set_position aus dem Client ist wirkungslos — die Extension
* schiebt das frisch gemappte Popup an den Mauszeiger (der beim Klick auf
* dem Icon steht), mit derselben Klemm-Logik wie position_popup in app.rs.
* - X11: Muffins Focus-Stealing-Prevention verwirft das present() der App —
* ohne initialen Fokus schließt das Blur-Verstecken nie. Die Extension
* fokussiert das Popup beim Map aus dem Compositor heraus (platziert wird
* client-seitig über die SNI-Activate-Koordinaten).
*/
const Meta = imports.gi.Meta;
const WIN_TITLE = 'ai-control-popup';
let mapId = 0;
function _place(win) {
const [px, py] = global.get_pointer();
const rect = win.get_frame_rect();
// Work-Area statt Monitor: die Panel-Struts bleiben frei, das Popup sitzt
// über der Leiste statt darauf.
const wa = win.get_work_area_current_monitor();
const maxX = wa.x + wa.width - rect.width;
const maxY = wa.y + wa.height - rect.height;
const x = Math.min(Math.max(px - rect.width, wa.x), Math.max(maxX, wa.x));
const y = Math.min(Math.max(py, wa.y), Math.max(maxY, wa.y));
win.move_frame(true, x, y);
}
// Beim Mappen unsichtbar schalten und vor dem nächsten Redraw positionieren —
// der erste sichtbare Frame sitzt schon am Zielort (Muster wie in der
// GNOME-Extension, dort über global.compositor.get_laters()).
function _placeBeforeRedraw(actor, win) {
actor.remove_all_transitions();
actor.opacity = 0;
Meta.later_add(Meta.LaterType.BEFORE_REDRAW, () => {
const rect = win.get_frame_rect();
// Noch keine echte Größe -> nächster Redraw, bis positionierbar.
if (!rect || rect.width <= 1) return true;
_place(win);
actor.opacity = 255;
return false;
});
}
function init(_metadata) {}
function enable() {
mapId = global.window_manager.connect('map', (_wm, actor) => {
const win = actor.meta_window;
if (!win || win.get_title() !== WIN_TITLE) return;
if (Meta.is_wayland_compositor()) {
_placeBeforeRedraw(actor, win);
} else {
win.activate(global.get_current_time());
}
});
}
function disable() {
if (mapId) {
global.window_manager.disconnect(mapId);
mapId = 0;
}
}
@@ -0,0 +1,7 @@
{
"uuid": "ai-control-popup@local",
"name": "ai-control popup placement",
"description": "ai-control-Tray-Popup: platziert es unter Wayland am Mauszeiger und gibt ihm unter X11 den initialen Fokus (Focus-Stealing-Prevention).",
"cinnamon-version": ["6.0", "6.2", "6.4", "6.6"],
"version": 1
}
+10 -3
View File
@@ -61,15 +61,21 @@ pub(crate) fn show_popup(app: &tauri::AppHandle, anchor: Anchor) {
};
let _ = w.set_position(tauri::PhysicalPosition::new(x, y));
}
// SNI (KDE/XFCE/Cinnamon): Activate liefert keine brauchbaren
// Koordinaten, aber der Zeiger sitzt beim Klick auf dem Icon.
Anchor::Cursor => {
// SNI (KDE/XFCE/Cinnamon): Activate-Koordinaten, wenn der Host welche
// mitgibt (Cinnamon); sonst Zeigerposition — der Zeiger sitzt beim
// Klick auf dem Icon. Unter Wayland liefert cursor_position (0,0).
Anchor::Click { x, y } => {
#[cfg(not(target_os = "linux"))]
let _ = (x, y);
#[cfg(target_os = "linux")]
{
// Gelöst wird die Sperre vom ersten Focused(true) im Event-Handler.
if let Some(g) = win.try_state::<PopupBlurGuard>() {
g.0.store(true, std::sync::atomic::Ordering::SeqCst);
}
if (x, y) != (0, 0) {
position_popup(&w, x, y);
} else {
match win.cursor_position() {
Ok(p) => position_popup(&w, p.x as i32, p.y as i32),
Err(_) => {
@@ -78,6 +84,7 @@ pub(crate) fn show_popup(app: &tauri::AppHandle, anchor: Anchor) {
}
}
}
}
// GNOME-Extension/KWin: der Compositor positioniert selbst.
Anchor::Managed => {}
}
+4 -4
View File
@@ -1,7 +1,7 @@
//! Eigenes StatusNotifierItem (ksni) statt Tauris libappindicator-Tray:
//! libappindicator kann nur ein Menü, wir wollen Linksklick → Popup-Fenster.
//! Activate liefert keine brauchbaren Koordinaten — der Zeiger sitzt beim
//! Klick auf dem Icon, daher Anchor::Cursor.
//! Die Activate-Koordinaten gehen als Anchor::Click durch; ob sie brauchbar
//! sind, hängt vom SNI-Host ab (Cinnamon ja, KDE 0,0).
use crate::platform::{Anchor, TrayCallbacks};
@@ -21,8 +21,8 @@ impl ksni::Tray for AiControlTray {
fn icon_pixmap(&self) -> Vec<ksni::Icon> {
self.icon.clone()
}
fn activate(&mut self, _x: i32, _y: i32) {
(self.cb.show)(&self.app, Anchor::Cursor);
fn activate(&mut self, x: i32, y: i32) {
(self.cb.show)(&self.app, Anchor::Click { x, y });
}
fn menu(&self) -> Vec<ksni::MenuItem<Self>> {
use ksni::menu::StandardItem;
+4 -3
View File
@@ -55,9 +55,10 @@ pub(crate) enum Anchor {
/// entscheidet die Plattform: Menüleiste oben → Popup darunter (macOS),
/// Taskbar unten → Popup darüber (Windows).
IconRect { rect: tauri::Rect, popup_below: bool },
/// SNI/ksni liefert keine Koordinaten — der Zeiger steht beim Klick auf dem
/// Icon (KDE/XFCE/Cinnamon).
Cursor,
/// SNI/ksni (KDE/XFCE/Cinnamon): Klick-Koordinaten aus Activate. Cinnamon
/// liefert echte Werte; wo der Host keine mitgibt (0,0), fällt app.rs auf
/// die Zeigerposition zurück — der Zeiger steht beim Klick auf dem Icon.
Click { x: i32, y: i32 },
/// Positionierung übernimmt der Compositor bzw. die Shell-Extension
/// (GNOME, KDE-Wayland/KWin).
Managed,
+4
View File
@@ -33,6 +33,8 @@
"/usr/share/gnome-shell/extensions/ai-control-popup@local/extension.js": "../gnome-shell-extension/ai-control-popup@local/extension.js",
"/usr/share/gnome-shell/extensions/ai-control-popup@local/metadata.json": "../gnome-shell-extension/ai-control-popup@local/metadata.json",
"/usr/share/gnome-shell/extensions/ai-control-popup@local/trayLinux.png": "icons/trayLinux.png",
"/usr/share/cinnamon/extensions/ai-control-popup@local/extension.js": "../cinnamon-extension/ai-control-popup@local/extension.js",
"/usr/share/cinnamon/extensions/ai-control-popup@local/metadata.json": "../cinnamon-extension/ai-control-popup@local/metadata.json",
"/usr/share/kwin/scripts/ai-control-popup/metadata.json": "../kwin-script/ai-control-popup/metadata.json",
"/usr/share/kwin/scripts/ai-control-popup/contents/code/main.js": "../kwin-script/ai-control-popup/contents/code/main.js"
}
@@ -42,6 +44,8 @@
"/usr/share/gnome-shell/extensions/ai-control-popup@local/extension.js": "../gnome-shell-extension/ai-control-popup@local/extension.js",
"/usr/share/gnome-shell/extensions/ai-control-popup@local/metadata.json": "../gnome-shell-extension/ai-control-popup@local/metadata.json",
"/usr/share/gnome-shell/extensions/ai-control-popup@local/trayLinux.png": "icons/trayLinux.png",
"/usr/share/cinnamon/extensions/ai-control-popup@local/extension.js": "../cinnamon-extension/ai-control-popup@local/extension.js",
"/usr/share/cinnamon/extensions/ai-control-popup@local/metadata.json": "../cinnamon-extension/ai-control-popup@local/metadata.json",
"/usr/share/kwin/scripts/ai-control-popup/metadata.json": "../kwin-script/ai-control-popup/metadata.json",
"/usr/share/kwin/scripts/ai-control-popup/contents/code/main.js": "../kwin-script/ai-control-popup/contents/code/main.js"
}