Give Input keycaps, a shortcut search, and the missing pointer basics
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -16,6 +16,24 @@ pragma Singleton
|
||||
// Read on demand rather than polled. Input devices do come and go -- a mouse is
|
||||
// unplugged, a receiver is moved -- so this also refreshes when Hyprland says
|
||||
// the device list changed, which is the only moment the answer can differ.
|
||||
//
|
||||
// Two views of the same data, deliberately:
|
||||
//
|
||||
// mice / keyboards every name libinput reports, lowercased. What the
|
||||
// hardware-presence checks are built on, and what
|
||||
// they must keep being built on -- a filter that
|
||||
// dropped the wrong entry would hide a real control.
|
||||
// realMice / realKeyboards the subset worth SHOWING a person, because the
|
||||
// raw lists are mostly not devices. This machine
|
||||
// reports seven "mice" and eighteen "keyboards" for
|
||||
// two actual peripherals: a wireless receiver
|
||||
// registers a mouse, a keyboard, three consumer
|
||||
// controls and a system control, the webcam and the
|
||||
// USB audio dongle each claim a keyboard, and the
|
||||
// power button, the lid switch and every virtual
|
||||
// typing tool are in there too. Listing all of that
|
||||
// under "Connected devices" would be honest about
|
||||
// libinput and useless about the desk.
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
@@ -28,12 +46,69 @@ Singleton {
|
||||
property var mice: []
|
||||
property var keyboards: []
|
||||
|
||||
// The main keyboard's live layout, already in human form -- Hyprland
|
||||
// resolves the XKB name for us and answers "English (US)", not "us". Empty
|
||||
// until the first read finishes, so a page must treat it as optional.
|
||||
property string mainKeyboardLayout: ""
|
||||
|
||||
// True when anything that looks like a touchpad is attached.
|
||||
readonly property bool hasTouchpad: root.mice.some(name =>
|
||||
name.includes("touchpad") || name.includes("trackpad"))
|
||||
|
||||
readonly property bool hasMouse: root.mice.length > 0
|
||||
|
||||
// Substrings that mark a name as an endpoint rather than a device. Matched
|
||||
// anywhere in the name, because the interesting part of these is never at a
|
||||
// fixed position: "generic-usb-audio-consumer-control-1" is caught twice
|
||||
// over, and "onn-usb-2.0-webcam:-onn-usb-2.0" only by its middle.
|
||||
//
|
||||
// "fake" is here for `mouce-library-fake-mouse`, the phantom pointer the
|
||||
// mouce input library registers. It is not a class of endpoint the way the
|
||||
// others are, but it is precisely the kind of entry this list exists to keep
|
||||
// off a card headed "Connected devices".
|
||||
readonly property var phantomMarkers: [
|
||||
"consumer-control", "virtual", "video-bus", "power-button",
|
||||
"webcam", "audio", "uinput", "fake"
|
||||
]
|
||||
|
||||
readonly property var realMice: root.realDevices(root.mice)
|
||||
readonly property var realKeyboards: root.realDevices(root.keyboards)
|
||||
|
||||
// Names worth showing, as { name, pretty }.
|
||||
//
|
||||
// Two passes. The first drops endpoints by name. The second collapses
|
||||
// siblings: one physical transceiver announces itself as
|
||||
// "microsoft-...-v9.0" AND "microsoft-...-v9.0-system-control", and a
|
||||
// keyboard as both its own name and "...-keyboard". Where one surviving
|
||||
// name is a prefix of another, the shorter is the device and the longer is
|
||||
// one of its endpoints, so the shortest of each family is the one kept.
|
||||
function realDevices(names: var): var {
|
||||
const kept = [];
|
||||
const named = (names ?? [])
|
||||
.filter(name => name && !root.phantomMarkers.some(marker => name.includes(marker)))
|
||||
.sort((left, right) => left.length - right.length);
|
||||
|
||||
for (const name of named) {
|
||||
if (kept.some(base => name.startsWith(base + "-")))
|
||||
continue;
|
||||
kept.push(name);
|
||||
}
|
||||
|
||||
return kept.map(name => ({ name: name, pretty: root.prettyName(name) }));
|
||||
}
|
||||
|
||||
// "keychron-keychron-q10" -> "Keychron Keychron Q10". Hyprland's names are
|
||||
// lowercased and dash-joined; this is the smallest transform that makes one
|
||||
// readable. A word starting with a digit ("2.4ghz") is left as it is rather
|
||||
// than mangled.
|
||||
function prettyName(name: string): string {
|
||||
return String(name)
|
||||
.split("-")
|
||||
.map(word => word.length > 0 ? word[0].toUpperCase() + word.slice(1) : word)
|
||||
.join(" ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function refresh(): void {
|
||||
if (!query.running)
|
||||
query.running = true;
|
||||
@@ -49,6 +124,13 @@ Singleton {
|
||||
const parsed = JSON.parse(this.text);
|
||||
root.mice = (parsed.mice ?? []).map(device => String(device.name ?? "").toLowerCase());
|
||||
root.keyboards = (parsed.keyboards ?? []).map(device => String(device.name ?? "").toLowerCase());
|
||||
|
||||
// Names are lowercased above because every consumer matches
|
||||
// substrings against them. The keymap is not: it is already
|
||||
// a display string, and "english (us)" would be a downgrade.
|
||||
const boards = parsed.keyboards ?? [];
|
||||
const main = boards.find(device => device.main === true) ?? boards[0];
|
||||
root.mainKeyboardLayout = main ? String(main.active_keymap ?? "") : "";
|
||||
} catch (error) {
|
||||
console.warn("InputDevices: could not parse hyprctl devices:", error);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user