gemba 0.1.1 → 0.2.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 (285) hide show
  1. checksums.yaml +4 -4
  2. data/THIRD_PARTY_NOTICES +37 -2
  3. data/assets/placeholder_boxart.png +0 -0
  4. data/bin/gemba +2 -2
  5. data/ext/gemba/extconf.rb +23 -1
  6. data/ext/gemba/gemba_ext.c +436 -2
  7. data/ext/gemba/gemba_ext.h +2 -0
  8. data/gemba.gemspec +5 -3
  9. data/lib/gemba/achievements/achievement.rb +23 -0
  10. data/lib/gemba/achievements/backend.rb +186 -0
  11. data/lib/gemba/achievements/cache.rb +70 -0
  12. data/lib/gemba/achievements/credentials_presenter.rb +142 -0
  13. data/lib/gemba/achievements/fake_backend.rb +205 -0
  14. data/lib/gemba/achievements/null_backend.rb +11 -0
  15. data/lib/gemba/achievements/offline_backend.rb +168 -0
  16. data/lib/gemba/achievements/retro_achievements/backend.rb +453 -0
  17. data/lib/gemba/achievements/retro_achievements/cli_sync_requester.rb +64 -0
  18. data/lib/gemba/achievements/retro_achievements/ping_worker.rb +27 -0
  19. data/lib/gemba/achievements.rb +19 -0
  20. data/lib/gemba/achievements_window.rb +556 -0
  21. data/lib/gemba/app_controller.rb +1015 -0
  22. data/lib/gemba/bios.rb +54 -0
  23. data/lib/gemba/boxart_fetcher/libretro_backend.rb +39 -0
  24. data/lib/gemba/boxart_fetcher/null_backend.rb +12 -0
  25. data/lib/gemba/boxart_fetcher.rb +79 -0
  26. data/lib/gemba/bus_emitter.rb +13 -0
  27. data/lib/gemba/child_window.rb +24 -1
  28. data/lib/gemba/cli/commands/config_cmd.rb +83 -0
  29. data/lib/gemba/cli/commands/decode.rb +154 -0
  30. data/lib/gemba/cli/commands/patch.rb +78 -0
  31. data/lib/gemba/cli/commands/play.rb +78 -0
  32. data/lib/gemba/cli/commands/record.rb +114 -0
  33. data/lib/gemba/cli/commands/replay.rb +161 -0
  34. data/lib/gemba/cli/commands/retro_achievements.rb +213 -0
  35. data/lib/gemba/cli/commands/version.rb +22 -0
  36. data/lib/gemba/cli.rb +52 -364
  37. data/lib/gemba/config.rb +134 -1
  38. data/lib/gemba/data/gb_games.json +1 -0
  39. data/lib/gemba/data/gb_md5.json +1 -0
  40. data/lib/gemba/data/gba_games.json +1 -0
  41. data/lib/gemba/data/gba_md5.json +1 -0
  42. data/lib/gemba/data/gbc_games.json +1 -0
  43. data/lib/gemba/data/gbc_md5.json +1 -0
  44. data/lib/gemba/emulator_frame.rb +1060 -0
  45. data/lib/gemba/event_bus.rb +48 -0
  46. data/lib/gemba/frame_stack.rb +60 -0
  47. data/lib/gemba/game_index.rb +84 -0
  48. data/lib/gemba/game_picker_frame.rb +268 -0
  49. data/lib/gemba/gamepad_map.rb +103 -0
  50. data/lib/gemba/headless.rb +6 -5
  51. data/lib/gemba/headless_player.rb +33 -3
  52. data/lib/gemba/help_window.rb +61 -0
  53. data/lib/gemba/hotkey_map.rb +3 -1
  54. data/lib/gemba/input_recorder.rb +107 -0
  55. data/lib/gemba/input_replayer.rb +119 -0
  56. data/lib/gemba/keyboard_map.rb +90 -0
  57. data/lib/gemba/locales/en.yml +97 -5
  58. data/lib/gemba/locales/ja.yml +97 -5
  59. data/lib/gemba/main_window.rb +56 -0
  60. data/lib/gemba/modal_stack.rb +81 -0
  61. data/lib/gemba/patcher_window.rb +223 -0
  62. data/lib/gemba/platform/gb.rb +21 -0
  63. data/lib/gemba/platform/gba.rb +21 -0
  64. data/lib/gemba/platform/gbc.rb +23 -0
  65. data/lib/gemba/platform.rb +20 -0
  66. data/lib/gemba/platform_open.rb +19 -0
  67. data/lib/gemba/recorder.rb +4 -3
  68. data/lib/gemba/replay_player.rb +691 -0
  69. data/lib/gemba/rom_info.rb +57 -0
  70. data/lib/gemba/rom_info_window.rb +16 -3
  71. data/lib/gemba/rom_library.rb +106 -0
  72. data/lib/gemba/rom_overrides.rb +47 -0
  73. data/lib/gemba/rom_patcher/bps.rb +161 -0
  74. data/lib/gemba/rom_patcher/ips.rb +101 -0
  75. data/lib/gemba/rom_patcher/ups.rb +118 -0
  76. data/lib/gemba/rom_patcher.rb +109 -0
  77. data/lib/gemba/{rom_loader.rb → rom_resolver.rb} +7 -6
  78. data/lib/gemba/runtime.rb +59 -26
  79. data/lib/gemba/save_state_manager.rb +4 -7
  80. data/lib/gemba/save_state_picker.rb +17 -4
  81. data/lib/gemba/session_logger.rb +64 -0
  82. data/lib/gemba/settings/audio_tab.rb +77 -0
  83. data/lib/gemba/settings/gamepad_tab.rb +351 -0
  84. data/lib/gemba/settings/hotkeys_tab.rb +259 -0
  85. data/lib/gemba/settings/paths.rb +11 -0
  86. data/lib/gemba/settings/recording_tab.rb +83 -0
  87. data/lib/gemba/settings/save_states_tab.rb +91 -0
  88. data/lib/gemba/settings/system_tab.rb +362 -0
  89. data/lib/gemba/settings/video_tab.rb +318 -0
  90. data/lib/gemba/settings_window.rb +162 -1036
  91. data/lib/gemba/version.rb +1 -1
  92. data/lib/gemba/virtual_keyboard.rb +19 -0
  93. data/lib/gemba.rb +2 -12
  94. data/test/achievements_window/test_bulk_sync.rb +218 -0
  95. data/test/achievements_window/test_bus_events.rb +125 -0
  96. data/test/achievements_window/test_close_confirmation.rb +201 -0
  97. data/test/achievements_window/test_initial_state.rb +164 -0
  98. data/test/achievements_window/test_sorting.rb +227 -0
  99. data/test/achievements_window/test_tree_rendering.rb +133 -0
  100. data/test/fixtures/fake_bios.bin +0 -0
  101. data/test/fixtures/pong.gba +0 -0
  102. data/test/fixtures/test.gb +0 -0
  103. data/test/fixtures/test.gbc +0 -0
  104. data/test/fixtures/test_quicksave.ss +0 -0
  105. data/test/screenshots/no_focus.png +0 -0
  106. data/test/shared/teek_test_worker.rb +17 -1
  107. data/test/shared/tk_test_helper.rb +91 -4
  108. data/test/support/achievements_window_helpers.rb +18 -0
  109. data/test/support/fake_core.rb +25 -0
  110. data/test/support/fake_ra_runtime.rb +74 -0
  111. data/test/support/fake_requester.rb +68 -0
  112. data/test/support/player_helpers.rb +20 -5
  113. data/test/test_achievement.rb +32 -0
  114. data/test/{test_player.rb → test_app_controller.rb} +353 -85
  115. data/test/test_bios.rb +123 -0
  116. data/test/test_boxart_fetcher.rb +150 -0
  117. data/test/test_cli.rb +17 -265
  118. data/test/test_cli_config.rb +64 -0
  119. data/test/test_cli_decode.rb +97 -0
  120. data/test/test_cli_patch.rb +58 -0
  121. data/test/test_cli_play.rb +213 -0
  122. data/test/test_cli_ra.rb +175 -0
  123. data/test/test_cli_record.rb +69 -0
  124. data/test/test_cli_replay.rb +72 -0
  125. data/test/test_cli_sync_requester.rb +152 -0
  126. data/test/test_cli_version.rb +27 -0
  127. data/test/test_config.rb +2 -3
  128. data/test/test_config_ra.rb +69 -0
  129. data/test/test_core.rb +62 -1
  130. data/test/test_credentials_presenter.rb +192 -0
  131. data/test/test_event_bus.rb +100 -0
  132. data/test/test_fake_backend_achievements.rb +130 -0
  133. data/test/test_fake_backend_auth.rb +68 -0
  134. data/test/test_game_index.rb +77 -0
  135. data/test/test_game_picker_frame.rb +310 -0
  136. data/test/test_gamepad_map.rb +1 -3
  137. data/test/test_headless_player.rb +17 -3
  138. data/test/test_help_window.rb +82 -0
  139. data/test/test_hotkey_map.rb +22 -1
  140. data/test/test_input_recorder.rb +179 -0
  141. data/test/test_input_replay_determinism.rb +113 -0
  142. data/test/test_input_replayer.rb +162 -0
  143. data/test/test_keyboard_map.rb +1 -3
  144. data/test/test_libretro_backend.rb +41 -0
  145. data/test/test_locale.rb +1 -1
  146. data/test/test_logging.rb +123 -0
  147. data/test/test_null_backend.rb +42 -0
  148. data/test/test_offline_backend.rb +116 -0
  149. data/test/test_overlay_renderer.rb +1 -1
  150. data/test/test_platform.rb +149 -0
  151. data/test/test_ra_backend.rb +313 -0
  152. data/test/test_ra_backend_unlock_gate.rb +56 -0
  153. data/test/test_recorder.rb +0 -3
  154. data/test/test_replay_player.rb +316 -0
  155. data/test/test_rom_info.rb +149 -0
  156. data/test/test_rom_overrides.rb +86 -0
  157. data/test/test_rom_patcher.rb +382 -0
  158. data/test/{test_rom_loader.rb → test_rom_resolver.rb} +25 -26
  159. data/test/test_save_state_manager.rb +2 -4
  160. data/test/test_settings_audio.rb +107 -0
  161. data/test/test_settings_hotkeys.rb +83 -66
  162. data/test/test_settings_recording.rb +49 -0
  163. data/test/test_settings_save_states.rb +97 -0
  164. data/test/test_settings_system.rb +133 -0
  165. data/test/test_settings_video.rb +450 -0
  166. data/test/test_settings_window.rb +76 -507
  167. data/test/test_tip_service.rb +6 -6
  168. data/test/test_toast_overlay.rb +1 -1
  169. data/test/test_virtual_events.rb +156 -0
  170. data/test/test_virtual_keyboard.rb +1 -1
  171. data/vendor/rcheevos/CHANGELOG.md +495 -0
  172. data/vendor/rcheevos/LICENSE +21 -0
  173. data/vendor/rcheevos/Package.swift +33 -0
  174. data/vendor/rcheevos/README.md +67 -0
  175. data/vendor/rcheevos/include/module.modulemap +70 -0
  176. data/vendor/rcheevos/include/rc_api_editor.h +296 -0
  177. data/vendor/rcheevos/include/rc_api_info.h +280 -0
  178. data/vendor/rcheevos/include/rc_api_request.h +77 -0
  179. data/vendor/rcheevos/include/rc_api_runtime.h +417 -0
  180. data/vendor/rcheevos/include/rc_api_user.h +262 -0
  181. data/vendor/rcheevos/include/rc_client.h +877 -0
  182. data/vendor/rcheevos/include/rc_client_raintegration.h +101 -0
  183. data/vendor/rcheevos/include/rc_consoles.h +138 -0
  184. data/vendor/rcheevos/include/rc_error.h +59 -0
  185. data/vendor/rcheevos/include/rc_export.h +100 -0
  186. data/vendor/rcheevos/include/rc_hash.h +200 -0
  187. data/vendor/rcheevos/include/rc_runtime.h +148 -0
  188. data/vendor/rcheevos/include/rc_runtime_types.h +452 -0
  189. data/vendor/rcheevos/include/rc_util.h +51 -0
  190. data/vendor/rcheevos/include/rcheevos.h +8 -0
  191. data/vendor/rcheevos/src/rapi/rc_api_common.c +1379 -0
  192. data/vendor/rcheevos/src/rapi/rc_api_common.h +88 -0
  193. data/vendor/rcheevos/src/rapi/rc_api_editor.c +625 -0
  194. data/vendor/rcheevos/src/rapi/rc_api_info.c +587 -0
  195. data/vendor/rcheevos/src/rapi/rc_api_runtime.c +901 -0
  196. data/vendor/rcheevos/src/rapi/rc_api_user.c +483 -0
  197. data/vendor/rcheevos/src/rc_client.c +6941 -0
  198. data/vendor/rcheevos/src/rc_client_external.c +281 -0
  199. data/vendor/rcheevos/src/rc_client_external.h +177 -0
  200. data/vendor/rcheevos/src/rc_client_external_versions.h +171 -0
  201. data/vendor/rcheevos/src/rc_client_internal.h +409 -0
  202. data/vendor/rcheevos/src/rc_client_raintegration.c +566 -0
  203. data/vendor/rcheevos/src/rc_client_raintegration_internal.h +61 -0
  204. data/vendor/rcheevos/src/rc_client_types.natvis +396 -0
  205. data/vendor/rcheevos/src/rc_compat.c +251 -0
  206. data/vendor/rcheevos/src/rc_compat.h +121 -0
  207. data/vendor/rcheevos/src/rc_libretro.c +915 -0
  208. data/vendor/rcheevos/src/rc_libretro.h +98 -0
  209. data/vendor/rcheevos/src/rc_util.c +199 -0
  210. data/vendor/rcheevos/src/rc_version.c +11 -0
  211. data/vendor/rcheevos/src/rc_version.h +32 -0
  212. data/vendor/rcheevos/src/rcheevos/alloc.c +312 -0
  213. data/vendor/rcheevos/src/rcheevos/condition.c +754 -0
  214. data/vendor/rcheevos/src/rcheevos/condset.c +777 -0
  215. data/vendor/rcheevos/src/rcheevos/consoleinfo.c +1215 -0
  216. data/vendor/rcheevos/src/rcheevos/format.c +330 -0
  217. data/vendor/rcheevos/src/rcheevos/lboard.c +287 -0
  218. data/vendor/rcheevos/src/rcheevos/memref.c +805 -0
  219. data/vendor/rcheevos/src/rcheevos/operand.c +607 -0
  220. data/vendor/rcheevos/src/rcheevos/rc_internal.h +390 -0
  221. data/vendor/rcheevos/src/rcheevos/rc_runtime_types.natvis +541 -0
  222. data/vendor/rcheevos/src/rcheevos/rc_validate.c +1406 -0
  223. data/vendor/rcheevos/src/rcheevos/rc_validate.h +18 -0
  224. data/vendor/rcheevos/src/rcheevos/richpresence.c +922 -0
  225. data/vendor/rcheevos/src/rcheevos/runtime.c +852 -0
  226. data/vendor/rcheevos/src/rcheevos/runtime_progress.c +1073 -0
  227. data/vendor/rcheevos/src/rcheevos/trigger.c +344 -0
  228. data/vendor/rcheevos/src/rcheevos/value.c +935 -0
  229. data/vendor/rcheevos/src/rhash/aes.c +480 -0
  230. data/vendor/rcheevos/src/rhash/aes.h +49 -0
  231. data/vendor/rcheevos/src/rhash/cdreader.c +838 -0
  232. data/vendor/rcheevos/src/rhash/hash.c +1402 -0
  233. data/vendor/rcheevos/src/rhash/hash_disc.c +1340 -0
  234. data/vendor/rcheevos/src/rhash/hash_encrypted.c +566 -0
  235. data/vendor/rcheevos/src/rhash/hash_rom.c +426 -0
  236. data/vendor/rcheevos/src/rhash/hash_zip.c +460 -0
  237. data/vendor/rcheevos/src/rhash/md5.c +382 -0
  238. data/vendor/rcheevos/src/rhash/md5.h +91 -0
  239. data/vendor/rcheevos/src/rhash/rc_hash_internal.h +116 -0
  240. data/vendor/rcheevos/test/libretro.h +205 -0
  241. data/vendor/rcheevos/test/rapi/test_rc_api_common.c +941 -0
  242. data/vendor/rcheevos/test/rapi/test_rc_api_editor.c +931 -0
  243. data/vendor/rcheevos/test/rapi/test_rc_api_info.c +545 -0
  244. data/vendor/rcheevos/test/rapi/test_rc_api_runtime.c +2213 -0
  245. data/vendor/rcheevos/test/rapi/test_rc_api_user.c +998 -0
  246. data/vendor/rcheevos/test/rcheevos/mock_memory.h +32 -0
  247. data/vendor/rcheevos/test/rcheevos/test_condition.c +570 -0
  248. data/vendor/rcheevos/test/rcheevos/test_condset.c +5170 -0
  249. data/vendor/rcheevos/test/rcheevos/test_consoleinfo.c +203 -0
  250. data/vendor/rcheevos/test/rcheevos/test_format.c +112 -0
  251. data/vendor/rcheevos/test/rcheevos/test_lboard.c +746 -0
  252. data/vendor/rcheevos/test/rcheevos/test_memref.c +520 -0
  253. data/vendor/rcheevos/test/rcheevos/test_operand.c +692 -0
  254. data/vendor/rcheevos/test/rcheevos/test_rc_validate.c +502 -0
  255. data/vendor/rcheevos/test/rcheevos/test_richpresence.c +1564 -0
  256. data/vendor/rcheevos/test/rcheevos/test_runtime.c +1667 -0
  257. data/vendor/rcheevos/test/rcheevos/test_runtime_progress.c +1821 -0
  258. data/vendor/rcheevos/test/rcheevos/test_timing.c +166 -0
  259. data/vendor/rcheevos/test/rcheevos/test_trigger.c +2521 -0
  260. data/vendor/rcheevos/test/rcheevos/test_value.c +870 -0
  261. data/vendor/rcheevos/test/rcheevos-test.sln +46 -0
  262. data/vendor/rcheevos/test/rcheevos-test.vcxproj +239 -0
  263. data/vendor/rcheevos/test/rcheevos-test.vcxproj.filters +335 -0
  264. data/vendor/rcheevos/test/rhash/data.c +657 -0
  265. data/vendor/rcheevos/test/rhash/data.h +32 -0
  266. data/vendor/rcheevos/test/rhash/mock_filereader.c +236 -0
  267. data/vendor/rcheevos/test/rhash/mock_filereader.h +31 -0
  268. data/vendor/rcheevos/test/rhash/test_cdreader.c +920 -0
  269. data/vendor/rcheevos/test/rhash/test_hash.c +310 -0
  270. data/vendor/rcheevos/test/rhash/test_hash_disc.c +1450 -0
  271. data/vendor/rcheevos/test/rhash/test_hash_rom.c +899 -0
  272. data/vendor/rcheevos/test/rhash/test_hash_zip.c +551 -0
  273. data/vendor/rcheevos/test/test.c +113 -0
  274. data/vendor/rcheevos/test/test_framework.h +205 -0
  275. data/vendor/rcheevos/test/test_rc_client.c +10509 -0
  276. data/vendor/rcheevos/test/test_rc_client_external.c +2197 -0
  277. data/vendor/rcheevos/test/test_rc_client_raintegration.c +441 -0
  278. data/vendor/rcheevos/test/test_rc_libretro.c +952 -0
  279. data/vendor/rcheevos/test/test_types.natvis +9 -0
  280. data/vendor/rcheevos/validator/validator.c +658 -0
  281. data/vendor/rcheevos/validator/validator.vcxproj +152 -0
  282. data/vendor/rcheevos/validator/validator.vcxproj.filters +82 -0
  283. metadata +274 -11
  284. data/lib/gemba/input_mappings.rb +0 -214
  285. data/lib/gemba/player.rb +0 -1525
