@desklink/host 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +44 -0
  3. package/README.md +266 -0
  4. package/bin/desklink-host.mjs +113 -0
  5. package/dist/bridge.d.ts +57 -0
  6. package/dist/bridge.d.ts.map +1 -0
  7. package/dist/bridge.js +208 -0
  8. package/dist/bridge.js.map +1 -0
  9. package/dist/engineProcess.d.ts +82 -0
  10. package/dist/engineProcess.d.ts.map +1 -0
  11. package/dist/engineProcess.js +243 -0
  12. package/dist/engineProcess.js.map +1 -0
  13. package/dist/index.d.ts +12 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +12 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/protocol.d.ts +169 -0
  18. package/dist/protocol.d.ts.map +1 -0
  19. package/dist/protocol.js +18 -0
  20. package/dist/protocol.js.map +1 -0
  21. package/dist/resolveEngine.d.ts +34 -0
  22. package/dist/resolveEngine.d.ts.map +1 -0
  23. package/dist/resolveEngine.js +103 -0
  24. package/dist/resolveEngine.js.map +1 -0
  25. package/docs/BOUNDARY.md +145 -0
  26. package/docs/PROTOCOL.md +363 -0
  27. package/engine/Cargo.lock +3077 -0
  28. package/engine/Cargo.toml +37 -0
  29. package/engine/build.rs +107 -0
  30. package/engine/native/inputtino_shim.cpp +11 -0
  31. package/engine/native/vpx_shim.c +184 -0
  32. package/engine/native/wrapper.h +7 -0
  33. package/engine/src/capture.rs +510 -0
  34. package/engine/src/clipboard.rs +132 -0
  35. package/engine/src/convert.rs +299 -0
  36. package/engine/src/encoder.rs +204 -0
  37. package/engine/src/input.rs +355 -0
  38. package/engine/src/keymap.rs +338 -0
  39. package/engine/src/main.rs +614 -0
  40. package/engine/src/peer.rs +775 -0
  41. package/engine/src/portal.rs +115 -0
  42. package/engine/src/protocol.rs +331 -0
  43. package/engine/src/session.rs +2159 -0
  44. package/engine/src/x11.rs +244 -0
  45. package/engine/vendor/inputtino/.clang-format +166 -0
  46. package/engine/vendor/inputtino/.clang-tidy +30 -0
  47. package/engine/vendor/inputtino/.dockerignore +11 -0
  48. package/engine/vendor/inputtino/.github/workflows/mirror-repo.yml +14 -0
  49. package/engine/vendor/inputtino/.github/workflows/python-lint-format-test-typecheck.yml +124 -0
  50. package/engine/vendor/inputtino/.github/workflows/python-test-install.yaml +49 -0
  51. package/engine/vendor/inputtino/.github/workflows/rust.yml +40 -0
  52. package/engine/vendor/inputtino/CMakeLists.txt +187 -0
  53. package/engine/vendor/inputtino/LICENSE +21 -0
  54. package/engine/vendor/inputtino/README.md +110 -0
  55. package/engine/vendor/inputtino/bindings/python/.devcontainer/Dockerfile +23 -0
  56. package/engine/vendor/inputtino/bindings/python/.devcontainer/devcontainer.json +28 -0
  57. package/engine/vendor/inputtino/bindings/python/.python-version +1 -0
  58. package/engine/vendor/inputtino/bindings/python/CMakeLists.txt +23 -0
  59. package/engine/vendor/inputtino/bindings/python/CPPLINT.cfg +2 -0
  60. package/engine/vendor/inputtino/bindings/python/Makefile +49 -0
  61. package/engine/vendor/inputtino/bindings/python/README.md +215 -0
  62. package/engine/vendor/inputtino/bindings/python/pyproject.toml +178 -0
  63. package/engine/vendor/inputtino/bindings/python/src/inputtino/__init__.py +43 -0
  64. package/engine/vendor/inputtino/bindings/python/src/inputtino/_core.pyi +533 -0
  65. package/engine/vendor/inputtino/bindings/python/src/inputtino/base.py +63 -0
  66. package/engine/vendor/inputtino/bindings/python/src/inputtino/joypad.py +248 -0
  67. package/engine/vendor/inputtino/bindings/python/src/inputtino/keyboard.py +256 -0
  68. package/engine/vendor/inputtino/bindings/python/src/inputtino/mouse.py +106 -0
  69. package/engine/vendor/inputtino/bindings/python/src/inputtino/pentablet.py +73 -0
  70. package/engine/vendor/inputtino/bindings/python/src/inputtino/py.typed +0 -0
  71. package/engine/vendor/inputtino/bindings/python/src/inputtino/touchscreen.py +61 -0
  72. package/engine/vendor/inputtino/bindings/python/src/inputtino/trackpad.py +74 -0
  73. package/engine/vendor/inputtino/bindings/python/src/main.cpp +217 -0
  74. package/engine/vendor/inputtino/bindings/python/tests/__init__.py +0 -0
  75. package/engine/vendor/inputtino/bindings/python/tests/helpers.py +3 -0
  76. package/engine/vendor/inputtino/bindings/python/tests/test_base.py +60 -0
  77. package/engine/vendor/inputtino/bindings/python/tests/test_inputtino.py +5 -0
  78. package/engine/vendor/inputtino/bindings/python/tests/test_joypad.py +332 -0
  79. package/engine/vendor/inputtino/bindings/python/tests/test_keyboard.py +164 -0
  80. package/engine/vendor/inputtino/bindings/python/tests/test_mouse.py +185 -0
  81. package/engine/vendor/inputtino/bindings/python/tests/test_pentablet.py +198 -0
  82. package/engine/vendor/inputtino/bindings/python/tests/test_touchscreen.py +144 -0
  83. package/engine/vendor/inputtino/bindings/python/tests/test_trackpad.py +146 -0
  84. package/engine/vendor/inputtino/bindings/python/uv.lock +390 -0
  85. package/engine/vendor/inputtino/bindings/rust/Cargo.toml +6 -0
  86. package/engine/vendor/inputtino/bindings/rust/inputtino/.dockerignore +1 -0
  87. package/engine/vendor/inputtino/bindings/rust/inputtino/Cargo.toml +18 -0
  88. package/engine/vendor/inputtino/bindings/rust/inputtino/examples/keyboard.rs +33 -0
  89. package/engine/vendor/inputtino/bindings/rust/inputtino/examples/mouse.rs +17 -0
  90. package/engine/vendor/inputtino/bindings/rust/inputtino/src/common.rs +116 -0
  91. package/engine/vendor/inputtino/bindings/rust/inputtino/src/error.rs +17 -0
  92. package/engine/vendor/inputtino/bindings/rust/inputtino/src/joypad.rs +74 -0
  93. package/engine/vendor/inputtino/bindings/rust/inputtino/src/joypad_nintendo.rs +135 -0
  94. package/engine/vendor/inputtino/bindings/rust/inputtino/src/joypad_ps5.rs +280 -0
  95. package/engine/vendor/inputtino/bindings/rust/inputtino/src/joypad_xbox.rs +134 -0
  96. package/engine/vendor/inputtino/bindings/rust/inputtino/src/keyboard.rs +76 -0
  97. package/engine/vendor/inputtino/bindings/rust/inputtino/src/lib.rs +32 -0
  98. package/engine/vendor/inputtino/bindings/rust/inputtino/src/mouse.rs +178 -0
  99. package/engine/vendor/inputtino/bindings/rust/inputtino/tests/common.rs +38 -0
  100. package/engine/vendor/inputtino/bindings/rust/inputtino/tests/joypads.rs +150 -0
  101. package/engine/vendor/inputtino/bindings/rust/inputtino/tests/keyboard.rs +73 -0
  102. package/engine/vendor/inputtino/bindings/rust/inputtino/tests/mouse.rs +139 -0
  103. package/engine/vendor/inputtino/bindings/rust/inputtino-sys/.dockerignore +1 -0
  104. package/engine/vendor/inputtino/bindings/rust/inputtino-sys/Cargo.toml +12 -0
  105. package/engine/vendor/inputtino/bindings/rust/inputtino-sys/build.rs +48 -0
  106. package/engine/vendor/inputtino/bindings/rust/inputtino-sys/src/lib.rs +5 -0
  107. package/engine/vendor/inputtino/bindings/rust/inputtino-sys/wrapper.h +1 -0
  108. package/engine/vendor/inputtino/cmake/utils.cmake +9 -0
  109. package/engine/vendor/inputtino/docker/Dockerfile +70 -0
  110. package/engine/vendor/inputtino/include/inputtino/input.h +324 -0
  111. package/engine/vendor/inputtino/include/inputtino/input.hpp +466 -0
  112. package/engine/vendor/inputtino/include/inputtino/result.hpp +37 -0
  113. package/engine/vendor/inputtino/share/pkgconfig/libinputtino.pc.in +17 -0
  114. package/engine/vendor/inputtino/src/c-bindings/helpers.hpp +19 -0
  115. package/engine/vendor/inputtino/src/c-bindings/joypad_ps5.cpp +115 -0
  116. package/engine/vendor/inputtino/src/c-bindings/joypad_switch.cpp +63 -0
  117. package/engine/vendor/inputtino/src/c-bindings/joypad_xone.cpp +62 -0
  118. package/engine/vendor/inputtino/src/c-bindings/keyboard.cpp +42 -0
  119. package/engine/vendor/inputtino/src/c-bindings/mouse.cpp +67 -0
  120. package/engine/vendor/inputtino/src/c-bindings/pen_tablet.cpp +50 -0
  121. package/engine/vendor/inputtino/src/c-bindings/touchscreen.cpp +43 -0
  122. package/engine/vendor/inputtino/src/c-bindings/trackpad.cpp +49 -0
  123. package/engine/vendor/inputtino/src/uhid/README.adoc +63 -0
  124. package/engine/vendor/inputtino/src/uhid/include/crc32.hpp +38 -0
  125. package/engine/vendor/inputtino/src/uhid/include/uhid/protected_types.hpp +36 -0
  126. package/engine/vendor/inputtino/src/uhid/include/uhid/ps5.hpp +550 -0
  127. package/engine/vendor/inputtino/src/uhid/include/uhid/uhid.hpp +168 -0
  128. package/engine/vendor/inputtino/src/uhid/joypad_ps5.cpp +580 -0
  129. package/engine/vendor/inputtino/src/uinput/include/inputtino/keyboard.hpp +84 -0
  130. package/engine/vendor/inputtino/src/uinput/include/inputtino/protected_ps5_types.hpp +7 -0
  131. package/engine/vendor/inputtino/src/uinput/include/inputtino/protected_types.hpp +110 -0
  132. package/engine/vendor/inputtino/src/uinput/joypad_nintendo.cpp +190 -0
  133. package/engine/vendor/inputtino/src/uinput/joypad_ps.cpp +203 -0
  134. package/engine/vendor/inputtino/src/uinput/joypad_utils.hpp +246 -0
  135. package/engine/vendor/inputtino/src/uinput/joypad_xbox.cpp +190 -0
  136. package/engine/vendor/inputtino/src/uinput/keyboard.cpp +120 -0
  137. package/engine/vendor/inputtino/src/uinput/mouse.cpp +199 -0
  138. package/engine/vendor/inputtino/src/uinput/pentablet.cpp +162 -0
  139. package/engine/vendor/inputtino/src/uinput/touchscreen.cpp +140 -0
  140. package/engine/vendor/inputtino/src/uinput/trackpad.cpp +192 -0
  141. package/examples/reference-client.html +428 -0
  142. package/package.json +38 -0
