fix linux
This commit is contained in:
parent
6af21dd21a
commit
e802ab2b3c
|
|
@ -65,7 +65,9 @@
|
||||||
"Bash(lspci)",
|
"Bash(lspci)",
|
||||||
"Bash(pacman -Q)",
|
"Bash(pacman -Q)",
|
||||||
"Bash(/run/media/chk/Ventoy/projects/chrka/whisper-dictation/.venv-linux/bin/pip install:*)",
|
"Bash(/run/media/chk/Ventoy/projects/chrka/whisper-dictation/.venv-linux/bin/pip install:*)",
|
||||||
"Bash(.venv-linux/bin/python build.py)"
|
"Bash(.venv-linux/bin/python build.py)",
|
||||||
|
"Bash(.venv-linux/bin/pip list:*)",
|
||||||
|
"Bash(.venv-linux/bin/python -c \":*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
README.md
10
README.md
|
|
@ -41,6 +41,16 @@ sudo apt install python3-tk libayatana-appindicator3-1 wl-clipboard xdotool
|
||||||
| `wl-clipboard` | Text injection on Wayland (`wl-copy`) |
|
| `wl-clipboard` | Text injection on Wayland (`wl-copy`) |
|
||||||
| `xdotool` | Simulates Ctrl+V paste on Wayland, text typing on X11 |
|
| `xdotool` | Simulates Ctrl+V paste on Wayland, text typing on X11 |
|
||||||
|
|
||||||
|
**Low-latency hotkey (recommended):**
|
||||||
|
|
||||||
|
For fast hotkey response via evdev (instead of the slower XWayland fallback), add your user to the `input` group:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo usermod -aG input $USER
|
||||||
|
```
|
||||||
|
|
||||||
|
Log out and back in for the change to take effect.
|
||||||
|
|
||||||
**Optional (for GPU acceleration):**
|
**Optional (for GPU acceleration):**
|
||||||
|
|
||||||
Arch/CachyOS:
|
Arch/CachyOS:
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,10 @@
|
||||||
{
|
{
|
||||||
"from": "Kiyosa",
|
"from": "Kiyosa",
|
||||||
"to": "Key-User"
|
"to": "Key-User"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Kashi",
|
||||||
|
"to": "Cachy"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -1,15 +1,151 @@
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
|
||||||
|
# ── evdev key name → hotkey string mapping ───────────────────────────────────
|
||||||
|
_EVDEV_MODIFIER_MAP = {
|
||||||
|
"ctrl": {"KEY_LEFTCTRL", "KEY_RIGHTCTRL"},
|
||||||
|
"ctrl_l": {"KEY_LEFTCTRL"}, "ctrl_r": {"KEY_RIGHTCTRL"},
|
||||||
|
"shift": {"KEY_LEFTSHIFT", "KEY_RIGHTSHIFT"},
|
||||||
|
"shift_l": {"KEY_LEFTSHIFT"}, "shift_r": {"KEY_RIGHTSHIFT"},
|
||||||
|
"alt": {"KEY_LEFTALT", "KEY_RIGHTALT"},
|
||||||
|
"alt_l": {"KEY_LEFTALT"}, "alt_r": {"KEY_RIGHTALT"},
|
||||||
|
}
|
||||||
|
|
||||||
|
_EVDEV_KEY_MAP = {
|
||||||
|
"space": "KEY_SPACE", "tab": "KEY_TAB", "enter": "KEY_ENTER",
|
||||||
|
"esc": "KEY_ESC", "escape": "KEY_ESC",
|
||||||
|
"up": "KEY_UP", "down": "KEY_DOWN", "left": "KEY_LEFT", "right": "KEY_RIGHT",
|
||||||
|
"home": "KEY_HOME", "end": "KEY_END", "page_up": "KEY_PAGEUP", "page_down": "KEY_PAGEDOWN",
|
||||||
|
"insert": "KEY_INSERT", "delete": "KEY_DELETE", "backspace": "KEY_BACKSPACE",
|
||||||
|
}
|
||||||
|
for _i in range(1, 13):
|
||||||
|
_EVDEV_KEY_MAP[f"f{_i}"] = f"KEY_F{_i}"
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_hotkey_evdev(hotkey_str):
|
||||||
|
"""Parse hotkey string into evdev key code sets."""
|
||||||
|
parts = [p.strip().lower() for p in hotkey_str.split("+")]
|
||||||
|
modifiers = []
|
||||||
|
for p in parts[:-1]:
|
||||||
|
if p in _EVDEV_MODIFIER_MAP:
|
||||||
|
modifiers.append(_EVDEV_MODIFIER_MAP[p])
|
||||||
|
elif p in _EVDEV_KEY_MAP:
|
||||||
|
modifiers.append({_EVDEV_KEY_MAP[p]})
|
||||||
|
else:
|
||||||
|
modifiers.append({f"KEY_{p.upper()}"})
|
||||||
|
trigger_part = parts[-1]
|
||||||
|
if trigger_part in _EVDEV_KEY_MAP:
|
||||||
|
trigger = _EVDEV_KEY_MAP[trigger_part]
|
||||||
|
elif trigger_part in _EVDEV_MODIFIER_MAP:
|
||||||
|
trigger = next(iter(_EVDEV_MODIFIER_MAP[trigger_part]))
|
||||||
|
else:
|
||||||
|
trigger = f"KEY_{trigger_part.upper()}"
|
||||||
|
return modifiers, trigger
|
||||||
|
|
||||||
|
|
||||||
|
def _can_use_evdev():
|
||||||
|
"""Check if evdev is available and we have access to input devices."""
|
||||||
|
if os.name == "nt":
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
import evdev
|
||||||
|
devices = [evdev.InputDevice(p) for p in evdev.list_devices()]
|
||||||
|
keyboards = [d for d in devices if 1 in d.capabilities()] # EV_KEY
|
||||||
|
for d in devices:
|
||||||
|
d.close()
|
||||||
|
return len(keyboards) > 0
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class _EvdevListener:
|
||||||
|
"""Low-latency hotkey listener using evdev (Linux kernel input)."""
|
||||||
|
|
||||||
|
def __init__(self, hotkey_str, on_press, on_release):
|
||||||
|
self._modifiers, self._trigger = _parse_hotkey_evdev(hotkey_str)
|
||||||
|
self._on_press = on_press
|
||||||
|
self._on_release = on_release
|
||||||
|
self._pressed = set()
|
||||||
|
self._active = False
|
||||||
|
self._stop_event = threading.Event()
|
||||||
|
self._thread = threading.Thread(target=self._run, daemon=True)
|
||||||
|
self._thread.start()
|
||||||
|
|
||||||
|
def _run(self):
|
||||||
|
import evdev
|
||||||
|
import select
|
||||||
|
|
||||||
|
devices = [evdev.InputDevice(p) for p in evdev.list_devices()]
|
||||||
|
keyboards = [d for d in devices if 1 in d.capabilities()]
|
||||||
|
# close non-keyboard devices
|
||||||
|
for d in devices:
|
||||||
|
if d not in keyboards:
|
||||||
|
d.close()
|
||||||
|
|
||||||
|
if not keyboards:
|
||||||
|
return
|
||||||
|
|
||||||
|
while not self._stop_event.is_set():
|
||||||
|
r, _, _ = select.select(keyboards, [], [], 0.5)
|
||||||
|
for dev in r:
|
||||||
|
try:
|
||||||
|
for event in dev.read():
|
||||||
|
if event.type != 1: # EV_KEY
|
||||||
|
continue
|
||||||
|
code_name = evdev.ecodes.KEY.get(event.code)
|
||||||
|
if isinstance(code_name, list):
|
||||||
|
code_name = code_name[0]
|
||||||
|
if code_name is None:
|
||||||
|
continue
|
||||||
|
if event.value == 1: # key down
|
||||||
|
self._key_down(code_name)
|
||||||
|
elif event.value == 0: # key up
|
||||||
|
self._key_up(code_name)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for d in keyboards:
|
||||||
|
d.close()
|
||||||
|
|
||||||
|
def _key_down(self, code_name):
|
||||||
|
self._pressed.add(code_name)
|
||||||
|
if not self._active and code_name == self._trigger and self._modifiers_held():
|
||||||
|
self._active = True
|
||||||
|
self._on_press()
|
||||||
|
|
||||||
|
def _key_up(self, code_name):
|
||||||
|
self._pressed.discard(code_name)
|
||||||
|
if self._active and code_name == self._trigger:
|
||||||
|
self._active = False
|
||||||
|
self._on_release()
|
||||||
|
|
||||||
|
def _modifiers_held(self):
|
||||||
|
return all(
|
||||||
|
any(k in self._pressed for k in mod_set)
|
||||||
|
for mod_set in self._modifiers
|
||||||
|
)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._stop_event.set()
|
||||||
|
|
||||||
|
|
||||||
|
# ── pynput fallback (Windows, or Linux without evdev/input group) ────────────
|
||||||
|
|
||||||
_pynput_loaded = False
|
_pynput_loaded = False
|
||||||
Key = KeyCode = KeyboardListener = None
|
Key = KeyCode = KeyboardListener = None
|
||||||
|
_PYNPUT_MODIFIER_MAP = {}
|
||||||
|
_PYNPUT_KEY_MAP = {}
|
||||||
|
|
||||||
|
|
||||||
def _ensure_pynput():
|
def _ensure_pynput():
|
||||||
global _pynput_loaded, Key, KeyCode, KeyboardListener, _MODIFIER_MAP, _KEY_MAP
|
global _pynput_loaded, Key, KeyCode, KeyboardListener
|
||||||
if _pynput_loaded:
|
if _pynput_loaded:
|
||||||
return
|
return
|
||||||
from pynput.keyboard import Listener as _Listener, Key as _Key, KeyCode as _KeyCode
|
from pynput.keyboard import Listener as _Listener, Key as _Key, KeyCode as _KeyCode
|
||||||
Key = _Key
|
Key = _Key
|
||||||
KeyCode = _KeyCode
|
KeyCode = _KeyCode
|
||||||
KeyboardListener = _Listener
|
KeyboardListener = _Listener
|
||||||
_MODIFIER_MAP.update({
|
_PYNPUT_MODIFIER_MAP.update({
|
||||||
"ctrl": {Key.ctrl_l, Key.ctrl_r},
|
"ctrl": {Key.ctrl_l, Key.ctrl_r},
|
||||||
"ctrl_l": {Key.ctrl_l}, "ctrl_r": {Key.ctrl_r},
|
"ctrl_l": {Key.ctrl_l}, "ctrl_r": {Key.ctrl_r},
|
||||||
"shift": {Key.shift_l, Key.shift_r},
|
"shift": {Key.shift_l, Key.shift_r},
|
||||||
|
|
@ -17,7 +153,7 @@ def _ensure_pynput():
|
||||||
"alt": {Key.alt_l, Key.alt_r},
|
"alt": {Key.alt_l, Key.alt_r},
|
||||||
"alt_l": {Key.alt_l}, "alt_r": {Key.alt_r},
|
"alt_l": {Key.alt_l}, "alt_r": {Key.alt_r},
|
||||||
})
|
})
|
||||||
_KEY_MAP.update({
|
_PYNPUT_KEY_MAP.update({
|
||||||
"space": Key.space, "tab": Key.tab, "enter": Key.enter,
|
"space": Key.space, "tab": Key.tab, "enter": Key.enter,
|
||||||
"esc": Key.esc, "escape": Key.esc,
|
"esc": Key.esc, "escape": Key.esc,
|
||||||
"up": Key.up, "down": Key.down, "left": Key.left, "right": Key.right,
|
"up": Key.up, "down": Key.down, "left": Key.left, "right": Key.right,
|
||||||
|
|
@ -25,42 +161,36 @@ def _ensure_pynput():
|
||||||
"insert": Key.insert, "delete": Key.delete, "backspace": Key.backspace,
|
"insert": Key.insert, "delete": Key.delete, "backspace": Key.backspace,
|
||||||
})
|
})
|
||||||
for i in range(1, 13):
|
for i in range(1, 13):
|
||||||
_KEY_MAP[f"f{i}"] = getattr(Key, f"f{i}")
|
_PYNPUT_KEY_MAP[f"f{i}"] = getattr(Key, f"f{i}")
|
||||||
_pynput_loaded = True
|
_pynput_loaded = True
|
||||||
|
|
||||||
_MODIFIER_MAP = {}
|
|
||||||
_KEY_MAP = {}
|
|
||||||
|
|
||||||
|
def _parse_hotkey_pynput(hotkey_str):
|
||||||
def _parse_hotkey(hotkey_str):
|
|
||||||
"""Parse hotkey string into (modifier_sets, trigger_key).
|
|
||||||
Returns: (list of sets-of-pynput-keys for each modifier, pynput key for trigger)
|
|
||||||
"""
|
|
||||||
_ensure_pynput()
|
_ensure_pynput()
|
||||||
parts = [p.strip().lower() for p in hotkey_str.split("+")]
|
parts = [p.strip().lower() for p in hotkey_str.split("+")]
|
||||||
modifiers = []
|
modifiers = []
|
||||||
for p in parts[:-1]:
|
for p in parts[:-1]:
|
||||||
if p in _MODIFIER_MAP:
|
if p in _PYNPUT_MODIFIER_MAP:
|
||||||
modifiers.append(_MODIFIER_MAP[p])
|
modifiers.append(_PYNPUT_MODIFIER_MAP[p])
|
||||||
elif p in _KEY_MAP:
|
elif p in _PYNPUT_KEY_MAP:
|
||||||
modifiers.append({_KEY_MAP[p]})
|
modifiers.append({_PYNPUT_KEY_MAP[p]})
|
||||||
else:
|
else:
|
||||||
modifiers.append({KeyCode.from_char(p)})
|
modifiers.append({KeyCode.from_char(p)})
|
||||||
trigger_part = parts[-1]
|
trigger_part = parts[-1]
|
||||||
if trigger_part in _KEY_MAP:
|
if trigger_part in _PYNPUT_KEY_MAP:
|
||||||
trigger = _KEY_MAP[trigger_part]
|
trigger = _PYNPUT_KEY_MAP[trigger_part]
|
||||||
elif trigger_part in _MODIFIER_MAP:
|
elif trigger_part in _PYNPUT_MODIFIER_MAP:
|
||||||
trigger = next(iter(_MODIFIER_MAP[trigger_part]))
|
trigger = next(iter(_PYNPUT_MODIFIER_MAP[trigger_part]))
|
||||||
else:
|
else:
|
||||||
trigger = KeyCode.from_char(trigger_part)
|
trigger = KeyCode.from_char(trigger_part)
|
||||||
return modifiers, trigger
|
return modifiers, trigger
|
||||||
|
|
||||||
|
|
||||||
class HotkeyListener:
|
class _PynputListener:
|
||||||
"""Hold-to-record hotkey using pynput. No root required on X11."""
|
"""Hotkey listener using pynput (Windows / fallback)."""
|
||||||
|
|
||||||
def __init__(self, hotkey_str, on_press, on_release):
|
def __init__(self, hotkey_str, on_press, on_release):
|
||||||
self._modifiers, self._trigger = _parse_hotkey(hotkey_str)
|
self._modifiers, self._trigger = _parse_hotkey_pynput(hotkey_str)
|
||||||
self._on_press = on_press
|
self._on_press = on_press
|
||||||
self._on_release = on_release
|
self._on_release = on_release
|
||||||
self._pressed = set()
|
self._pressed = set()
|
||||||
|
|
@ -69,23 +199,29 @@ class HotkeyListener:
|
||||||
self._listener.daemon = True
|
self._listener.daemon = True
|
||||||
self._listener.start()
|
self._listener.start()
|
||||||
|
|
||||||
def _matches_trigger(self, key):
|
|
||||||
return key == self._trigger
|
|
||||||
|
|
||||||
def _modifiers_held(self):
|
|
||||||
return all(any(k in self._pressed for k in mod_set) for mod_set in self._modifiers)
|
|
||||||
|
|
||||||
def _key_down(self, key):
|
def _key_down(self, key):
|
||||||
self._pressed.add(key)
|
self._pressed.add(key)
|
||||||
if not self._active and self._matches_trigger(key) and self._modifiers_held():
|
if not self._active and key == self._trigger and self._modifiers_held():
|
||||||
self._active = True
|
self._active = True
|
||||||
self._on_press()
|
self._on_press()
|
||||||
|
|
||||||
def _key_up(self, key):
|
def _key_up(self, key):
|
||||||
self._pressed.discard(key)
|
self._pressed.discard(key)
|
||||||
if self._active and self._matches_trigger(key):
|
if self._active and key == self._trigger:
|
||||||
self._active = False
|
self._active = False
|
||||||
self._on_release()
|
self._on_release()
|
||||||
|
|
||||||
|
def _modifiers_held(self):
|
||||||
|
return all(any(k in self._pressed for k in mod_set) for mod_set in self._modifiers)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self._listener.stop()
|
self._listener.stop()
|
||||||
|
|
||||||
|
|
||||||
|
# ── Public API ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def HotkeyListener(hotkey_str, on_press, on_release):
|
||||||
|
"""Create the best available hotkey listener for the current platform."""
|
||||||
|
if _can_use_evdev():
|
||||||
|
return _EvdevListener(hotkey_str, on_press, on_release)
|
||||||
|
return _PynputListener(hotkey_str, on_press, on_release)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue