98 lines
3.9 KiB
Python
98 lines
3.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Opt-in real key test in a disposable XWayland widget; never read clipboard.
|
|
|
|
Every fixture key binding returns break, so even a paste chord cannot invoke
|
|
clipboard_get or a normal editor action. Text-copy methods are mocked in the
|
|
production injector. Only keysyms/modifier masks are reported.
|
|
"""
|
|
import json
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
from unittest.mock import Mock
|
|
|
|
|
|
def fixture(result, title):
|
|
import tkinter as tk
|
|
window = tk.Tk(className='HyprwhsprSafeFixture')
|
|
window.title(title)
|
|
text = tk.Text(window)
|
|
text.insert('1.0', 'Synthetic key test; clipboard access disabled')
|
|
text.pack(); text.focus_set()
|
|
events = []
|
|
def key(event):
|
|
events.append({'keysym': event.keysym, 'state': event.state})
|
|
Path(result).write_text(json.dumps(events))
|
|
return 'break'
|
|
text.bind('<KeyPress>', key)
|
|
window.after(15000, window.destroy)
|
|
window.mainloop()
|
|
|
|
|
|
def nodes(node):
|
|
yield node
|
|
for key in ('nodes', 'floating_nodes'):
|
|
for child in node.get(key, []):
|
|
yield from nodes(child)
|
|
|
|
|
|
def tree():
|
|
return list(nodes(json.loads(subprocess.check_output(['swaymsg', '-t', 'get_tree', '-r']))))
|
|
|
|
|
|
def focus(identifier):
|
|
subprocess.run(['swaymsg', f'[con_id={identifier}] focus'], check=True, capture_output=True, timeout=2)
|
|
|
|
|
|
def probe():
|
|
overlay = Path.home()/'.local/share/hyprwhspr-swayfx/1.52.1-sway-v1/lib/src'
|
|
sys.path.insert(0, str(overlay))
|
|
from text_injector import TextInjector
|
|
original = next(n['id'] for n in tree() if n.get('focused'))
|
|
with tempfile.TemporaryDirectory(prefix='hyprwhspr-keys-') as folder:
|
|
result = Path(folder)/'events.json'
|
|
title = f'Hyprwhspr safe key fixture {Path(folder).name}'
|
|
child = subprocess.Popen([sys.executable, __file__, '--fixture', str(result), title])
|
|
injector = None
|
|
try:
|
|
deadline = time.monotonic()+5
|
|
target = None
|
|
while time.monotonic() < deadline:
|
|
target = next((n for n in tree() if n.get('name') == title), None)
|
|
if target: break
|
|
time.sleep(.1)
|
|
if not target: raise RuntimeError('Fixture did not map')
|
|
focus(target['id'])
|
|
injector = TextInjector()
|
|
info = injector._get_active_window_info()
|
|
if not info or not info.get('xwayland') or info['sway_id'] != target['id']:
|
|
raise RuntimeError('Fixture is not focused XWayland')
|
|
injector._save_clipboard = Mock(return_value=None)
|
|
injector._copy_text_to_clipboard = Mock(return_value=True)
|
|
injector._restore_clipboard = Mock()
|
|
for label, action in [
|
|
('upstream-wtype', lambda: injector._send_paste_keys_wtype('ctrl+v')),
|
|
('patched-dispatch', lambda: injector._paste_via_clipboard(
|
|
'Synthetic text only', 'ctrl+v', False, False, expected_window=info))]:
|
|
if injector._get_sway_window_info() != info:
|
|
raise RuntimeError('Focus changed; refusing test')
|
|
previous = len(json.loads(result.read_text())) if result.exists() else 0
|
|
ok = action(); time.sleep(.15)
|
|
events = json.loads(result.read_text())[previous:] if result.exists() else []
|
|
print(label, ok, events)
|
|
if label == 'patched-dispatch' and not (ok and any(e['keysym']=='v' and e['state'] & 4 for e in events)):
|
|
raise RuntimeError('Patched Ctrl+V not observed')
|
|
finally:
|
|
if injector: injector.close()
|
|
child.terminate()
|
|
try: child.wait(timeout=3)
|
|
except subprocess.TimeoutExpired:
|
|
child.kill(); child.wait(timeout=3)
|
|
focus(original)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
if len(sys.argv) > 1 and sys.argv[1] == '--fixture': fixture(*sys.argv[2:])
|
|
else: probe()
|