diff --git a/README.md b/README.md
index 0d1168f..151ddec 100644
--- a/README.md
+++ b/README.md
@@ -45,11 +45,15 @@
I left Windows.
-It went fine. Games ran. My editor ran. Everything I cared about had a Linux version or a good replacement. Then I picked up the little AliExpress macro pad on my desk, the one with twelve keys and two knobs, and realised the only way to change what its keys do was a Windows program called `MINI_KEYBOARD.exe` that looks like it was designed during a power cut.
+It went fine. Games ran. My editor ran. Everything I cared about had a Linux version or a good replacement. Then I picked up the little AliExpress macro pad on my desk, the one with twelve keys and two knobs, and remembered that the software it came with is a Windows program called `MINI_KEYBOARD.exe` that looks like it was designed during a power cut.
People online were running **entire Windows virtual machines** just to change one key. Others kept a Windows partition alive for it. That is a lot of Windows for a thing that costs less than a pizza.
-So I sat down with the vendor's software, the Linux kernel's USB sniffer, and a lot of stubbornness, and worked out exactly what bytes that program sends to the pad. Then I built the program I actually wanted:
+So I sat down with the vendor's software, the Linux kernel's USB sniffer, and a lot of stubbornness, and worked out exactly what bytes that program sends to the pad.
+
+Once it worked, I found out I wasn't the first. **[ch57x-keyboard-tool](https://github.com/kriomant/ch57x-keyboard-tool)** has handled these pads from the command line since 2023, on Linux, macOS and Windows, and it's excellent. If you're happy editing a YAML file and running a command, go and use it.
+
+But I wanted something a friend on their first week of Linux could use without ever seeing a config file. So this is the other half, the program I actually wanted:
- **It looks like your pad.** You click on a drawing of the real thing, not a settings list.
- **It's honest.** The pad can't tell anyone what's on it, so the app never pretends to know.
@@ -78,7 +82,7 @@ Bus 001 Device 011: ID 1189:8840 Acer Communications & Multimedia USB Composite
The important bit is **`1189:8840`**. Don't worry about the "Acer" part: it isn't made by Acer, these pads just borrow that ID.
> [!NOTE]
-> **Different number, like `1189:8890` or `1189:8830`?** That's a sibling of this pad with a different layout. The app will spot it and tell you it isn't supported yet, rather than risk sending it the wrong thing. [You can help add it!](#-other-pads-in-this-family)
+> **Different number, like `1189:8890` or `1189:8842`?** That's a sibling of this pad with a different layout. MacroPad will spot it and tell you it isn't supported yet, rather than risk sending it the wrong thing. [ch57x-keyboard-tool](https://github.com/kriomant/ch57x-keyboard-tool) supports several of them today, and [you can help add yours here](#-other-pads-in-this-family).
---
@@ -89,6 +93,7 @@ The important bit is **`1189:8840`**. Don't worry about the "Acer" part: it isn'
| 🎹 **Click the drawing** | The window shows your pad as it really is. Click a key, or any part of a knob (turn left, press, turn right). |
| ⌨️ **Keyboard shortcuts** | Anything like `Ctrl+C`, `Ctrl+Shift+T`, `Meta+Ctrl+Right`, `F5`. Type it or press **Record** and just do the combo. |
| 🎵 **Media keys** | Play/pause, next, previous, stop, volume, mute, **screen brightness**, calculator, email, browser, my computer. |
+| 🚫 **Nothing** | Switch a key off completely, so a stray press does nothing. |
| 🔄 **Rotate view** | Stand your pad up or lay it flat. The drawing turns to match. |
| 🧠 **Remembers what it wrote** | The pad can't be read back, so the app keeps its own notes and marks anything it doesn't know. |
| 🩺 **Tells you what's wrong** | Unplugged? No permission? Wrong pad? You get a plain explanation and the exact fix, with a copy button. |
@@ -107,6 +112,25 @@ The important bit is **`1189:8840`**. Don't worry about the "Acer" part: it isn'
The tiny `0x04` in each corner is that key's **action byte**, the number the pad uses for it internally. You can ignore it forever. It's there for anyone [adding support for another pad](#-other-pads-in-this-family).
+### MacroPad or ch57x-keyboard-tool?
+
+Both write straight to the pad's own memory, so you can even use both. Pick whichever suits you:
+
+| | MacroPad | [ch57x-keyboard-tool](https://github.com/kriomant/ch57x-keyboard-tool) |
+|---|---|---|
+| How you use it | Click a drawing of your pad | Write a YAML file, run a command |
+| Pads | `1189:8840` (12 keys, 2 knobs) | `1189:8840`, `8842`, `8890`, several layouts |
+| Systems | Linux | Linux, macOS, Windows |
+| Shortcuts and media keys | Yes | Yes |
+| Key sequences with delays | Not yet | Yes |
+| Mouse actions, LED modes, extra layers | Not yet | Yes |
+| Record a shortcut by pressing it | Yes | No |
+| Install and permissions | One line does both | AUR, a download or `cargo install`; rule by hand |
+
+**Short version:** new to Linux, or just want to click things? MacroPad. Want every feature, a different pad, or a config file you can keep in git? ch57x-keyboard-tool.
+
+Other people have built GUIs for these pads too, mostly for the smaller 3 and 6 key models, including one that runs in a web browser. You'll find them under the [ch57x topic on GitHub](https://github.com/topics/ch57x). The more of these there are, the fewer people are stuck running Windows for a macro pad.
+
---
## 🚀 Install it (the whole world is counting on you)
@@ -220,6 +244,14 @@ Click **Media key** and pick from the list. Volume and brightness on the knobs i
+### Or make it do nothing
+
+Bound something you don't want any more? Click **Nothing**, then write it. The key goes quiet: pressing it does nothing at all. Keys set this way show **Nothing** in grey on the drawing.
+
+
+
+
+
### 3. Write it to the pad
Click **Write to pad**. Changed a few things? The orange **Write N changes** button at the bottom right sends them all at once.
@@ -386,9 +418,12 @@ A few ideas for KDE Plasma, where these are the default shortcuts:
## 🧬 Other pads in this family
-This app is **confirmed on `1189:8840`**, the 12 key, 2 knob pad. It's the only one anyone has captured so far.
+This app is **confirmed on `1189:8840`**, the 12 key, 2 knob pad. It's the only one it knows so far.
-Siblings come in other sizes (the original Windows project was built around a 3 key, 1 knob version), and several share vendor ID `1189`. The original Windows project this work builds on lists `1189:8890`, `8830`, `8831`, `8832`, `8897` and `8874`. **They don't all speak the same language.** This pad alone turned out to differ from the documented protocol in four separate places (see [the findings](#what-we-found)), and none of those could have been guessed. That's why the app won't write to a pad it doesn't know.
+> [!TIP]
+> **Have a different pad right now?** [ch57x-keyboard-tool](https://github.com/kriomant/ch57x-keyboard-tool) supports `1189:8840`, `1189:8842` and `1189:8890` in several layouts (3, 6, 9, 12 and 15 keys) from the command line, today.
+
+Siblings come in other sizes, and several share vendor ID `1189`. **They don't all speak the same language:** ch57x-keyboard-tool has separate code for different families, and rOzzy1987's Windows project lists yet more IDs (`8830`, `8831`, `8832`, `8897`, `8874`) with their own formats. That's why MacroPad won't write to a pad it doesn't know.
@@ -396,7 +431,7 @@ Siblings come in other sizes (the original Windows project was built around a 3
### You can add yours
-If you have a different pad, you're exactly who this project needs, and it's the same process that cracked this one. You'll need the vendor software (`MINI_KEYBOARD.exe`) and Wine.
+If you'd like your pad in MacroPad too, you're exactly who this project needs. It's the same process used for this one. You'll need the vendor software (`MINI_KEYBOARD.exe`) and Wine.
#### Capture what the vendor software sends
@@ -427,7 +462,7 @@ If you have a different pad, you're exactly who this project needs, and it's the
## 🪟 What about Windows?
-Windows already has the vendor app, ugly as it is. **Linux is the gap, so Linux comes first.**
+Windows already has the vendor app, ugly as it is, and [ch57x-keyboard-tool](https://github.com/kriomant/ch57x-keyboard-tool) runs there too. **A friendly app on Linux was the gap, so Linux comes first.**
That said, the app is built on Qt, which runs on Windows and macOS too. The only Linux specific parts are the small pieces that talk to USB and check permissions. Swapping it for a cross platform one is on the [roadmap](#-roadmap), and once that's tested on real hardware, one app will work everywhere. If you'd like to help test on Windows, open an issue.
@@ -441,7 +476,9 @@ That said, the app is built on Qt, which runs on Windows and macOS too. The only
1. **Probe.** `macropad-probe.py` walks `/sys/class/hidraw` and decodes each HID report descriptor. The pad shows up as two interfaces: a normal keyboard (`Generic Desktop`) and a vendor defined one (usage page `0xff00`) with a 64 byte input and output report, ID 3. That's the config channel.
2. **Guess.** The upstream project documents two protocols, "Legacy" and "Extended". Both were tried. The pad accepted the reports and then quietly ignored them. Educated guessing had run out.
3. **Watch.** The breakthrough was noticing `MINI_KEYBOARD.exe` runs under Wine. Linux's `usbmon` can record every USB transfer, so `macropad-capture.py` watched the real software write a single key. Twenty minutes of watching beat hours of guessing.
-4. **Repeat.** Separate captures of the knobs and of media keys each turned up something new. Every packet from every capture is now replayed by the test suite.
+4. **Repeat.** Separate captures of the knobs and of media keys filled in the rest. Every packet from every capture is now replayed by the test suite.
+
+Lesson learned the hard way: [ch57x-keyboard-tool](https://github.com/kriomant/ch57x-keyboard-tool) had already worked out most of this, and its source is the best reference for these pads. Search before you sniff.
@@ -456,7 +493,7 @@ Every binding is two 65 byte writes to `/dev/hidrawN`: a config report, then a c
│ │ │ │ │ │ │ │ └───── modifier bitmask
│ │ │ │ │ │ │ └──────── count
│ │ │ │ │ │ └───────────── unused
-│ │ │ │ │ └──────────────────── delay, little endian (not yet verified)
+│ │ │ │ │ └──────────────────── delay? (unverified, see below)
│ │ │ │ └────────────────────────── type: 1 keys, 2 media, 3 mouse, 8 LED
│ │ │ └───────────────────────────── layer, counted from 1
│ │ └──────────────────────────────── action: which control
@@ -484,20 +521,26 @@ Every binding is two 65 byte writes to `/dev/hidrawN`: a config report, then a c
The vendor app sometimes sends several configs and one commit at the end. Both styles work. This app commits after every binding, so it always knows exactly which ones landed.
+**Delays** are the open question. The byte field above comes from rOzzy1987's format and hasn't been seen on this pad. ch57x-keyboard-tool sends a delay as a separate report of type `5` instead, which is probably right. That's why sequences are switched off in the app until a capture settles it.
+
-What we found (and what upstream had different)
+Three sources, compared
-| | Upstream "Extended" | 1189:8840 |
-|---|---|---|
-| Magic byte | `0xFE` | **`0xFD`** |
-| First layer | `0` | **`1`** (layer 0 is accepted, then silently thrown away) |
-| First knob action | `13` (derived) | **`0x10`** (keys get 1 to 15, knobs start on the next hex boundary) |
-| Media packing | leading zero, usage at offset 12, count from payload | **count `2`, usage at offset 11** |
+What each source sends to a `1189:8840`. The pad accepts the ch57x-keyboard-tool style and the vendor style, so it's evidently tolerant. Nothing here is a new discovery about the hardware; the last column is simply what the vendor's own software does, captured independently.
-None of these were guessable. Each one was found by watching the real software.
+| | rOzzy1987 "Extended" | [ch57x-keyboard-tool](https://github.com/kriomant/ch57x-keyboard-tool) | Vendor app (captured) |
+|---|---|---|---|
+| Byte 1 of a config report | `0xFE` | `0xFE` | **`0xFD`** |
+| Layers counted from | `0` | `1` | `1` |
+| First knob action | `13` | `0x10` | `0x10` |
+| Media: count, usage offset | from payload, 12 | `0`, 11 | **`2`**, 11 |
+| Delay | bytes 5 and 6 | separate report, type `5` | not captured yet |
+| Finishing a write | flash command | `aa aa`, `fd fe ff`, `aa aa` | `fd fe ff` |
+
+rOzzy1987's format is for different product IDs, which is why porting it to this pad failed until the capture.
@@ -604,7 +647,8 @@ tests/ real captures and the tests that replay them
- [x] Both knobs, all three actions each
- [x] Upright and flat views
- [x] One line installer for any distro
-- [ ] **Key sequences with delays** (waiting on one more capture to verify the format)
+- [ ] **Key sequences with delays** (ch57x-keyboard-tool shows the likely format; one capture to confirm)
+- [ ] Layers 2 and 3, and LED modes
- [ ] AUR package, so Arch users can install it like anything else
- [ ] Knob press to toggle brightness between 0% and 100%
- [ ] Save and switch between named profiles (gaming, editing, streaming)
@@ -615,7 +659,8 @@ tests/ real captures and the tests that replay them
## 🙏 Credits
-- **[rOzzy1987/MacroPad](https://github.com/rOzzy1987/MacroPad)**, whose Windows project did the original reverse engineering this whole thing stands on. Go give it a star.
+- **[kriomant/ch57x-keyboard-tool](https://github.com/kriomant/ch57x-keyboard-tool)**, the MIT licensed command line tool that has supported these pads since 2023. It does more than MacroPad does today (layers, sequences, mouse, LEDs, more pads, every OS), and its source is the best reference there is. Go give it a star.
+- **[rOzzy1987/MacroPad](https://github.com/rOzzy1987/MacroPad)**, whose Windows project this app's protocol code started from.
- Everyone on forums who documented running a whole VM to change a key. Your suffering was noted.
## ⚖️ Licence
diff --git a/docs/images/flat-view.png b/docs/images/flat-view.png
index a0ee2a1..42d431e 100644
Binary files a/docs/images/flat-view.png and b/docs/images/flat-view.png differ
diff --git a/docs/images/hero.png b/docs/images/hero.png
index 45a45b6..31b0a78 100644
Binary files a/docs/images/hero.png and b/docs/images/hero.png differ
diff --git a/docs/images/permission.png b/docs/images/permission.png
index 172654b..5b890b9 100644
Binary files a/docs/images/permission.png and b/docs/images/permission.png differ
diff --git a/docs/images/step-media.png b/docs/images/step-media.png
index 87cfa24..238c16d 100644
Binary files a/docs/images/step-media.png and b/docs/images/step-media.png differ
diff --git a/docs/images/step-nothing.png b/docs/images/step-nothing.png
new file mode 100644
index 0000000..b2d2681
Binary files /dev/null and b/docs/images/step-nothing.png differ
diff --git a/docs/images/step-pending.png b/docs/images/step-pending.png
index 1fa1805..c458fc1 100644
Binary files a/docs/images/step-pending.png and b/docs/images/step-pending.png differ
diff --git a/docs/images/step-record.png b/docs/images/step-record.png
index 235c7c3..01c0c80 100644
Binary files a/docs/images/step-record.png and b/docs/images/step-record.png differ
diff --git a/docs/images/step-type.png b/docs/images/step-type.png
index dcf86ae..a6546c9 100644
Binary files a/docs/images/step-type.png and b/docs/images/step-type.png differ
diff --git a/docs/images/step-written.png b/docs/images/step-written.png
index 1026bf2..cd4939c 100644
Binary files a/docs/images/step-written.png and b/docs/images/step-written.png differ
diff --git a/docs/images/unsupported.png b/docs/images/unsupported.png
index d09881c..bd4d6d9 100644
Binary files a/docs/images/unsupported.png and b/docs/images/unsupported.png differ
diff --git a/docs/make_screenshots.py b/docs/make_screenshots.py
index f6b389e..a49a140 100644
--- a/docs/make_screenshots.py
+++ b/docs/make_screenshots.py
@@ -163,6 +163,16 @@ def scene_media():
save(w, "step-media")
+def scene_nothing():
+ b = dict(DEMO)
+ b["key1"] = B(none=True)
+ w = window(bindings=b)
+ w.desired["key5"] = B(none=True)
+ select(w, "key5")
+ w._refresh()
+ save(w, "step-nothing")
+
+
def scene_pending():
w = window(bindings={k: v for k, v in DEMO.items()
if k not in ("key2", "key6", "key10")})
@@ -218,8 +228,9 @@ def scene_unsupported():
detail=["So far only 1189:8840 (12 keys, 2 dials) has been worked out. "
"Pads in this family differ in layout and protocol, so writing to "
"yours with the wrong one could fail or scramble it. The app won't try.",
- "You can help add it: the README's \"Other pads\" section shows how "
- "to capture what the vendor software sends."])
+ "ch57x-keyboard-tool, a command line tool, supports several of these "
+ "pads today. To get yours into this app, the README's \"Other pads\" "
+ "section shows how to capture what the vendor software sends."])
w = window(diag=d)
save(w, "unsupported")
diff --git a/install.sh b/install.sh
index 67404ab..a0b2afe 100644
--- a/install.sh
+++ b/install.sh
@@ -158,7 +158,7 @@ Name=MacroPad
GenericName=Macro pad configurator
Comment=Change what your macro pad's keys and dials do
Exec=$BIN/macropad
-Icon=macropad
+Icon=$ICON
Terminal=false
Categories=Utility;Settings;HardwareSettings;
Keywords=macro;keyboard;keypad;knob;dial;shortcut;hid;
@@ -166,6 +166,8 @@ StartupWMClass=macropad
EOF
command -v update-desktop-database >/dev/null && update-desktop-database -q "$(dirname "$DESKTOP")" 2>/dev/null || true
command -v kbuildsycoca6 >/dev/null && kbuildsycoca6 >/dev/null 2>&1 || true
+# Plasma caches icon lookups; drop the cache so the new icon shows straight away.
+rm -f "${XDG_CACHE_HOME:-$HOME/.cache}/icon-cache.kcache" 2>/dev/null || true
ok "MacroPad is in your app menu"
# -------------------------------------------------------------- 4. permission
diff --git a/macropad_gui/app.py b/macropad_gui/app.py
index 376ea2c..d89cdd6 100644
--- a/macropad_gui/app.py
+++ b/macropad_gui/app.py
@@ -8,7 +8,8 @@ import sys
from pathlib import Path
from PySide6.QtCore import QEvent, QSettings, Qt, QTimer, Signal
-from PySide6.QtGui import QColor, QFont, QFontDatabase, QGuiApplication, QPalette
+from PySide6.QtGui import (QColor, QFont, QFontDatabase, QGuiApplication, QIcon,
+ QPalette)
from PySide6.QtWidgets import (QApplication, QButtonGroup, QFileDialog, QFrame,
QGridLayout, QHBoxLayout, QLabel, QLineEdit,
QMainWindow, QMessageBox, QPlainTextEdit,
@@ -201,9 +202,11 @@ class Inspector(QWidget):
modes.setSpacing(6)
self.mode_keys = _button("Shortcut", checkable=True)
self.mode_media = _button("Media key", checkable=True)
+ self.mode_none = _button("Nothing", checkable=True)
+ self.mode_none.setToolTip("Make this control do nothing at all")
self.mode_group = QButtonGroup(self)
self.mode_group.setExclusive(True)
- for i, b in enumerate((self.mode_keys, self.mode_media)):
+ for i, b in enumerate((self.mode_keys, self.mode_media, self.mode_none)):
self.mode_group.addButton(b, i)
modes.addWidget(b)
modes.addStretch()
@@ -239,9 +242,12 @@ class Inspector(QWidget):
self.media_group.addButton(b, [m for m, _ in core.MEDIA].index(mid))
self.media_buttons[mid] = b
ml.addWidget(b, r, col)
- self.pages = (kp, mp)
+ np_ = _label("Pressing or turning it will do nothing. Handy for a key "
+ "you keep hitting by accident.", "dim", wrap=True)
+ self.pages = (kp, mp, np_)
ev.addWidget(kp)
ev.addWidget(mp)
+ ev.addWidget(np_)
ev.addSpacing(10)
actions = QHBoxLayout()
@@ -325,7 +331,12 @@ class Inspector(QWidget):
self.onpad_why.hide()
shown = desired or (None if entry.unknown else entry.binding)
- if shown is not None and shown.media:
+ if shown is not None and shown.none:
+ self.mode_none.setChecked(True)
+ self._page(2)
+ self.keys_edit.clear()
+ self._clear_media()
+ elif shown is not None and shown.media:
self.mode_media.setChecked(True)
self._page(1)
self.media_buttons[shown.media].setChecked(True)
@@ -357,6 +368,8 @@ class Inspector(QWidget):
def _mode(self, i):
self._page(i)
+ if i == 2 and not self._loading and self.control is not None:
+ self.changed.emit(self.control, core.Binding(none=True))
if i == 0 and self.record.isChecked():
self.keys_edit.setFocus()
@@ -484,7 +497,7 @@ class Window(QMainWindow):
elif e.unknown:
looks[c] = Look("", known=False)
else:
- looks[c] = Look(e.binding.label(), known=True)
+ looks[c] = Look(e.binding.label(), known=True, quiet=e.binding.none)
self.pad.set_looks(looks)
n = len(pending)
@@ -569,7 +582,10 @@ class Window(QMainWindow):
if len(done) == 1:
c, b = done[0]
- self._say(f"{core.control_name(c)} is now {b.label()}.", GOOD)
+ if b.none:
+ self._say(f"{core.control_name(c)} now does nothing.", GOOD)
+ else:
+ self._say(f"{core.control_name(c)} is now {b.label()}.", GOOD)
else:
self._say(f"Wrote {len(done)} changes to the pad.", GOOD)
self._refresh()
@@ -637,10 +653,17 @@ def apply_theme(app):
app.setStyleSheet(STYLE)
+ICON = Path(__file__).resolve().parent.parent / "packaging" / "macropad.svg"
+
+
def run():
app = QApplication(sys.argv)
app.setApplicationName("macropad")
app.setDesktopFileName("macropad")
+ # The launcher gives Plasma the icon; this covers the window itself, X11
+ # sessions, and running straight from a git checkout.
+ if ICON.exists():
+ app.setWindowIcon(QIcon(str(ICON)))
apply_theme(app)
w = Window()
w.resize(1000, 800)
diff --git a/macropad_gui/core.py b/macropad_gui/core.py
index 8cb01cb..8d7f92a 100644
--- a/macropad_gui/core.py
+++ b/macropad_gui/core.py
@@ -144,6 +144,7 @@ class Binding:
Binding(keys="ctrl+c")
Binding(keys="a,b,c", delay=50)
Binding(media="volumeup")
+ Binding(none=True) the control does nothing
The kind is explicit rather than guessed from the string, because
'pause' is both a keyboard key and a media key.
@@ -151,17 +152,18 @@ class Binding:
keys: str = ""
media: str = ""
delay: int = 0
+ none: bool = False
def validate(self):
- if bool(self.keys) == bool(self.media):
- raise ValueError("a binding is either keys or media, not both or neither")
+ if sum(map(bool, (self.keys, self.media, self.none))) != 1:
+ raise ValueError("a binding is exactly one of keys, media or nothing")
if self.media and self.media not in proto.MEDIA_KEYS:
raise ValueError(f"unknown media key '{self.media}'")
if self.keys:
proto.parse_keystroke(self.keys) # raises ValueError
if not 0 <= self.delay <= 0xFFFF:
raise ValueError("delay must be 0-65535 ms")
- if self.media and self.delay:
+ if self.delay and not self.keys:
raise ValueError("delay only applies to key sequences")
return self
@@ -169,9 +171,18 @@ class Binding:
def steps(self):
return len(proto.parse_keystroke(self.keys)) if self.keys else 1
+ @property
+ def is_nothing(self):
+ return self.none
+
def reports(self, report_id, action, layer=1):
"""The exact reports to send, built entirely by macropad.py."""
self.validate()
+ if self.none:
+ # A keystroke with no modifier and no key: count 0, pair 00 00.
+ # This is ch57x-keyboard-tool's encoding for a modifier-only key,
+ # with no modifier. Needs confirming on hardware (see README).
+ return proto.native_key(report_id, action, [(0, 0)], layer, 0)
if self.media:
return proto.native_media(report_id, action,
proto.MEDIA_KEYS[self.media], layer)
@@ -185,12 +196,16 @@ class Binding:
def label(self):
"""What a person reads on the key: 'Ctrl+Shift+N', 'Volume up'."""
+ if self.none:
+ return "Nothing"
if self.media:
return MEDIA_LABEL.get(self.media, self.media)
text = ", ".join(pretty_step(s) for s in self.keys.split(",") if s.strip())
return text + (f" ({self.delay} ms)" if self.delay else "")
def to_json(self):
+ if self.none:
+ return {"none": True}
if self.media:
return {"media": self.media}
d = {"keys": self.keys}
@@ -201,7 +216,8 @@ class Binding:
@classmethod
def from_json(cls, d):
return cls(keys=d.get("keys", ""), media=d.get("media", ""),
- delay=int(d.get("delay", 0))).validate()
+ delay=int(d.get("delay", 0)),
+ none=bool(d.get("none", False))).validate()
# ------------------------------------------------------------------- files
@@ -426,8 +442,10 @@ def diagnose(keyd=True):
"worked out. Pads in this family differ in layout and "
"protocol, so writing to yours with the wrong one could "
"fail or scramble it. The app won't try.",
- "You can help add it: the README's \"Other pads\" section "
- "shows how to capture what the vendor software sends."],
+ "ch57x-keyboard-tool, a command line tool, supports several "
+ "of these pads today. To get yours into this app, the "
+ "README's \"Other pads\" section shows how to capture what "
+ "the vendor software sends."],
warnings=warnings)
if not found:
@@ -570,6 +588,12 @@ def decode_config(report):
media = _MEDIA_BY_USAGE.get(report[11] | (report[12] << 8))
return (control, layer, Binding(media=media)) if media else None
+ if kind == proto.KeyType.BASIC and count == 0:
+ mods, code = report[11], report[12]
+ if not mods and not code:
+ return control, layer, Binding(none=True)
+ return None
+
if kind == proto.KeyType.BASIC and count:
steps = []
for i in range(count):
diff --git a/macropad_gui/padview.py b/macropad_gui/padview.py
index 8bbd1ac..4d1e52e 100644
--- a/macropad_gui/padview.py
+++ b/macropad_gui/padview.py
@@ -43,6 +43,7 @@ class Look:
text: str = "" # what's on the pad, or will be once written
known: bool = False
pending: bool = False
+ quiet: bool = False # known, but does nothing: legend drawn dimmed
def _key_grid(orientation):
@@ -268,7 +269,7 @@ class PadView(QWidget):
if look.pending:
p.setPen(TAPE)
else:
- p.setPen(INK if look.known else INK_DIM)
+ p.setPen(INK if look.known and not look.quiet else INK_DIM)
text = look.text if (look.known or look.pending) else "Unknown"
body = rect.adjusted(8, 22, -8, -10)
# Let 'Meta+Ctrl+Left' break after a '+' instead of being cut off.
@@ -379,7 +380,8 @@ class PadView(QWidget):
f = QFont(self.font())
f.setPixelSize(12)
p.setFont(f)
- p.setPen(TAPE if look.pending else (INK if look.known else INK_DIM))
+ p.setPen(TAPE if look.pending else
+ (INK if look.known and not look.quiet else INK_DIM))
text = look.text if (look.known or look.pending) else "Unknown"
body = rect.adjusted(22, 0, -26, 0)
p.drawText(body, Qt.AlignLeft | Qt.AlignVCenter,
diff --git a/tests/test_core.py b/tests/test_core.py
index 1211379..9b39d7b 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -141,6 +141,21 @@ class BindingModel(unittest.TestCase):
self.assertEqual(core.Binding(keys="super+pageup").label(), "Meta+Page Up")
self.assertEqual(core.Binding(media="mycomputer").label(), "My computer")
+ def test_nothing(self):
+ b = core.Binding(none=True).validate()
+ config, commit = b.reports(3, 0x04)
+ # type 1 (keys), count 0, empty pair: press nothing
+ self.assertEqual(config[:14].hex(" "), "03 fd 04 01 01 00 00 00 00 00 00 00 00 00")
+ self.assertEqual(commit[:4], b"\x03\xfd\xfe\xff")
+ self.assertFalse(any(config[11:]))
+ self.assertEqual(b.label(), "Nothing")
+ self.assertEqual(core.Binding.from_json(b.to_json()), b)
+ self.assertEqual(core.decode_config(config), ("key4", 1, b))
+ for bad in (core.Binding(none=True, keys="a"), core.Binding(none=True, media="mute"),
+ core.Binding(none=True, delay=5)):
+ with self.assertRaises(ValueError):
+ bad.validate()
+
def test_pause_is_unambiguous(self):
key = core.Binding(keys="pause").reports(3, 1)[0]
media = core.Binding(media="pause").reports(3, 1)[0]
@@ -293,5 +308,11 @@ class GuiSmoke(unittest.TestCase):
self.assertEqual(w.inspector.title.text(), core.control_name(c))
w._changed("key1", core.Binding(media="mute"))
self.assertEqual(w._pending(), ["key1"])
+ # choosing Nothing marks the control as changed straight away
+ w.pad.select("key5")
+ w._refresh()
+ w.inspector.mode_none.click()
+ self.assertEqual(w.desired.get("key5"), core.Binding(none=True))
+ self.assertIn("key5", w._pending())
w.desired.clear()
w.close()