@@ -8,7 +8,7 @@ class TestTipService < Minitest::Test
8
8
 
9
9
  def test_register_sets_underline_font
10
10
  assert_tk_app("register sets underlined font") do
11
- require "gemba/tip_service"
11
+ require "gemba/headless"
12
12
  tips = Gemba::TipService.new(app)
13
13
 
14
14
  lbl = ".test_lbl"
@@ -27,7 +27,7 @@ class TestTipService < Minitest::Test
27
27
 
28
28
  def test_show_creates_tooltip
29
29
  assert_tk_app("show creates a tooltip toplevel") do
30
- require "gemba/tip_service"
30
+ require "gemba/headless"
31
31
  tips = Gemba::TipService.new(app)
32
32
 
33
33
  lbl = ".test_lbl"
@@ -47,7 +47,7 @@ class TestTipService < Minitest::Test
47
47
 
48
48
  def test_hide_destroys_tooltip
49
49
  assert_tk_app("hide destroys tooltip") do
50
- require "gemba/tip_service"
50
+ require "gemba/headless"
51
51
  tips = Gemba::TipService.new(app)
52
52
 
53
53
  lbl = ".test_lbl"
@@ -69,7 +69,7 @@ class TestTipService < Minitest::Test
69
69
 
70
70
  def test_toggle_behavior
71
71
  assert_tk_app("clicking same label toggles tooltip") do
72
- require "gemba/tip_service"
72
+ require "gemba/headless"
73
73
  tips = Gemba::TipService.new(app)
74
74
 
75
75
  lbl = ".test_lbl"
@@ -96,7 +96,7 @@ class TestTipService < Minitest::Test
96
96
 
97
97
  def test_only_one_tooltip_at_a_time
98
98
  assert_tk_app("showing a second tooltip hides the first") do
99
- require "gemba/tip_service"
99
+ require "gemba/headless"
100
100
  tips = Gemba::TipService.new(app)
101
101
 
102
102
  lbl1 = ".test_lbl1"
@@ -126,7 +126,7 @@ class TestTipService < Minitest::Test
126
126
 
127
127
  def test_dismiss_ms_is_configurable
128
128
  assert_tk_app("dismiss_ms is configurable") do
129
- require "gemba/tip_service"
129
+ require "gemba/headless"
130
130
  tips = Gemba::TipService.new(app, dismiss_ms: 2000)
131
131
 
132
132
  assert_equal 2000, tips.dismiss_ms
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "minitest/autorun"
4
- require_relative "../lib/gemba/toast_overlay"
4
+ require "gemba/headless"
5
5
 
6
6
  class TestToastOverlay < Minitest::Test
7
7
  # Minimal texture mock — records destroy calls.
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require_relative "shared/tk_test_helper"
5
+ require "gemba"
6
+
7
+ # Tests for the virtual event layer.
8
+ #
9
+ # Physical key → virtual event translation is one line per action; the
10
+ # interesting thing to test is that the virtual event bindings on '.'
11
+ # actually fire when triggered directly (no focus needed).
12
+ #
13
+ # assert_virtual_event_fires runs a subprocess that:
14
+ # 1. Appends {+lappend ::virt_fired EventName} to the binding on '.'
15
+ # 2. Generates the virtual event from an after(50) callback
16
+ # 3. In an after(0) reads ::virt_fired and prints EVENTNAME_FIRED
17
+ # 4. Quits via <<Quit>> (or player.running = false for no-ROM cases)
18
+ #
19
+ # <<Quit>> is special — its original binding stops the mainloop before
20
+ # after(0) can run, so it prints synchronously inside the appended binding.
21
+ class TestVirtualEvents < Minitest::Test
22
+ include TeekTestHelper
23
+
24
+ TEST_ROM = File.expand_path("fixtures/test.gba", __dir__)
25
+
26
+ # ── Key → action mapping (pure unit tests, no subprocess) ─────────────────
27
+
28
+ def test_default_quit_key_maps_to_quit_action
29
+ assert_equal :quit, Gemba::HotkeyMap.new(Gemba::Config.new).action_for('q')
30
+ end
31
+
32
+ def test_default_quick_save_key_maps_to_quick_save_action
33
+ assert_equal :quick_save, Gemba::HotkeyMap.new(Gemba::Config.new).action_for('F5')
34
+ end
35
+
36
+ def test_default_quick_load_key_maps_to_quick_load_action
37
+ assert_equal :quick_load, Gemba::HotkeyMap.new(Gemba::Config.new).action_for('F8')
38
+ end
39
+
40
+ def test_default_record_key_maps_to_record_action
41
+ assert_equal :record, Gemba::HotkeyMap.new(Gemba::Config.new).action_for('F10')
42
+ end
43
+
44
+ # ── Virtual event bindings (subprocess, no physical keypresses) ────────────
45
+
46
+ def test_quit_virtual_event_fires_binding
47
+ # <<Quit>> stops the mainloop before after(0) runs — print synchronously
48
+ # inside the appended binding instead.
49
+ code = <<~RUBY
50
+ require "gemba"
51
+ require "support/player_helpers"
52
+
53
+ player = Gemba::AppController.new("#{TEST_ROM}")
54
+ player.disable_confirmations!
55
+ app = player.app
56
+
57
+ poll_until_ready(player) do
58
+ app.tcl_eval('bind . <<Quit>> {+puts QUIT_FIRED; flush stdout}')
59
+ app.after(50) { app.command(:event, 'generate', '.', '<<Quit>>') }
60
+ end
61
+
62
+ player.run
63
+ RUBY
64
+
65
+ _, stdout, stderr, _ = tk_subprocess(code)
66
+ output = ["STDOUT:\n#{stdout}", "STDERR:\n#{stderr}"].reject { |s| s.end_with?("\n") }
67
+ assert_includes stdout, "QUIT_FIRED", "<<Quit>> binding did not fire\n#{output.join("\n")}"
68
+ end
69
+
70
+ def test_quick_save_virtual_event_fires_binding
71
+ assert_virtual_event_fires('QuickSave',
72
+ setup_code: 'player.config.states_dir = Dir.mktmpdir("gemba-virt-test")',
73
+ cleanup_code: 'FileUtils.rm_rf(player.config.states_dir)')
74
+ end
75
+
76
+ def test_quick_load_virtual_event_fires_binding
77
+ assert_virtual_event_fires('QuickLoad')
78
+ end
79
+
80
+ def test_record_toggle_virtual_event_fires_binding
81
+ assert_virtual_event_fires('RecordToggle',
82
+ setup_code: 'player.config.recordings_dir = Dir.mktmpdir("gemba-virt-rec")',
83
+ cleanup_code: 'FileUtils.rm_rf(player.config.recordings_dir)')
84
+ end
85
+
86
+ def test_toggle_help_window_virtual_event_fires_binding
87
+ assert_virtual_event_fires('ToggleHelpWindow', with_rom: false)
88
+ end
89
+
90
+ private
91
+
92
+ # Verifies that generating <<EventName>> on '.' fires the binding.
93
+ # Uses lappend ::virt_fired as a Tcl side-channel — no physical keypresses.
94
+ #
95
+ # Options:
96
+ # with_rom: load TEST_ROM (default true; false for no-ROM app controller)
97
+ # setup_code: Ruby injected after player/app created, before poll_until_ready
98
+ # cleanup_code: Ruby injected in after(0) before quitting
99
+ def assert_virtual_event_fires(event_name, with_rom: true, setup_code: '', cleanup_code: '')
100
+ marker = "#{event_name.upcase.tr('-', '_')}_FIRED"
101
+
102
+ if with_rom
103
+ code = <<~RUBY
104
+ require "gemba"
105
+ require "support/player_helpers"
106
+ require "tmpdir"
107
+ require "fileutils"
108
+
109
+ player = Gemba::AppController.new("#{TEST_ROM}")
110
+ player.disable_confirmations!
111
+ app = player.app
112
+ #{setup_code}
113
+
114
+ poll_until_ready(player) do
115
+ app.tcl_eval('bind . <<#{event_name}>> {+lappend ::virt_fired #{event_name}}')
116
+ app.after(50) do
117
+ app.command(:event, 'generate', '.', '<<#{event_name}>>')
118
+ app.after(0) do
119
+ fired = app.tcl_eval('lsearch -exact $::virt_fired #{event_name}') rescue "-1"
120
+ puts fired.to_i >= 0 ? "#{marker}" : "NOT_FIRED"
121
+ #{cleanup_code}
122
+ app.command(:event, 'generate', '.', '<<Quit>>')
123
+ end
124
+ end
125
+ end
126
+
127
+ player.run
128
+ RUBY
129
+ else
130
+ code = <<~RUBY
131
+ require "gemba"
132
+
133
+ player = Gemba::AppController.new
134
+ app = player.app
135
+ #{setup_code}
136
+
137
+ app.tcl_eval('bind . <<#{event_name}>> {+lappend ::virt_fired #{event_name}}')
138
+ app.after(50) do
139
+ app.command(:event, 'generate', '.', '<<#{event_name}>>')
140
+ app.after(0) do
141
+ fired = app.tcl_eval('lsearch -exact $::virt_fired #{event_name}') rescue "-1"
142
+ puts fired.to_i >= 0 ? "#{marker}" : "NOT_FIRED"
143
+ #{cleanup_code}
144
+ player.running = false
145
+ end
146
+ end
147
+
148
+ player.run
149
+ RUBY
150
+ end
151
+
152
+ _, stdout, stderr, _ = tk_subprocess(code)
153
+ output = ["STDOUT:\n#{stdout}", "STDERR:\n#{stderr}"].reject { |s| s.end_with?("\n") }
154
+ assert_includes stdout, marker, "<<#{event_name}>> binding did not fire\n#{output.join("\n")}"
155
+ end
156
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "minitest/autorun"
4
- require_relative "../lib/gemba/input_mappings"
4
+ require "gemba/headless"
5
5
 
6
6
  class TestVirtualKeyboard < Minitest::Test
7
7
  def setup