Skip to content
Open
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 Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions crates/openlogi-core/src/brand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ pub const HELP_URL: &str = "https://github.com/AprilNEA/OpenLogi#readme";
/// The "latest release" page.
pub const RELEASES_URL: &str = "https://github.com/AprilNEA/OpenLogi/releases/latest";

/// The application identifier: the Wayland xdg-toplevel `app_id` (and X11
/// `WM_CLASS`) the GUI advertises, the root of the macOS bundle-id family
/// (`org.openlogi.agent`, `org.openlogi.openlogi.dev`), and the value the Linux
/// `.desktop` file pins as `StartupWMClass`. Defined once here so the window the
/// compositor sees, the launcher that groups it, and the frontmost backend that
/// self-identifies OpenLogi can never disagree. The `.desktop` file carries its
/// own literal copy (it can't reference Rust) — keep the two in sync.
pub const APP_ID: &str = "org.openlogi.openlogi";

/// The release page for a specific version tag (e.g. the running build).
#[must_use]
pub fn release_tag_url(version: &str) -> String {
Expand Down
9 changes: 8 additions & 1 deletion crates/openlogi-gui/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ use gpui::{
WindowBounds, WindowOptions, px,
};
use gpui_component::{ActiveTheme, Root, Theme, ThemeMode};
use openlogi_core::brand::DeeplinkCommand;
use openlogi_core::brand::{APP_ID, DeeplinkCommand};
use openlogi_core::config::Config;
use openlogi_core::device::{DeviceInventory, DeviceModelInfo};
use tracing::{info, warn};
Expand Down Expand Up @@ -547,6 +547,13 @@ fn main_window_options(cx: &mut gpui::App) -> WindowOptions {
let bounds = Bounds::centered(None, Size::new(px(1100.), px(750.)), cx);
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
// Advertise a Wayland xdg-toplevel app_id (and X11 WM_CLASS). Without it
// the window ships no app_id, so GNOME's `get_wm_class()` returns empty
// and our own `gnome_shell` frontmost backend reports OpenLogi as `None`
// (and the dash can't group the window under its launcher icon). The id
// is the shared `brand::APP_ID`, matching the desktop file's
// `StartupWMClass` and the macOS bundle-id family.
app_id: Some(APP_ID.into()),
// Min height keeps the buttons tab's mouse model above its scale floor
// (`MODEL_MIN_H` + the chrome/padding reserve) so its side labels never
// overlap; below this the model can't shrink further without crowding.
Expand Down
3 changes: 3 additions & 0 deletions crates/openlogi-hook/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ tracing = { workspace = true }
[target.'cfg(target_os = "linux")'.dependencies]
evdev = "0.13"
libc = "0.2"
wayland-client = "0.31"
wayland-protocols-wlr = { version = "0.3", features = ["client"] }
x11rb = "0.13"
zbus = "5"

[target.'cfg(target_os = "linux")'.dev-dependencies]
ctrlc = "3"
Expand Down
59 changes: 59 additions & 0 deletions crates/openlogi-hook/gnome-shell-extension/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# OpenLogi Frontmost Window — GNOME Shell extension

GNOME (Mutter) does not let ordinary clients see which window is focused on
Wayland, and it implements neither `wlr-foreign-toplevel` nor a focused-window
portal. This minimal extension bridges that gap: it exports the WM_CLASS of the
focused window over D-Bus so OpenLogi's `gnome-shell` frontmost backend can
drive per-app mouse-profile switching.

It reads only `global.display.focus_window.get_wm_class()`. No titles, no window
contents, no input, no UI.

## D-Bus surface

- name: `org.openlogi.Frontmost`
- path: `/org/openlogi/Frontmost`
- method: `GetFocusedWmClass() -> s` (empty string when nothing is focused)

## Install

```sh
UUID=openlogi-frontmost@openlogi.dev
DEST="$HOME/.local/share/gnome-shell/extensions/$UUID"
mkdir -p "$DEST"
cp metadata.json extension.js "$DEST"/
```

On Wayland the shell cannot be reloaded in place, so **log out and back in** to
let GNOME pick up the newly added extension, then enable it:

```sh
gnome-extensions enable "$UUID"
gnome-extensions info "$UUID" # State should be ACTIVE
```

## Verify

```sh
# Introspect the service:
busctl --user introspect org.openlogi.Frontmost /org/openlogi/Frontmost

# Focus a window, then query it:
gdbus call --session \
-d org.openlogi.Frontmost \
-o /org/openlogi/Frontmost \
-m org.openlogi.Frontmost.GetFocusedWmClass
```

If `gdbus call` prints the focused window's WM_CLASS, OpenLogi's GNOME backend
will pick it up automatically the next time the hook starts.

## Notes

- The `shell-version` list in `metadata.json` covers GNOME 45–50. Newer GNOME
releases may need an added entry; the API used here (`Gio.DBusExportedObject`,
`global.display.focus_window`, `Meta.Window.get_wm_class`) has been stable
across these versions.
- The extension name/UUID and the D-Bus name (`org.openlogi.*`) are placeholders
that should track the project's namespace; if they change, update the matching
constants in `crates/openlogi-hook/src/linux/gnome_shell.rs`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// OpenLogi Frontmost Window — GNOME Shell extension.
//
// Exports a tiny D-Bus service that returns the WM_CLASS of the currently
// focused window. OpenLogi's `gnome_shell` frontmost backend polls this to
// drive per-app mouse-profile switching on GNOME Wayland, where the focused
// window is otherwise not visible to ordinary clients.
//
// It reads only `global.display.focus_window.get_wm_class()` — no titles, no
// window contents, no input. ESM module style; targets GNOME Shell 45+.

import Gio from 'gi://Gio';
import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';

const DBUS_NAME = 'org.openlogi.Frontmost';
const DBUS_PATH = '/org/openlogi/Frontmost';
const DBUS_INTERFACE = `
<node>
<interface name="org.openlogi.Frontmost">
<method name="GetFocusedWmClass">
<arg type="s" direction="out" name="wmClass"/>
</method>
</interface>
</node>`;

export default class OpenLogiFrontmostExtension extends Extension {
enable() {
this._dbus = Gio.DBusExportedObject.wrapJSObject(DBUS_INTERFACE, this);
this._dbus.export(Gio.DBus.session, DBUS_PATH);
this._nameId = Gio.bus_own_name_on_connection(
Gio.DBus.session,
DBUS_NAME,
Gio.BusNameOwnerFlags.NONE,
null,
null);
}

disable() {
if (this._nameId) {
Gio.bus_unown_name(this._nameId);
this._nameId = 0;
}
if (this._dbus) {
this._dbus.unexport();
this._dbus = null;
}
}

// D-Bus method org.openlogi.Frontmost.GetFocusedWmClass.
GetFocusedWmClass() {
const win = global.display.focus_window;
if (!win)
return '';
return win.get_wm_class() || '';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"uuid": "openlogi-frontmost@openlogi.dev",
"name": "OpenLogi Frontmost Window",
"description": "Exposes the focused window's WM_CLASS over D-Bus so OpenLogi can switch per-app mouse profiles on GNOME Wayland. Read-only; no UI, no window contents.",
"shell-version": ["45", "46", "47", "48", "49", "50"],
"url": "https://github.com/AprilNEA/OpenLogi",
"version": 1
}
Loading