Autostart option added
This commit is contained in:
138
AGENTS.md
138
AGENTS.md
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user