Autostart option added

This commit is contained in:
2026-10-18 18:26:06 +02:00
parent 74503c27dd
commit 95f101c0c9
22 changed files with 2987 additions and 80 deletions

138
AGENTS.md
View File

@@ -40,7 +40,11 @@ live in the daemon, not the GUI. Code is split across `src/` (GUI), `shared/`
(the background service). Shared API lives in `namespace gkeymapper`.
`shared/mapping` owns the mappings file format, `shared/keys` the canonical
key-name table (lowercase GDK names -> Linux `KEY_*`), `shared/ipc_ids.h` the
app id, the version and the D-Bus names both sides use.
app id, the version and the D-Bus names both sides use. The GUI side is
`main.cpp` (window, actions) plus `keyrow.h`, `mappings`, `capturedialog`,
`daemonclient`, `singleinstance`, `trayicon` (StatusNotifierItem + dbusmenu),
`settings` (`gui.conf`), `autostart` (the session's `.desktop` entry), `help`
and `optionsdialog`.
- The Makefile globs sources: `make` builds the GUI (`src/*.cpp` + `shared/*.cpp`
+ the generated `build/gresources.c`, see Assets) **and** the daemon
(`daemon/*.cpp` + `shared/*.cpp`, needs `libusb-1.0-0-dev`,
@@ -95,6 +99,12 @@ live in the daemon, not the GUI. Code is split across `src/` (GUI), `shared/`
not an error), so a save from a fresh window rewrites the file instead of
emptying it. App id:
`dev.rager.gkeymapper` (`ipc::kAppId`); the About dialog uses `ipc::kVersion`.
- The About dialog is a stock `GtkAboutDialog`, but its texts get switched off as
selectable before it is mapped (`make_labels_unselectable`): GTK hands the
keyboard focus to the program name and a focused selectable `GtkLabel` selects
its whole text, so the dialog opened with the name drawn as a blue block. Do
not "clean that up" back — as long as the labels are selectable there is always
one that grabs the focus and shows up marked.
- GUI ↔ daemon is D-Bus on the session bus, GDBus (GIO) on both sides:
`src/daemonclient.{h,cpp}` is the client, `daemon/ipc_dbus.{h,cpp}` the
service. The daemon owns `dev.rager.gkeymapper.Daemon`, **not** the plain app
@@ -121,6 +131,71 @@ live in the daemon, not the GUI. Code is split across `src/` (GUI), `shared/`
`DaemonClient` uses async calls and signal subscriptions, which arrive on the
GTK main loop, and it follows `NameOwnerChanged` so the footer reacts when the
daemon starts or stops.
- The tray icon (`src/trayicon.{h,cpp}`) is a `StatusNotifierItem` plus a
`com.canonical.dbusmenu` menu, spoken over GIO — **not** through libxapp or
libappindicator: both are GTK3 libraries (measured: `ldd libxapp.so.1` →
`libgtk-3.so.0`, deps `libgtk-3-0` + `libdbusmenu-gtk3-4`), and loading GTK3
next to GTK4 in one process is not supported. GTK4 also has no tray API at all
(`GtkStatusIcon` is GTK3). This way the feature adds no build dependency. The
icon travels as `IconPixmap` (ARGB32, network byte order): the panel is another
process and cannot resolve the icons compiled into the binary via GResource.
- In the dbusmenu layout the **children array holds full nodes**
(`(ia{sv}av)` each), never bare ids: `libdbusmenu-glib`, which the panel's
watcher uses, walks them and aborts the whole process on anything else
(`g_variant_serialised_n_children: code should not be reached`) — that crashed
`xapp-sn-watcher` on every registration until it was fixed. Do not "simplify"
this back to ids.
- The panel's watcher sends its clicks batched as `EventGroup` (a list of
`(isvu)`), not as single `Event` calls: both must be handled, `EventGroup` is the
path a real click takes (verified with `dbus-monitor`). Both entries go through
one `handle_event(id, eventId)`, which only reacts to `"clicked"` on the Show and
Close ids.
- Two watcher names exist (`org.kde.StatusNotifierWatcher` and the older
`org.x.StatusNotifierWatcher`; on Mint both belong to `xapp-sn-watcher`). Both are
watched and the item registers **once**, preferring the `org.kde.` name — a
second registration would add a second icon. `ready()` (registered with a
watcher that still owns the name) is what gates hiding the window.
- The GUI's own preferences (minimize to tray, language) live in
`~/.config/gKeyMapper/gui.conf` (GKeyFile, `src/settings.{h,cpp}`), deliberately
**not** in `mappings.conf`, so a mapping file stays a mapping file and can be
exported/imported without dragging a window preference along.
- Autostart is **not** a setting in `gui.conf`: it is the presence of the entry
the session reads at login, `~/.config/autostart/gkeymapper.desktop`
(`src/autostart.{h,cpp}`). The switch writes that file or removes it, so there
is no flag that could disagree with what the session actually does;
`autostart_enabled()` treats `Hidden=true` and `X-GNOME-Autostart-enabled=false`
as off, because the desktop's own startup tool disables an entry instead of
deleting it. The `Exec` line is the binary that is running now
(`g_file_read_link("/proc/self/exe")`, quoted, falls back to the name from
`PATH`), so an enabled switch starts the build the user was looking at, and the
`Comment` reuses the About text (no second sentence for translators). Only the
**window** is started; the daemon is a systemd user service and stays untouched
— the GUI is a convenience, the daemon is the part that has to run.
- `Minimize to tray` hides the window on close (`"close-request"` handler) and on
minimize. GTK4 has **no** minimize signal and `GdkToplevel` exposes no
properties, so `gdk_toplevel_get_state()` is polled every 250 ms while the
option is on. Hiding happens only while `tray.ready()` — without a hosted icon
there would be no way back to the window, so a close then quits as usual. The
tray menu's Show uses `gtk_widget_set_visible` + `gtk_window_present` (which also
restores a minimized window).
- Help, options and the tray menu are separate windows/files instead of a
`GMenu`: a `GMenu` cannot host a switch, and the options window is deliberately
non-modal so the footer keeps updating while it is open. The daemon data behind
both is one `DaemonView` (last status reply + last key); the footer shows it in
one line, the options window in full.
- The language is a setting in `gui.conf`, not something that is guessed: gettext
gets it through `LANGUAGE` plus a fresh `setlocale` (`apply_gui_language`), and
that is also why the English catalog is an identity one — an explicit `en` has
to work on a German desktop. Widgets keep the strings they were built with, so
switching rebuilds the windows (`rebuild_ui_idle` in `main.cpp`): the main
window is destroyed and built again, options and help are closed and reopened
if they were open, and the tray menu is announced to the panel with
`LayoutUpdated` (it mirrors the menu once, so the revision is bumped). The
application is held across the gap — destroying the last window would end the
process. The rows go over with `collect_mappings`/`apply_mappings`, so a
language switch keeps unsaved edits; never rebuild the window without that
step. Rebuilding also means the poll and the tray are **not** started in
`build_ui()`: they are one-time, in `on_activate`.
- `DaemonService::pump()` dispatches messages once per daemon loop iteration, so
a control call is answered within one read timeout (≤500 ms) and outgoing
signals are flushed then too. Keep it off the keypress path.
@@ -361,3 +436,64 @@ is up. Three notes for tests:
sees.
- `XTest`-synthesized clicks need the window mapped and focused; the `Save`
button is `push button` + name `Mappings speichern` in the German locale.
The tray icon, the help and the options window need neither hardware nor a click:
- Is the item registered? The panel watcher lists it by name:
`gdbus call --session --dest org.kde.StatusNotifierWatcher --object-path
/StatusNotifierWatcher --method org.freedesktop.DBus.Properties.Get
org.kde.StatusNotifierWatcher RegisteredStatusNotifierItems` — the entry is
`org.kde.StatusNotifierItem-<pid>-1/StatusNotifierItem`. A second entry (same
pid, two registrations) means the "register once" rule broke.
- Its menu is readable without the panel:
`gdbus call --session --dest org.kde.StatusNotifierItem-<pid>-1 --object-path
/MenuBar --method com.canonical.dbusmenu.GetLayout 0 -1` returns the root node
with the two children. Drive a click the way the panel does — **batched**, as
`EventGroup`, because that is the path a real click takes:
`gdbus call … --method com.canonical.dbusmenu.EventGroup
"[(1, 'clicked', <'', >, 0)]"` shows the window, id `2` quits the program.
A single `Event` call works too but is not what the watcher sends.
- The watcher is fragile by construction: it walks the layout it mirrors, so a
malformed node (bare ids instead of full nodes) aborts it. After a change to
the layout, check it survived (`pgrep -a xapp-sn-watcher`) and that no new
`coredumpctl list xapp-sn-watcher` entry appeared.
- Minimize-to-tray is testable without the WM: send the `WM_CHANGE_STATE` client
message (`IconicState = 3`) to the window id from `wmctrl -l` with python-xlib
(`root.send_event(..., SubstructureRedirect|SubstructureNotify)`) — `wmctrl -b
add,hidden` does **not** work, the WM ignores a client-set state. With the
option on, the window disappears from `wmctrl -l` within ~250 ms (the poll) and
the process stays; `Show` from the menu brings it back. With the option off, a
close quits the process.
- The help and options windows are plain `GtkWindow`s in the same process, so
they are reachable over their app actions:
`gdbus call --session --dest dev.rager.gkeymapper --object-path
/dev/rager/gkeymapper --method org.gtk.Actions.Activate help "[]" "{}"`
(same with `options`), then screenshot with
`import -window $(wmctrl -l | grep -i Hilfe | awk '{print $1}') …`. The options
switch can be clicked with `xtest` at `xwininfo`'s absolute position plus its
offset in the window (~422, 56), and the result is visible in `gui.conf` and in
the close behavior right away, no restart.
- The language dropdown sits at about (82, 192) from the options window's
top-left, and its popup entries follow about 25 px apart (the list opens below
the widget). Check the result in three places: `gui.conf` (`language=de`),
the rebuilt window, and the tray menu (`GetLayout` then says "Anzeigen").
Writing `language=en` into `gui.conf` and starting the GUI covers the startup
path without a click; a `language=` (empty value) is "follow the session".
One pitfall while clicking: the options window is transient for the main
window, so it always stays above it — clicks meant for the main window hit the
options window unless it is closed first.
- The autostart switch needs no click to read and one click to write, because the
`.desktop` entry *is* the state: put `$HOME/.config/autostart/gkeymapper.desktop`
there (or delete it), close the options window (`wmctrl -i -c <id>`) and
activate the `options` action again — the switch shows what the file says. A
single `xtest` click below the tray switch (about (428, 149) from the window's
top-left; find it by looking for the accent blue, the tray switch is grey when
`gui.conf` has no `minimizeToTray`) writes or removes the file right away.
**Use a throwaway `HOME`** — with the real one the next login would start the
repo build. `src/autostart.cpp` also compiles on its own (glib only, plus
`-DGETTEXT_PACKAGE`), which is the fastest way to cover the round trip and the
two "disabled" spellings (`Hidden=true`,
`X-GNOME-Autostart-enabled=false`).
- A daemon that stops and starts again (`systemctl --user stop/start
gkeymapper-daemon`) is the cheapest way to see the options window update while
it is open (the rows change, the switches must not).