Single instance
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
# gKeyMapper daemon — implementation plan
|
||||
|
||||
Status: implemented (M0–M5 done, see "Implementation status")
|
||||
Scope: a native C/C++ background service that detects G1–G9 (and later M1–M3/MR
|
||||
and media keys) on the Logitech G910 and replays the mappings stored by the GUI.
|
||||
Scope: a native C/C++ background service that detects G1–G9 (and later M1–M3/MR)
|
||||
on the Logitech G910 and replays the mappings stored by the GUI. The media keys
|
||||
are not part of that: the daemon forwards them unchanged (§11, *Decided*).
|
||||
|
||||
## Implementation status
|
||||
|
||||
@@ -50,6 +51,18 @@ and media keys) on the Logitech G910 and replays the mappings stored by the GUI.
|
||||
hardware with a synthetic decoder test and a fake device driving the real loop,
|
||||
with a GUI attached to it over D-Bus; the bitmap decoding and the media
|
||||
passthrough were additionally confirmed on the real keyboard (see §9, §11).
|
||||
- **`keyhold` (added after M5)** — `G<n>=keyhold:<shortcut>` presses one step and
|
||||
keeps it down until that G key's state bit clears, i.e. for exactly as long as
|
||||
the key is held (games, push-to-talk). The release report was already there, so
|
||||
the work was the falling edge in the loop, per-G-key bookkeeping of the held
|
||||
codes (`ActionRunner::held_`, the last release wins when two G keys hold the
|
||||
same code) and `release_held()` on device loss and shutdown, so nothing can stay
|
||||
stuck. A sequence falls back to a tap. In the GUI it is the fourth Type entry
|
||||
(`Key (hold)`, token `keyhold`, dropdown index 3). Verified with the fake device
|
||||
through the real loop plus `xinput test`: a 1.5 s hold = one press and one
|
||||
release 1.5 s later (the repeated press reports fire nothing), correct release
|
||||
order (key up before its modifiers), release on device loss; X repeats a held
|
||||
key on its own (660 ms delay, then ~40 ms), exactly like a physical key.
|
||||
|
||||
## 1. Goals and non-goals
|
||||
|
||||
@@ -63,6 +76,10 @@ Goals
|
||||
|
||||
Non-goals (v1)
|
||||
- Profiles (M1–M3/MR), per-locale text typing (`typeout`), embedded scripting.
|
||||
The M keys are only deferred — they come after v1 (§11, *Decided*) — while
|
||||
`typeout` is still undecided (§11).
|
||||
- Remapping the media keys: they are forwarded unchanged, that is the decision
|
||||
(§11, *Decided*).
|
||||
- GUI redesign beyond a status indicator / reload / test button.
|
||||
|
||||
## 2. What the Python reference does (and what we keep)
|
||||
@@ -207,11 +224,16 @@ artifact so the runtime does zero parsing.
|
||||
by the mappings, `UI_DEV_CREATE`. Keep the fd for the daemon's lifetime.
|
||||
- Emit events by `write()`-ing `struct input_event` (from
|
||||
`<linux/uinput.h>` / `<linux/input-event-codes.h>` — no libevdev needed).
|
||||
- `key` mapping = shortcut grammar `ctrl+c,ctrl+v`:
|
||||
- single combo: press modifiers+key in order, remember the keys, release them
|
||||
when the keyboard's `release` event arrives (mirror the reference's
|
||||
`press_release_fifo` stack).
|
||||
- comma list: `emit_combo()` each group (press+release).
|
||||
- `key` mapping = shortcut grammar `ctrl+c,ctrl+v`: every step is pressed and
|
||||
released (a tap of ~2 ms), so a comma list replays the chords in order. (The
|
||||
original plan held a single combo down until the release event arrived; that
|
||||
became its own type, `keyhold`, so a plain `key` still fires once.)
|
||||
- `keyhold` mapping = one step, pressed on the 0 → 1 transition and released on the
|
||||
1 → 0 transition, i.e. exactly as long as the G key is held. A comma list falls
|
||||
back to a tap (holding several chords has no meaning). The held codes are tracked
|
||||
per G key, so a code that two G keys hold goes up with the last of them, and
|
||||
`release_held()` runs on device loss and on shutdown — otherwise a lost release
|
||||
report would leave a key down in the whole session.
|
||||
- `command` mapping: `fork()`; in the child `setsid()`, redirect stdio to
|
||||
`/dev/null`, `execve("/bin/sh", "-c", cmd)`. Reap via `SIGCHLD`.
|
||||
- `disabled`: no-op.
|
||||
@@ -220,7 +242,7 @@ artifact so the runtime does zero parsing.
|
||||
|
||||
### 4.6 Config
|
||||
- Parse the existing `mappings.conf`:
|
||||
`G<n>=<type>:<target>` with `type ∈ {key, command, disabled}`.
|
||||
`G<n>=<type>:<target>` with `type ∈ {key, command, disabled, keyhold}`.
|
||||
- Parse at load and on change only; the runtime holds a small pre-parsed action
|
||||
per G-key (enum + fixed target buffer / `std::string`), so no parsing in the
|
||||
hot path.
|
||||
@@ -333,12 +355,19 @@ Start-up orders:
|
||||
without widening the fixed-width rows.
|
||||
- The key-name vocabulary matches because the capture dialog already writes
|
||||
lowercase GDK key names and the canonical list lives in `shared/keys` (§6).
|
||||
- One window, and it is the one that was just started: `main()` closes a running
|
||||
instance before it becomes the primary one — over D-Bus through the `quit`
|
||||
action (`src/singleinstance.{h,cpp}`), or with `SIGTERM` when that instance
|
||||
comes from a build without the action — and `on_activate` re-presents the
|
||||
window that is already there instead of building a second one. A stale window
|
||||
therefore never stays next to a fresh one, and the daemon is not involved.
|
||||
|
||||
## 6. Code layout
|
||||
|
||||
```
|
||||
src/ # GUI (existing)
|
||||
daemonclient.{h,cpp} # D-Bus client (status/reload/signals)
|
||||
singleinstance.{h,cpp} # one GUI only: a new start closes the running instance
|
||||
daemon/ # as implemented (see §6 above)
|
||||
ipc_dbus.{h,cpp} # GDBus service (control plane)
|
||||
shared/
|
||||
@@ -462,16 +491,41 @@ only dispatches what the loop's read timeout already woke up for.
|
||||
The questions this section used to list were all settled while implementing M3–M5
|
||||
— app id/bus name (`dev.rager.gkeymapper` in M4), IPC (D-Bus with GDBus, in M4)
|
||||
and the event loop (blocking loop + `DaemonService::pump()`, in M4). Their
|
||||
answers are kept under *Resolved* below. What is genuinely still open:
|
||||
answers are kept under *Resolved* below. What is still open:
|
||||
|
||||
- **Scope beyond v1.** v1 ships `key`/`command`/`disabled` plus the media-key
|
||||
passthrough. The reference project additionally has `typeout` (type out a fixed
|
||||
text), handles the media /memory keys itself and can *remap* them: our daemon
|
||||
forwards the media keys unchanged (see *Resolved*) but ignores the memory keys
|
||||
(`11 ff 09/0a`) and the command/LED answers. `typeout` would be a pure action
|
||||
addition with no decoding involved (it needs the text typed through the active
|
||||
layout); media-key *remapping* would turn the passthrough into mappings with
|
||||
config entries and GUI rows. Not in v1 — decide when the need comes up.
|
||||
- **`typeout` (open, low priority).** The reference project types out a fixed
|
||||
text; our daemon has no such action. It would be a pure addition to the action
|
||||
layer with no decoding involved: a token in the `mappings.conf` format, a fourth
|
||||
Type entry in the GUI, the four `.po` files, and a typing path that maps the
|
||||
text to key codes through the active layout (xkbcommon is already linked).
|
||||
Nothing decided yet.
|
||||
|
||||
### Decided
|
||||
|
||||
- **The media keys stay a passthrough — they are not going to be remappable
|
||||
(2026-10-18).** Mute / volume / play / next / previous / stop keep the
|
||||
keyboard's own behaviour rather than becoming configurable rows. They are not G
|
||||
keys: the GUI has no slot for them, they are not part of the `mappings.conf`
|
||||
format, and adding them would mean a second kind of key in the format, the
|
||||
decoder, the daemon and the GUI — for keys whose job the keyboard already does.
|
||||
Remapping would not even let the passthrough go away, because those reports only
|
||||
reach the kernel through the interface the daemon holds; the passthrough stays
|
||||
in any case and is the place to hook into if this decision is ever revisited
|
||||
(`decode_media_key()` and `mediaKeys` in `daemon/device_tables.cpp`).
|
||||
- **The M1–M3 / MR keys and profiles come later (2026-10-18).** Not dropped, just
|
||||
after v1: the M keys switch profiles, so they need the profile concept and the
|
||||
reserved `SetProfile` D-Bus call first, and the device tables carry G1–G9 only.
|
||||
The README lists them under "Planned next steps".
|
||||
- **Holding a G key is its own type (`keyhold`), not an option on `key`
|
||||
(2026-10-18).** `key` keeps firing once; `keyhold` is the mapping whose keys stay
|
||||
down. Two types instead of a per-row flag keep the file format and the dropdown
|
||||
simple (one token and one entry per type) and make the intent visible in the
|
||||
file. The limits are deliberate: a *sequence* cannot be held (it is replayed as
|
||||
a tap, with a log line), because holding several chords at once has no meaning,
|
||||
and a modifier *alone* (`keyhold:shift` for a push-to-talk on Shift) is still not
|
||||
expressible — `parse_shortcut()` requires a real key and the capture dialog
|
||||
ignores modifier-only presses, so that would need a small change in both places.
|
||||
A single key or modifier+key (`keyhold:space`, `keyhold:ctrl+w`) works.
|
||||
|
||||
### Resolved
|
||||
|
||||
@@ -572,8 +626,8 @@ answers are kept under *Resolved* below. What is genuinely still open:
|
||||
regression. Verified live: five volume-up and three volume-down presses moved
|
||||
the sink by exactly two 2.5 % steps, and `media -> mute` / `media -> stop` /
|
||||
`G6 -> key:` appeared in the daemon log.
|
||||
Remapping the media keys instead of forwarding them stays open (§11, "Scope
|
||||
beyond v1").
|
||||
Remapping them instead of forwarding them is decided against (see *Decided*
|
||||
above).
|
||||
- **`command` environment (checked in M5).** Nothing to do: the daemon runs as a
|
||||
**user** service, and the user manager's environment already carries
|
||||
`DISPLAY=:0`, `XAUTHORITY` and a sane `PATH` on this machine (verified with
|
||||
|
||||
Reference in New Issue
Block a user