@@ -0,0 +1,332 @@
1
+ """Tests for joypad.py."""
2
+
3
+ import os
4
+ import time
5
+ from unittest.mock import MagicMock, patch
6
+
7
+ import pytest
8
+
9
+ from inputtino import _core
10
+ from inputtino.joypad import (
11
+ ControllerButton,
12
+ DeviceDefinition,
13
+ PS5BatteryState,
14
+ PS5Joypad,
15
+ PS5MotionType,
16
+ SwitchJoypad,
17
+ XBoxOneJoypad,
18
+ )
19
+ from tests.helpers import mark_practical
20
+
21
+
22
+ @pytest.fixture
23
+ def mock_joypad():
24
+ """Create a mock for base Joypad."""
25
+ with patch("inputtino._core.Joypad") as mock_joypad_cls:
26
+ mock_instance = MagicMock(spec=_core.Joypad)
27
+ mock_joypad_cls.create.return_value = mock_instance
28
+ yield mock_instance
29
+
30
+
31
+ @pytest.fixture
32
+ def mock_xbox_joypad():
33
+ """Create a mock for XBoxOneJoypad."""
34
+ with patch("inputtino._core.XboxOneJoypad") as mock_joypad_cls:
35
+ mock_instance = MagicMock()
36
+ mock_joypad_cls.create = MagicMock(return_value=mock_instance)
37
+ yield mock_joypad_cls
38
+
39
+
40
+ @pytest.fixture
41
+ def mock_switch_joypad():
42
+ """Create a mock for SwitchJoypad."""
43
+ with patch("inputtino._core.SwitchJoypad") as mock_joypad_cls:
44
+ mock_instance = MagicMock()
45
+ mock_joypad_cls.create = MagicMock(return_value=mock_instance)
46
+ yield mock_joypad_cls
47
+
48
+
49
+ @pytest.fixture
50
+ def mock_ps5_joypad():
51
+ """Create a mock for PS5Joypad."""
52
+ with patch("inputtino._core.PS5Joypad") as mock_joypad_cls:
53
+ mock_instance = MagicMock()
54
+ mock_joypad_cls.create = MagicMock(return_value=mock_instance)
55
+ yield mock_joypad_cls
56
+
57
+
58
+ def test_xbox_joypad_creation_with_default_device(mock_xbox_joypad):
59
+ """Test creating an Xbox joypad with default device definition."""
60
+ XBoxOneJoypad()
61
+ created_def = mock_xbox_joypad.create.call_args[0][0]
62
+
63
+ assert created_def.name == "Wolf X-Box One (virtual) pad"
64
+ assert created_def.vendor_id == 0x045E
65
+ assert created_def.product_id == 0x02EA
66
+ assert created_def.version == 0x0408
67
+
68
+
69
+ def test_xbox_joypad_with_custom_device(mock_xbox_joypad):
70
+ """Test creating an Xbox joypad with custom device definition."""
71
+ custom_def = DeviceDefinition(
72
+ name="Custom Xbox",
73
+ vendor_id=0x1234,
74
+ product_id=0x5678,
75
+ version=0x0001,
76
+ )
77
+
78
+ XBoxOneJoypad(custom_def)
79
+ created_def = mock_xbox_joypad.create.call_args[0][0]
80
+
81
+ assert created_def.name == custom_def.name
82
+ assert created_def.vendor_id == custom_def.vendor_id
83
+ assert created_def.product_id == custom_def.product_id
84
+ assert created_def.version == custom_def.version
85
+
86
+
87
+ def test_joypad_button_press(mock_xbox_joypad):
88
+ """Test joypad button pressing."""
89
+ joypad = XBoxOneJoypad()
90
+ instance = mock_xbox_joypad.create.return_value
91
+
92
+ buttons = ControllerButton.A | ControllerButton.B | ControllerButton.DPAD_UP
93
+ joypad.set_pressed_buttons(buttons)
94
+ instance.set_pressed_buttons.assert_called_once_with(int(buttons))
95
+
96
+
97
+ def test_joypad_triggers(mock_xbox_joypad):
98
+ """Test setting trigger values."""
99
+ joypad = XBoxOneJoypad()
100
+ instance = mock_xbox_joypad.create.return_value
101
+
102
+ joypad.set_triggers(1000, 2000)
103
+ instance.set_triggers.assert_called_once_with(1000, 2000)
104
+
105
+
106
+ def test_joypad_stick(mock_xbox_joypad):
107
+ """Test setting stick position."""
108
+ joypad = XBoxOneJoypad()
109
+ instance = mock_xbox_joypad.create.return_value
110
+
111
+ joypad.set_stick(_core.StickPosition.LS, 100, 200)
112
+ instance.set_stick.assert_called_once_with(_core.StickPosition.LS, 100, 200)
113
+
114
+
115
+ def test_ps5_joypad_specific_features(mock_ps5_joypad):
116
+ """Test PS5-specific features."""
117
+ joypad = PS5Joypad()
118
+ instance = mock_ps5_joypad.create.return_value
119
+ instance.get_mac_address.return_value = "00:11:22:33:44:55"
120
+
121
+ assert joypad.mac_address == "00:11:22:33:44:55"
122
+
123
+ joypad.place_finger(1, 960, 540)
124
+ instance.place_finger.assert_called_once_with(1, 960, 540)
125
+
126
+ joypad.set_motion(PS5MotionType.ACCELERATION, 1.0, 2.0, 3.0)
127
+ instance.set_motion.assert_called_once_with(
128
+ PS5MotionType.ACCELERATION, 1.0, 2.0, 3.0
129
+ )
130
+
131
+ joypad.set_battery(PS5BatteryState.BATTERY_FULL, 100)
132
+ instance.set_battery.assert_called_once_with(PS5BatteryState.BATTERY_FULL, 100)
133
+
134
+
135
+ def test_rumble_callback():
136
+ """Test rumble callback functionality."""
137
+ with patch("inputtino._core.XboxOneJoypad") as mock_joypad_cls:
138
+ joypad = XBoxOneJoypad()
139
+
140
+ callback_called = False
141
+
142
+ def rumble_callback(low, high):
143
+ nonlocal callback_called
144
+ callback_called = True
145
+ assert low == 1000
146
+ assert high == 2000
147
+
148
+ joypad.set_on_rumble(rumble_callback)
149
+
150
+ # Simulate rumble event
151
+ mock_joypad_cls.create.return_value.set_on_rumble.call_args[0][0](1000, 2000)
152
+ assert callback_called
153
+
154
+
155
+ def test_ps5_led_callback():
156
+ """Test PS5 LED callback functionality."""
157
+ with patch("inputtino._core.PS5Joypad") as mock_joypad_cls:
158
+ joypad = PS5Joypad()
159
+
160
+ callback_called = False
161
+
162
+ def led_callback(r, g, b):
163
+ nonlocal callback_called
164
+ callback_called = True
165
+ assert r == 255
166
+ assert g == 0
167
+ assert b == 0
168
+
169
+ joypad.set_on_led(led_callback)
170
+
171
+ # Simulate LED event
172
+ mock_joypad_cls.create.return_value.set_on_led.call_args[0][0](255, 0, 0)
173
+ assert callback_called
174
+
175
+
176
+ def test_joypad_creation_failure():
177
+ """Test handling of joypad creation failure."""
178
+ with patch("inputtino._core.XboxOneJoypad") as mock_joypad_cls:
179
+ mock_joypad_cls.create.side_effect = RuntimeError(
180
+ "Failed to create joypad device"
181
+ )
182
+
183
+ with pytest.raises(RuntimeError, match="Failed to create joypad device"):
184
+ XBoxOneJoypad()
185
+
186
+
187
+ def test_switch_joypad_creation_with_default_device(mock_switch_joypad):
188
+ """Test creating a Switch joypad with default device definition."""
189
+ SwitchJoypad()
190
+ created_def = mock_switch_joypad.create.call_args[0][0]
191
+
192
+ assert created_def.name == "Wolf Nintendo (virtual) pad"
193
+ assert created_def.vendor_id == 0x057E
194
+ assert created_def.product_id == 0x2009
195
+ assert created_def.version == 0x8111
196
+
197
+
198
+ def test_switch_joypad_with_custom_device(mock_switch_joypad):
199
+ """Test creating a Switch joypad with custom device definition."""
200
+ custom_def = DeviceDefinition(
201
+ name="Custom Switch",
202
+ vendor_id=0x1234,
203
+ product_id=0x5678,
204
+ version=0x0001,
205
+ )
206
+
207
+ SwitchJoypad(custom_def)
208
+ created_def = mock_switch_joypad.create.call_args[0][0]
209
+
210
+ assert created_def.name == custom_def.name
211
+ assert created_def.vendor_id == custom_def.vendor_id
212
+ assert created_def.product_id == custom_def.product_id
213
+ assert created_def.version == custom_def.version
214
+
215
+
216
+ def test_switch_rumble_callback():
217
+ """Test Switch rumble callback functionality."""
218
+ with patch("inputtino._core.SwitchJoypad") as mock_joypad_cls:
219
+ joypad = SwitchJoypad()
220
+
221
+ callback_called = False
222
+
223
+ def rumble_callback(low, high):
224
+ nonlocal callback_called
225
+ callback_called = True
226
+ assert low == 1000
227
+ assert high == 2000
228
+
229
+ joypad.set_on_rumble(rumble_callback)
230
+
231
+ mock_joypad_cls.create.return_value.set_on_rumble.call_args[0][0](1000, 2000)
232
+ assert callback_called
233
+
234
+
235
+ @mark_practical
236
+ def test_practical_joypad_gameplay():
237
+ """Test gamepad controls in a simulated gameplay scenario.
238
+
239
+ This practical test demonstrates:
240
+ 1. Basic directional movement
241
+ 2. Button combinations
242
+ 3. Trigger and stick input
243
+ 4. Rumble feedback
244
+
245
+ Note:
246
+ This test requires actual device access (/dev/uinput)
247
+ and may fail if run without proper permissions.
248
+ """
249
+ joypad = XBoxOneJoypad()
250
+ assert len(joypad.nodes) > 0, "No joypad nodes created"
251
+
252
+ time.sleep(0.1) # Allow device registration
253
+
254
+ rumble_received = False
255
+
256
+ def on_rumble(low, high):
257
+ nonlocal rumble_received
258
+ rumble_received = True
259
+
260
+ joypad.set_on_rumble(on_rumble)
261
+
262
+ try:
263
+ # Test movement sequence
264
+ movements = [
265
+ (ControllerButton.DPAD_RIGHT, _core.StickPosition.LS, 32767, 0),
266
+ (ControllerButton.DPAD_DOWN, _core.StickPosition.LS, 0, 32767),
267
+ (ControllerButton.DPAD_LEFT, _core.StickPosition.LS, -32767, 0),
268
+ (ControllerButton.DPAD_UP, _core.StickPosition.LS, 0, -32767),
269
+ ]
270
+
271
+ for button, stick, x, y in movements:
272
+ joypad.set_pressed_buttons(button)
273
+ joypad.set_stick(stick, x, y)
274
+ joypad.set_triggers(32767, 32767)
275
+ time.sleep(0.1)
276
+ joypad.set_pressed_buttons(ControllerButton(0))
277
+ time.sleep(0.1)
278
+
279
+ # Test button combinations
280
+ joypad.set_pressed_buttons(
281
+ ControllerButton.A | ControllerButton.B | ControllerButton.X
282
+ )
283
+ time.sleep(0.1)
284
+
285
+ finally:
286
+ joypad.set_pressed_buttons(ControllerButton(0))
287
+ joypad.set_triggers(0, 0)
288
+ joypad.set_stick(_core.StickPosition.LS, 0, 0)
289
+
290
+
291
+ @mark_practical
292
+ def test_practical_ps5_features():
293
+ """Test PS5-specific features in a practical scenario.
294
+
295
+ Tests touchpad, motion sensors, LED, and battery reporting.
296
+
297
+ Note:
298
+ Requires /dev/uinput access.
299
+ """
300
+ joypad = PS5Joypad()
301
+ # NOTE: 2025/02/05 Not working.
302
+ # assert len(joypad.nodes) > 0
303
+
304
+ time.sleep(0.1)
305
+
306
+ led_changed = False
307
+
308
+ def on_led(r, g, b):
309
+ nonlocal led_changed
310
+ led_changed = True
311
+
312
+ joypad.set_on_led(on_led)
313
+
314
+ try:
315
+ # Touchpad gesture
316
+ joypad.place_finger(0, 960, 540)
317
+ time.sleep(0.1)
318
+ joypad.place_finger(0, 1200, 540)
319
+ time.sleep(0.1)
320
+ joypad.release_finger(0)
321
+
322
+ # Motion controls
323
+ joypad.set_motion(PS5MotionType.ACCELERATION, 0.5, 0.1, 9.8)
324
+ time.sleep(0.1)
325
+ joypad.set_motion(PS5MotionType.GYROSCOPE, 45.0, 0.0, 0.0)
326
+ time.sleep(0.1)
327
+
328
+ # Battery update
329
+ joypad.set_battery(PS5BatteryState.BATTERY_FULL, 100)
330
+
331
+ finally:
332
+ joypad.release_finger(0)
@@ -0,0 +1,164 @@
1
+ """Tests for keyboard.py."""
2
+
3
+ import time
4
+ from unittest.mock import MagicMock, patch
5
+
6
+ import pytest
7
+
8
+ from inputtino._core import Keyboard as CoreKeyboard
9
+ from inputtino.base import DeviceDefinition
10
+ from inputtino.keyboard import DEFAULT_KEYBOARD, Keyboard, KeyCode
11
+ from tests.helpers import mark_practical
12
+
13
+
14
+ @pytest.fixture
15
+ def mock_core_keyboard():
16
+ """Create a mock for CoreKeyboard with basic functionality."""
17
+ with patch("inputtino._core.Keyboard") as mock_keyboard_cls:
18
+ mock_instance = MagicMock(spec=CoreKeyboard)
19
+ mock_keyboard_cls.create.return_value = mock_instance
20
+ yield mock_instance
21
+
22
+
23
+ def test_keyboard_creation_with_default_device():
24
+ """Test creating a keyboard with default device definition."""
25
+ with patch("inputtino._core.Keyboard") as mock_keyboard_cls:
26
+ Keyboard()
27
+
28
+ mock_keyboard_cls.create.assert_called_once()
29
+ created_def = mock_keyboard_cls.create.call_args[0][0]
30
+ assert created_def.name == DEFAULT_KEYBOARD.name
31
+ assert created_def.vendor_id == DEFAULT_KEYBOARD.vendor_id
32
+ assert created_def.product_id == DEFAULT_KEYBOARD.product_id
33
+ assert created_def.version == DEFAULT_KEYBOARD.version
34
+
35
+
36
+ def test_keyboard_creation_with_custom_device():
37
+ """Test creating a keyboard with custom device definition."""
38
+ custom_def = DeviceDefinition(
39
+ name="Custom Keyboard",
40
+ vendor_id=0x1234,
41
+ product_id=0x5678,
42
+ version=0x0001,
43
+ device_phys="/dev/input/test",
44
+ device_uniq="unique_id",
45
+ )
46
+
47
+ with patch("inputtino._core.Keyboard") as mock_keyboard_cls:
48
+ Keyboard(custom_def)
49
+
50
+ mock_keyboard_cls.create.assert_called_once()
51
+ created_def = mock_keyboard_cls.create.call_args[0][0]
52
+ assert created_def.name == custom_def.name
53
+ assert created_def.vendor_id == custom_def.vendor_id
54
+ assert created_def.product_id == custom_def.product_id
55
+ assert created_def.version == custom_def.version
56
+ assert created_def.device_phys == custom_def.device_phys
57
+ assert created_def.device_uniq == custom_def.device_uniq
58
+
59
+
60
+ def test_keyboard_custom_repress_time():
61
+ """Test creating a keyboard with custom key repress time."""
62
+ with patch("inputtino._core.Keyboard") as mock_keyboard_cls:
63
+ Keyboard(millis_repress_key=100)
64
+ mock_keyboard_cls.create.assert_called_once()
65
+ assert mock_keyboard_cls.create.call_args[0][1] == 100
66
+
67
+
68
+ def test_keyboard_key_press_and_release(mock_core_keyboard):
69
+ """Test keyboard key press and release."""
70
+ keyboard = Keyboard()
71
+
72
+ # Test single key press/release
73
+ keyboard.press(KeyCode.A)
74
+ mock_core_keyboard.press.assert_called_with(KeyCode.A)
75
+
76
+ keyboard.release(KeyCode.A)
77
+ mock_core_keyboard.release.assert_called_with(KeyCode.A)
78
+
79
+ # Test modifier key press/release
80
+ keyboard.press(KeyCode.SHIFT)
81
+ mock_core_keyboard.press.assert_called_with(KeyCode.SHIFT)
82
+
83
+ keyboard.release(KeyCode.SHIFT)
84
+ mock_core_keyboard.release.assert_called_with(KeyCode.SHIFT)
85
+
86
+
87
+ def test_keyboard_nodes(mock_core_keyboard):
88
+ """Test getting keyboard device nodes."""
89
+ expected_nodes = ["/dev/input/event1"]
90
+ mock_core_keyboard.get_nodes.return_value = expected_nodes
91
+
92
+ keyboard = Keyboard()
93
+ assert keyboard.nodes == expected_nodes
94
+ mock_core_keyboard.get_nodes.assert_called_once()
95
+
96
+
97
+ def test_keyboard_type(mock_core_keyboard):
98
+ """Test keyboard type method."""
99
+ keyboard = Keyboard()
100
+
101
+ with patch("time.sleep") as mock_sleep:
102
+ # Test with default duration
103
+ keyboard.type(KeyCode.A)
104
+ mock_core_keyboard.press.assert_called_with(KeyCode.A)
105
+ mock_sleep.assert_called_with(0.1)
106
+ mock_core_keyboard.release.assert_called_with(KeyCode.A)
107
+
108
+ # Test with custom duration
109
+ keyboard.type(KeyCode.B, 0.5)
110
+ mock_core_keyboard.press.assert_called_with(KeyCode.B)
111
+ mock_sleep.assert_called_with(0.5)
112
+ mock_core_keyboard.release.assert_called_with(KeyCode.B)
113
+
114
+
115
+ def test_keyboard_creation_failure():
116
+ """Test handling of keyboard creation failure."""
117
+ with patch("inputtino._core.Keyboard") as mock_keyboard_cls:
118
+ mock_keyboard_cls.create.side_effect = RuntimeError(
119
+ "Failed to create keyboard device"
120
+ )
121
+
122
+ with pytest.raises(RuntimeError, match="Failed to create keyboard device"):
123
+ Keyboard()
124
+
125
+
126
+ def test_key_code_from_string():
127
+ assert KeyCode.from_str("a") is KeyCode.A
128
+ assert KeyCode.from_str("SHIFT") is KeyCode.SHIFT
129
+
130
+ with pytest.raises(KeyError):
131
+ KeyCode.from_str("")
132
+
133
+
134
+ @mark_practical
135
+ def test_practical_keyboard_input():
136
+ """Test simulating keyboard input with a practical example.
137
+
138
+ This practical test demonstrates:
139
+ 1. Creating a keyboard device
140
+ 2. Simulating Ctrl+A (select all) keyboard shortcut
141
+ 3. Verifying device creation and basic functionality
142
+
143
+ Note:
144
+ This test requires actual device access (/dev/uinput)
145
+ and may fail if run without proper permissions.
146
+ """
147
+ keyboard = Keyboard()
148
+
149
+ # Ensure we have valid device nodes
150
+ assert len(keyboard.nodes) > 0, "No keyboard device nodes created"
151
+
152
+ # Give system time to register the device
153
+ time.sleep(0.1)
154
+
155
+ try:
156
+ # Simulate Ctrl+A (select all)
157
+ keyboard.press(KeyCode.CTRL)
158
+ keyboard.press(KeyCode.A)
159
+ time.sleep(0.1)
160
+ keyboard.release(KeyCode.A)
161
+ keyboard.release(KeyCode.CTRL)
162
+
163
+ except Exception as e:
164
+ raise RuntimeError(f"Failed to simulate keyboard input: {e}") from e
@@ -0,0 +1,185 @@
1
+ """Tests for mouse.py."""
2
+
3
+ import time
4
+ from unittest.mock import MagicMock, patch
5
+
6
+ import pytest
7
+
8
+ from inputtino._core import Mouse as CoreMouse
9
+ from inputtino.mouse import DeviceDefinition, Mouse, MouseButton
10
+ from tests.helpers import mark_practical
11
+
12
+
13
+ @pytest.fixture
14
+ def mock_core_mouse():
15
+ """Create a mock for CoreMouse with basic functionality."""
16
+ with patch("inputtino._core.Mouse") as mock_mouse_cls:
17
+ mock_instance = MagicMock(spec=CoreMouse)
18
+ mock_mouse_cls.create.return_value = mock_instance
19
+ yield mock_instance
20
+
21
+
22
+ def test_mouse_creation_with_default_device():
23
+ """Test creating a mouse with default device definition."""
24
+ with patch("inputtino._core.Mouse") as mock_mouse_cls:
25
+ Mouse()
26
+
27
+ mock_mouse_cls.create.assert_called_once()
28
+ created_def = mock_mouse_cls.create.call_args[0][0]
29
+ assert created_def.name == "Wolf mouse virtual device"
30
+ assert created_def.vendor_id == 0xAB00
31
+ assert created_def.product_id == 0xAB01
32
+ assert created_def.version == 0xAB00
33
+
34
+
35
+ def test_mouse_creation_with_custom_device():
36
+ """Test creating a mouse with custom device definition."""
37
+ custom_def = DeviceDefinition(
38
+ name="Custom Mouse",
39
+ vendor_id=0x1234,
40
+ product_id=0x5678,
41
+ version=0x0001,
42
+ device_phys="/dev/input/test",
43
+ device_uniq="unique_id",
44
+ )
45
+
46
+ with patch("inputtino._core.Mouse") as mock_mouse_cls:
47
+ Mouse(custom_def)
48
+
49
+ mock_mouse_cls.create.assert_called_once()
50
+ created_def = mock_mouse_cls.create.call_args[0][0]
51
+ assert created_def.name == custom_def.name
52
+ assert created_def.vendor_id == custom_def.vendor_id
53
+ assert created_def.product_id == custom_def.product_id
54
+ assert created_def.version == custom_def.version
55
+ assert created_def.device_phys == custom_def.device_phys
56
+ assert created_def.device_uniq == custom_def.device_uniq
57
+
58
+
59
+ def test_mouse_movement(mock_core_mouse):
60
+ """Test relative mouse movement."""
61
+ mouse = Mouse()
62
+ mouse.move(100, -50)
63
+
64
+ mock_core_mouse.move.assert_called_once_with(100, -50)
65
+
66
+
67
+ def test_mouse_absolute_movement(mock_core_mouse):
68
+ """Test absolute mouse movement."""
69
+ mouse = Mouse()
70
+ mouse.move_abs(1920, 1080, 3840, 2160)
71
+
72
+ mock_core_mouse.move_abs.assert_called_once_with(1920, 1080, 3840, 2160)
73
+
74
+
75
+ def test_mouse_button_press_and_release(mock_core_mouse):
76
+ """Test mouse button press and release."""
77
+ mouse = Mouse()
78
+
79
+ # Test pressing each button
80
+ for button in [
81
+ MouseButton.EXTRA,
82
+ MouseButton.LEFT,
83
+ MouseButton.RIGHT,
84
+ MouseButton.MIDDLE,
85
+ MouseButton.SIDE,
86
+ ]:
87
+ mouse.press(button)
88
+ mock_core_mouse.press.assert_called_with(button)
89
+
90
+ mouse.release(button)
91
+ mock_core_mouse.release.assert_called_with(button)
92
+
93
+
94
+ def test_mouse_scrolling(mock_core_mouse):
95
+ """Test mouse scrolling in both directions."""
96
+ mouse = Mouse()
97
+
98
+ # Test vertical scrolling
99
+ mouse.scroll_vertical(120)
100
+ mock_core_mouse.vertical_scroll.assert_called_once_with(120)
101
+
102
+ # Test horizontal scrolling
103
+ mouse.scroll_horizontal(-120)
104
+ mock_core_mouse.horizontal_scroll.assert_called_once_with(-120)
105
+
106
+
107
+ def test_mouse_nodes(mock_core_mouse):
108
+ """Test getting mouse device nodes."""
109
+ expected_nodes = ["/dev/input/event0", "/dev/input/mouse0"]
110
+ mock_core_mouse.get_nodes.return_value = expected_nodes
111
+
112
+ mouse = Mouse()
113
+ assert mouse.nodes == expected_nodes
114
+ mock_core_mouse.get_nodes.assert_called_once()
115
+
116
+
117
+ def test_click(mock_core_mouse):
118
+ """Test single click functionality."""
119
+ mouse = Mouse()
120
+ with patch("time.sleep") as mock_sleep:
121
+ mouse.click(MouseButton.LEFT, 0.5)
122
+
123
+ mock_core_mouse.press.assert_called_once_with(MouseButton.LEFT)
124
+ mock_sleep.assert_called_once_with(0.5)
125
+ mock_core_mouse.release.assert_called_once_with(MouseButton.LEFT)
126
+
127
+
128
+ def test_mouse_creation_failure():
129
+ """Test handling of mouse creation failure."""
130
+ with patch("inputtino._core.Mouse") as mock_mouse_cls:
131
+ mock_mouse_cls.create.side_effect = RuntimeError(
132
+ "Failed to create mouse device"
133
+ )
134
+
135
+ with pytest.raises(RuntimeError, match="Failed to create mouse device"):
136
+ Mouse()
137
+
138
+
139
+ @mark_practical
140
+ def test_practical_mouse_draw_square():
141
+ """Test drawing a square pattern with the mouse.
142
+
143
+ This practical test demonstrates drawing a square pattern by:
144
+ 1. Moving the mouse in a square pattern
145
+ 2. Holding left button during the movement
146
+ 3. Creating a visible mouse movement pattern
147
+
148
+ Note:
149
+ This test requires actual device access (/dev/uinput)
150
+ and may fail if run without proper permissions.
151
+ """
152
+ mouse = Mouse()
153
+
154
+ # Ensure we have valid device nodes
155
+ assert len(mouse.nodes) > 0, "No mouse device nodes created"
156
+
157
+ # Give system time to register the device
158
+ time.sleep(0.1)
159
+
160
+ # Start at center
161
+ center_x, center_y = 960, 540 # Assuming 1920x1080 screen
162
+ square_size = 200
163
+
164
+ mouse.move_abs(center_x - square_size // 2, center_y - square_size // 2, 1920, 1080)
165
+ mouse.press(MouseButton.LEFT)
166
+
167
+ try:
168
+ # Draw square pattern
169
+ movements = [
170
+ (square_size, 0), # Right
171
+ (0, square_size), # Down
172
+ (-square_size, 0), # Left
173
+ (0, -square_size), # Up
174
+ ]
175
+
176
+ for dx, dy in movements:
177
+ # Move slowly to create visible pattern
178
+ steps = 10
179
+ for _ in range(steps):
180
+ mouse.move(dx // steps, dy // steps)
181
+ time.sleep(0.01) # Small delay for smooth movement
182
+
183
+ finally:
184
+ # Ensure we always release the button
185
+ mouse.release(MouseButton.LEFT)