shoko 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 (294) hide show
  1. checksums.yaml +7 -0
  2. data/.bundle/config +4 -0
  3. data/.bundle/config.bak +3 -0
  4. data/.rspec_status +42 -0
  5. data/.rubocop.yml +124 -0
  6. data/Gemfile +19 -0
  7. data/LICENSE +21 -0
  8. data/README.md +82 -0
  9. data/Rakefile +29 -0
  10. data/bin/start +15 -0
  11. data/lib/shoko/adapters/book_sources/document_service.rb +201 -0
  12. data/lib/shoko/adapters/book_sources/download_service.rb +95 -0
  13. data/lib/shoko/adapters/book_sources/epub/epub_resource_loader.rb +137 -0
  14. data/lib/shoko/adapters/book_sources/epub/parsers/html_processor.rb +151 -0
  15. data/lib/shoko/adapters/book_sources/epub/parsers/metadata_extractor.rb +53 -0
  16. data/lib/shoko/adapters/book_sources/epub/parsers/opf/entry_reader.rb +77 -0
  17. data/lib/shoko/adapters/book_sources/epub/parsers/opf/metadata_extractor.rb +67 -0
  18. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_context.rb +86 -0
  19. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_document_index.rb +75 -0
  20. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_document_scanner.rb +47 -0
  21. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_extractor.rb +46 -0
  22. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_label_resolver.rb +83 -0
  23. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_list_item.rb +55 -0
  24. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_result.rb +8 -0
  25. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_selector.rb +100 -0
  26. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_source_locator.rb +93 -0
  27. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_traversal.rb +103 -0
  28. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_walker.rb +56 -0
  29. data/lib/shoko/adapters/book_sources/epub/parsers/opf_processor.rb +102 -0
  30. data/lib/shoko/adapters/book_sources/epub/parsers/xhtml_content_parser.rb +661 -0
  31. data/lib/shoko/adapters/book_sources/epub/parsers/xml_text_normalizer.rb +41 -0
  32. data/lib/shoko/adapters/book_sources/epub_document.rb +253 -0
  33. data/lib/shoko/adapters/book_sources/epub_finder/directory_scanner.rb +134 -0
  34. data/lib/shoko/adapters/book_sources/epub_finder/scanner_context.rb +28 -0
  35. data/lib/shoko/adapters/book_sources/epub_finder.rb +161 -0
  36. data/lib/shoko/adapters/book_sources/epub_importer.rb +268 -0
  37. data/lib/shoko/adapters/book_sources/gutendex_client.rb +150 -0
  38. data/lib/shoko/adapters/book_sources/library_scanner.rb +93 -0
  39. data/lib/shoko/adapters/book_sources/source_fingerprint.rb +57 -0
  40. data/lib/shoko/adapters/input/annotations/mouse_handler.rb +84 -0
  41. data/lib/shoko/adapters/input/command_bridge.rb +148 -0
  42. data/lib/shoko/adapters/input/command_factory.rb +255 -0
  43. data/lib/shoko/adapters/input/commands.rb +60 -0
  44. data/lib/shoko/adapters/input/dispatcher.rb +69 -0
  45. data/lib/shoko/adapters/input/input_controller.rb +250 -0
  46. data/lib/shoko/adapters/input/key_definitions.rb +108 -0
  47. data/lib/shoko/adapters/input/validators/file_path_validator.rb +81 -0
  48. data/lib/shoko/adapters/input/validators/terminal_size_validator.rb +76 -0
  49. data/lib/shoko/adapters/monitoring/logger.rb +150 -0
  50. data/lib/shoko/adapters/monitoring/perf_tracer.rb +183 -0
  51. data/lib/shoko/adapters/monitoring/performance_monitor.rb +110 -0
  52. data/lib/shoko/adapters/output/clipboard/clipboard_service.rb +125 -0
  53. data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/image_builder.rb +149 -0
  54. data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/text_wrapper.rb +149 -0
  55. data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/tokenizer.rb +91 -0
  56. data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler.rb +144 -0
  57. data/lib/shoko/adapters/output/formatting/formatting_service/plain_lines_builder.rb +54 -0
  58. data/lib/shoko/adapters/output/formatting/formatting_service.rb +247 -0
  59. data/lib/shoko/adapters/output/formatting/wrapping_service.rb +228 -0
  60. data/lib/shoko/adapters/output/instrumentation_service.rb +52 -0
  61. data/lib/shoko/adapters/output/kitty/image_transcoder.rb +71 -0
  62. data/lib/shoko/adapters/output/kitty/kitty_graphics.rb +114 -0
  63. data/lib/shoko/adapters/output/kitty/kitty_image_renderer.rb +239 -0
  64. data/lib/shoko/adapters/output/kitty/kitty_unicode_placeholders.rb +139 -0
  65. data/lib/shoko/adapters/output/kitty/kitty_unicode_placeholders_diacritic_codepoints.txt +26 -0
  66. data/lib/shoko/adapters/output/notification_service.rb +58 -0
  67. data/lib/shoko/adapters/output/render_registry.rb +45 -0
  68. data/lib/shoko/adapters/output/rendering/models/line_geometry.rb +60 -0
  69. data/lib/shoko/adapters/output/rendering/models/page_rendering_context.rb +22 -0
  70. data/lib/shoko/adapters/output/rendering/models/render_params.rb +28 -0
  71. data/lib/shoko/adapters/output/rendering/models/rendering_context.rb +58 -0
  72. data/lib/shoko/adapters/output/terminal/buffer.rb +275 -0
  73. data/lib/shoko/adapters/output/terminal/constants/terminal_defaults.rb +11 -0
  74. data/lib/shoko/adapters/output/terminal/input/decoder.rb +347 -0
  75. data/lib/shoko/adapters/output/terminal/input.rb +161 -0
  76. data/lib/shoko/adapters/output/terminal/output.rb +105 -0
  77. data/lib/shoko/adapters/output/terminal/terminal.rb +167 -0
  78. data/lib/shoko/adapters/output/terminal/terminal_sanitizer.rb +243 -0
  79. data/lib/shoko/adapters/output/terminal/terminal_service.rb +138 -0
  80. data/lib/shoko/adapters/output/terminal/text_metrics.rb +273 -0
  81. data/lib/shoko/adapters/output/ui/builders/page_setup_builder.rb +47 -0
  82. data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay/footer_renderer.rb +80 -0
  83. data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay/geometry.rb +61 -0
  84. data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay/note_renderer.rb +86 -0
  85. data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay_component.rb +234 -0
  86. data/lib/shoko/adapters/output/ui/components/annotations_overlay/list_renderer.rb +142 -0
  87. data/lib/shoko/adapters/output/ui/components/annotations_overlay_component.rb +185 -0
  88. data/lib/shoko/adapters/output/ui/components/base_component.rb +110 -0
  89. data/lib/shoko/adapters/output/ui/components/component_interface.rb +80 -0
  90. data/lib/shoko/adapters/output/ui/components/content_component.rb +61 -0
  91. data/lib/shoko/adapters/output/ui/components/enhanced_popup_menu.rb +191 -0
  92. data/lib/shoko/adapters/output/ui/components/footer_component.rb +120 -0
  93. data/lib/shoko/adapters/output/ui/components/header_component.rb +46 -0
  94. data/lib/shoko/adapters/output/ui/components/layouts/horizontal.rb +63 -0
  95. data/lib/shoko/adapters/output/ui/components/layouts/vertical.rb +73 -0
  96. data/lib/shoko/adapters/output/ui/components/main_menu_component.rb +103 -0
  97. data/lib/shoko/adapters/output/ui/components/reading/base_view_renderer.rb +199 -0
  98. data/lib/shoko/adapters/output/ui/components/reading/config_helpers.rb +42 -0
  99. data/lib/shoko/adapters/output/ui/components/reading/help_renderer.rb +62 -0
  100. data/lib/shoko/adapters/output/ui/components/reading/inline_segment_highlighter.rb +144 -0
  101. data/lib/shoko/adapters/output/ui/components/reading/kitty_image_line_renderer.rb +262 -0
  102. data/lib/shoko/adapters/output/ui/components/reading/line_content_composer.rb +114 -0
  103. data/lib/shoko/adapters/output/ui/components/reading/line_drawer.rb +87 -0
  104. data/lib/shoko/adapters/output/ui/components/reading/line_geometry_builder.rb +41 -0
  105. data/lib/shoko/adapters/output/ui/components/reading/rendered_lines_recorder.rb +64 -0
  106. data/lib/shoko/adapters/output/ui/components/reading/single_view_renderer.rb +156 -0
  107. data/lib/shoko/adapters/output/ui/components/reading/split_view_renderer.rb +221 -0
  108. data/lib/shoko/adapters/output/ui/components/reading/view_renderer_factory.rb +20 -0
  109. data/lib/shoko/adapters/output/ui/components/reading/wrapped_lines_fetcher.rb +139 -0
  110. data/lib/shoko/adapters/output/ui/components/rect.rb +15 -0
  111. data/lib/shoko/adapters/output/ui/components/render_style.rb +84 -0
  112. data/lib/shoko/adapters/output/ui/components/screen_component.rb +24 -0
  113. data/lib/shoko/adapters/output/ui/components/screens/annotation_detail_screen_component.rb +175 -0
  114. data/lib/shoko/adapters/output/ui/components/screens/annotation_edit_screen_component.rb +221 -0
  115. data/lib/shoko/adapters/output/ui/components/screens/annotation_editor_screen_component.rb +205 -0
  116. data/lib/shoko/adapters/output/ui/components/screens/annotation_rendering_helpers.rb +190 -0
  117. data/lib/shoko/adapters/output/ui/components/screens/annotations_screen_component.rb +266 -0
  118. data/lib/shoko/adapters/output/ui/components/screens/base_screen_component.rb +49 -0
  119. data/lib/shoko/adapters/output/ui/components/screens/browse_screen_component.rb +319 -0
  120. data/lib/shoko/adapters/output/ui/components/screens/download_books_screen_component.rb +340 -0
  121. data/lib/shoko/adapters/output/ui/components/screens/library_screen_component.rb +205 -0
  122. data/lib/shoko/adapters/output/ui/components/screens/loading_overlay_component.rb +49 -0
  123. data/lib/shoko/adapters/output/ui/components/screens/menu_screen_component.rb +107 -0
  124. data/lib/shoko/adapters/output/ui/components/screens/settings_screen_component.rb +238 -0
  125. data/lib/shoko/adapters/output/ui/components/sidebar/annotations_tab_renderer.rb +159 -0
  126. data/lib/shoko/adapters/output/ui/components/sidebar/bookmarks_tab_renderer.rb +139 -0
  127. data/lib/shoko/adapters/output/ui/components/sidebar/tab_header_component.rb +157 -0
  128. data/lib/shoko/adapters/output/ui/components/sidebar/toc_tab_renderer.rb +111 -0
  129. data/lib/shoko/adapters/output/ui/components/sidebar/toc_tab_support.rb +1606 -0
  130. data/lib/shoko/adapters/output/ui/components/sidebar_panel_component.rb +217 -0
  131. data/lib/shoko/adapters/output/ui/components/surface.rb +88 -0
  132. data/lib/shoko/adapters/output/ui/components/tooltip_overlay_component.rb +224 -0
  133. data/lib/shoko/adapters/output/ui/components/ui/box_drawer.rb +32 -0
  134. data/lib/shoko/adapters/output/ui/components/ui/list_helpers.rb +33 -0
  135. data/lib/shoko/adapters/output/ui/components/ui/overlay_layout.rb +79 -0
  136. data/lib/shoko/adapters/output/ui/components/ui/text_utils.rb +46 -0
  137. data/lib/shoko/adapters/output/ui/constants/highlighting.rb +21 -0
  138. data/lib/shoko/adapters/output/ui/constants/messages.rb +12 -0
  139. data/lib/shoko/adapters/output/ui/constants/themes.rb +79 -0
  140. data/lib/shoko/adapters/output/ui/constants/ui_constants.rb +85 -0
  141. data/lib/shoko/adapters/output/ui/rendering/frame_coordinator.rb +42 -0
  142. data/lib/shoko/adapters/output/ui/rendering/reader_render_coordinator.rb +169 -0
  143. data/lib/shoko/adapters/output/ui/rendering/render_pipeline.rb +55 -0
  144. data/lib/shoko/adapters/storage/atomic_file_writer.rb +43 -0
  145. data/lib/shoko/adapters/storage/background_worker.rb +66 -0
  146. data/lib/shoko/adapters/storage/book_cache_pipeline.rb +653 -0
  147. data/lib/shoko/adapters/storage/cache/epub/memory_cache.rb +99 -0
  148. data/lib/shoko/adapters/storage/cache/epub/persistence.rb +131 -0
  149. data/lib/shoko/adapters/storage/cache/epub/serializer/deserialize.rb +225 -0
  150. data/lib/shoko/adapters/storage/cache/epub/serializer/helpers.rb +63 -0
  151. data/lib/shoko/adapters/storage/cache/epub/serializer/serialize.rb +83 -0
  152. data/lib/shoko/adapters/storage/cache/epub/serializer.rb +5 -0
  153. data/lib/shoko/adapters/storage/cache/epub/source_reference.rb +58 -0
  154. data/lib/shoko/adapters/storage/cache_paths.rb +21 -0
  155. data/lib/shoko/adapters/storage/cache_pointer_manager.rb +60 -0
  156. data/lib/shoko/adapters/storage/config_paths.rb +30 -0
  157. data/lib/shoko/adapters/storage/epub_cache.rb +195 -0
  158. data/lib/shoko/adapters/storage/file_writer_service.rb +47 -0
  159. data/lib/shoko/adapters/storage/json_cache_store/chapters.rb +141 -0
  160. data/lib/shoko/adapters/storage/json_cache_store/layouts.rb +67 -0
  161. data/lib/shoko/adapters/storage/json_cache_store/manifest.rb +42 -0
  162. data/lib/shoko/adapters/storage/json_cache_store/payload_helpers.rb +113 -0
  163. data/lib/shoko/adapters/storage/json_cache_store/resources.rb +84 -0
  164. data/lib/shoko/adapters/storage/json_cache_store.rb +167 -0
  165. data/lib/shoko/adapters/storage/lazy_file_string.rb +65 -0
  166. data/lib/shoko/adapters/storage/pagination_cache.rb +127 -0
  167. data/lib/shoko/adapters/storage/recent_files.rb +78 -0
  168. data/lib/shoko/adapters/storage/repositories/annotation_repository.rb +182 -0
  169. data/lib/shoko/adapters/storage/repositories/base_repository.rb +81 -0
  170. data/lib/shoko/adapters/storage/repositories/bookmark_repository.rb +132 -0
  171. data/lib/shoko/adapters/storage/repositories/cached_library_repository.rb +129 -0
  172. data/lib/shoko/adapters/storage/repositories/config_repository.rb +262 -0
  173. data/lib/shoko/adapters/storage/repositories/progress_repository.rb +166 -0
  174. data/lib/shoko/adapters/storage/repositories/storage/annotation_file_store.rb +128 -0
  175. data/lib/shoko/adapters/storage/repositories/storage/bookmark_file_store.rb +109 -0
  176. data/lib/shoko/adapters/storage/repositories/storage/file_store_utils.rb +20 -0
  177. data/lib/shoko/adapters/storage/repositories/storage/progress_file_store.rb +59 -0
  178. data/lib/shoko/application/annotation_editor_overlay_session.rb +138 -0
  179. data/lib/shoko/application/cli.rb +134 -0
  180. data/lib/shoko/application/controllers/menu/input_controller.rb +189 -0
  181. data/lib/shoko/application/controllers/menu/state_controller.rb +642 -0
  182. data/lib/shoko/application/controllers/menu_controller.rb +469 -0
  183. data/lib/shoko/application/controllers/mouseable_reader.rb +377 -0
  184. data/lib/shoko/application/controllers/reader_controller.rb +449 -0
  185. data/lib/shoko/application/controllers/state_controller.rb +410 -0
  186. data/lib/shoko/application/controllers/ui_controller.rb +782 -0
  187. data/lib/shoko/application/dependency_container.rb +301 -0
  188. data/lib/shoko/application/infrastructure/event_bus.rb +80 -0
  189. data/lib/shoko/application/infrastructure/observer_state_store.rb +136 -0
  190. data/lib/shoko/application/infrastructure/state_store.rb +413 -0
  191. data/lib/shoko/application/main_menu/menu_progress_presenter.rb +83 -0
  192. data/lib/shoko/application/pending_jump_handler.rb +122 -0
  193. data/lib/shoko/application/reader_lifecycle.rb +65 -0
  194. data/lib/shoko/application/reader_startup_orchestrator.rb +113 -0
  195. data/lib/shoko/application/selectors/config_selectors.rb +62 -0
  196. data/lib/shoko/application/selectors/menu_selectors.rb +62 -0
  197. data/lib/shoko/application/selectors/reader_selectors.rb +186 -0
  198. data/lib/shoko/application/state/actions/base_action.rb +24 -0
  199. data/lib/shoko/application/state/actions/quit_to_menu_action.rb +16 -0
  200. data/lib/shoko/application/state/actions/switch_reader_mode_action.rb +22 -0
  201. data/lib/shoko/application/state/actions/toggle_view_mode_action.rb +31 -0
  202. data/lib/shoko/application/state/actions/update_annotation_editor_overlay_action.rb +27 -0
  203. data/lib/shoko/application/state/actions/update_annotations_action.rb +20 -0
  204. data/lib/shoko/application/state/actions/update_annotations_overlay_action.rb +27 -0
  205. data/lib/shoko/application/state/actions/update_bookmarks_action.rb +20 -0
  206. data/lib/shoko/application/state/actions/update_chapter_action.rb +24 -0
  207. data/lib/shoko/application/state/actions/update_config_action.rb +22 -0
  208. data/lib/shoko/application/state/actions/update_field_helpers.rb +26 -0
  209. data/lib/shoko/application/state/actions/update_menu_action.rb +21 -0
  210. data/lib/shoko/application/state/actions/update_message_action.rb +35 -0
  211. data/lib/shoko/application/state/actions/update_page_action.rb +21 -0
  212. data/lib/shoko/application/state/actions/update_pagination_state_action.rb +21 -0
  213. data/lib/shoko/application/state/actions/update_popup_menu_action.rb +27 -0
  214. data/lib/shoko/application/state/actions/update_reader_meta_action.rb +21 -0
  215. data/lib/shoko/application/state/actions/update_reader_mode_action.rb +20 -0
  216. data/lib/shoko/application/state/actions/update_rendered_lines_action.rb +40 -0
  217. data/lib/shoko/application/state/actions/update_selection_action.rb +27 -0
  218. data/lib/shoko/application/state/actions/update_selections_action.rb +21 -0
  219. data/lib/shoko/application/state/actions/update_sidebar_action.rb +34 -0
  220. data/lib/shoko/application/state/actions/update_ui_loading_action.rb +23 -0
  221. data/lib/shoko/application/ui/reader_view_model_builder.rb +74 -0
  222. data/lib/shoko/application/ui/view_models/reader_view_model.rb +177 -0
  223. data/lib/shoko/application/unified_application.rb +48 -0
  224. data/lib/shoko/application/use_cases/catalog_service.rb +117 -0
  225. data/lib/shoko/application/use_cases/commands/annotation_editor_commands.rb +105 -0
  226. data/lib/shoko/application/use_cases/commands/application_commands.rb +208 -0
  227. data/lib/shoko/application/use_cases/commands/base_command.rb +166 -0
  228. data/lib/shoko/application/use_cases/commands/bookmark_commands.rb +114 -0
  229. data/lib/shoko/application/use_cases/commands/conditional_navigation_commands.rb +57 -0
  230. data/lib/shoko/application/use_cases/commands/menu_commands.rb +170 -0
  231. data/lib/shoko/application/use_cases/commands/navigation_commands.rb +183 -0
  232. data/lib/shoko/application/use_cases/commands/reader_commands.rb +46 -0
  233. data/lib/shoko/application/use_cases/commands/sidebar_commands.rb +55 -0
  234. data/lib/shoko/application/use_cases/settings_service.rb +123 -0
  235. data/lib/shoko/core/events/annotation_events.rb +94 -0
  236. data/lib/shoko/core/events/base_domain_event.rb +169 -0
  237. data/lib/shoko/core/events/bookmark_events.rb +41 -0
  238. data/lib/shoko/core/events/domain_event_bus.rb +163 -0
  239. data/lib/shoko/core/events/progress_events.rb +108 -0
  240. data/lib/shoko/core/models/bookmark.rb +36 -0
  241. data/lib/shoko/core/models/bookmark_data.rb +10 -0
  242. data/lib/shoko/core/models/chapter.rb +25 -0
  243. data/lib/shoko/core/models/content_block.rb +44 -0
  244. data/lib/shoko/core/models/reader_settings.rb +20 -0
  245. data/lib/shoko/core/models/selection_anchor.rb +73 -0
  246. data/lib/shoko/core/models/toc_entry.rb +14 -0
  247. data/lib/shoko/core/ports/annotation_repository.rb +0 -0
  248. data/lib/shoko/core/ports/book_repository.rb +0 -0
  249. data/lib/shoko/core/ports/book_source.rb +0 -0
  250. data/lib/shoko/core/ports/bookmark_repository.rb +0 -0
  251. data/lib/shoko/core/ports/cache.rb +0 -0
  252. data/lib/shoko/core/ports/input_handler.rb +0 -0
  253. data/lib/shoko/core/ports/renderer.rb +0 -0
  254. data/lib/shoko/core/ports/storage.rb +0 -0
  255. data/lib/shoko/core/services/annotation_service.rb +102 -0
  256. data/lib/shoko/core/services/base_service.rb +60 -0
  257. data/lib/shoko/core/services/bookmark_service.rb +267 -0
  258. data/lib/shoko/core/services/coordinate_service.rb +265 -0
  259. data/lib/shoko/core/services/layout_service.rb +95 -0
  260. data/lib/shoko/core/services/navigation/absolute_change_applier.rb +96 -0
  261. data/lib/shoko/core/services/navigation/absolute_layout.rb +101 -0
  262. data/lib/shoko/core/services/navigation/absolute_strategy.rb +179 -0
  263. data/lib/shoko/core/services/navigation/context_builder.rb +52 -0
  264. data/lib/shoko/core/services/navigation/context_helpers.rb +63 -0
  265. data/lib/shoko/core/services/navigation/dynamic_change_applier.rb +50 -0
  266. data/lib/shoko/core/services/navigation/dynamic_strategy.rb +51 -0
  267. data/lib/shoko/core/services/navigation/image_offset_snapper.rb +150 -0
  268. data/lib/shoko/core/services/navigation/nav_context.rb +27 -0
  269. data/lib/shoko/core/services/navigation/state_updater.rb +29 -0
  270. data/lib/shoko/core/services/navigation/strategy_factory.rb +20 -0
  271. data/lib/shoko/core/services/navigation_service.rb +150 -0
  272. data/lib/shoko/core/services/page_calculator_service.rb +242 -0
  273. data/lib/shoko/core/services/pagination/internal/absolute_page_map_builder.rb +28 -0
  274. data/lib/shoko/core/services/pagination/internal/chapter_cache.rb +60 -0
  275. data/lib/shoko/core/services/pagination/internal/dynamic_page_map_builder.rb +157 -0
  276. data/lib/shoko/core/services/pagination/internal/layout_metrics_calculator.rb +73 -0
  277. data/lib/shoko/core/services/pagination/internal/page_hydrator.rb +145 -0
  278. data/lib/shoko/core/services/pagination/internal/pagination_workflow.rb +152 -0
  279. data/lib/shoko/core/services/pagination/page_info_calculator.rb +247 -0
  280. data/lib/shoko/core/services/pagination/pagination_cache_preloader.rb +173 -0
  281. data/lib/shoko/core/services/pagination/pagination_coordinator.rb +202 -0
  282. data/lib/shoko/core/services/pagination/pagination_orchestrator.rb +291 -0
  283. data/lib/shoko/core/services/pagination.rb +10 -0
  284. data/lib/shoko/core/services/progress_helper.rb +22 -0
  285. data/lib/shoko/core/services/selection_service.rb +126 -0
  286. data/lib/shoko/core/validator.rb +76 -0
  287. data/lib/shoko/shared/errors.rb +97 -0
  288. data/lib/shoko/shared/version.rb +5 -0
  289. data/lib/shoko/test_support/terminal_double.rb +175 -0
  290. data/lib/shoko/test_support/test_mode.rb +78 -0
  291. data/lib/shoko.rb +279 -0
  292. data/lib/zip.rb +732 -0
  293. data/zip.rb +5 -0
  294. metadata +370 -0
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../terminal/text_metrics.rb'
4
+
5
+ module Shoko
6
+ module Adapters::Output::Ui::Components
7
+ module UI
8
+ # Shared text layout utilities for terminal UI rendering.
9
+ module TextUtils
10
+ module_function
11
+
12
+ def wrap_text(text, width)
13
+ t = (text || '').to_s
14
+ w = width.to_i
15
+ return [''] if t.empty?
16
+ return [''] if w <= 0
17
+
18
+ Shoko::Adapters::Output::Terminal::TextMetrics.wrap_cells(t, w)
19
+ end
20
+
21
+ def truncate_text(text, max_length)
22
+ str = (text || '').to_s
23
+ w = max_length.to_i
24
+ return '' if w <= 0
25
+
26
+ return str if Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(str) <= w
27
+
28
+ ellipsis = '...'
29
+ ellipsis_w = Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(ellipsis)
30
+ return Shoko::Adapters::Output::Terminal::TextMetrics.truncate_to(str, w) if ellipsis_w >= w
31
+
32
+ base = Shoko::Adapters::Output::Terminal::TextMetrics.truncate_to(str, w - ellipsis_w)
33
+ base + ellipsis
34
+ end
35
+
36
+ def pad_right(text, width, pad: ' ')
37
+ Shoko::Adapters::Output::Terminal::TextMetrics.pad_right(text.to_s, width.to_i, pad: pad)
38
+ end
39
+
40
+ def pad_left(text, width, pad: ' ')
41
+ Shoko::Adapters::Output::Terminal::TextMetrics.pad_left(text.to_s, width.to_i, pad: pad)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Adapters::Output::Ui
5
+ module Constants
6
+ # Text highlighting patterns used by reading renderers.
7
+ module Highlighting
8
+ HIGHLIGHT_WORDS = [
9
+ 'Chinese poets', 'philosophers', 'Taoyuen-ming', 'celebrated', 'fragrance',
10
+ 'plum-blossoms', 'Linwosing', 'Chowmushih'
11
+ ].freeze
12
+ HIGHLIGHT_PATTERNS = Regexp.union(HIGHLIGHT_WORDS)
13
+ # Matches basic quoted spans for optional highlighting. Supports:
14
+ # - ASCII quotes: "..." and '...'
15
+ # - Curly quotes: “...” and ‘...’
16
+ # - Guillemets: «...» and ‹...›
17
+ QUOTE_PATTERNS = /(["“„«‹][^"“”„«»‹›]*["”»›])|(['‘‚][^'‘’‚]*['’])/
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Adapters::Output::Ui
5
+ module Constants
6
+ module Messages
7
+ BOOKMARK_ADDED = 'Bookmark added!'
8
+ BOOKMARK_DELETED = 'Bookmark deleted!'
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../terminal/terminal.rb'
4
+
5
+ module Shoko
6
+ module Adapters::Output::Ui
7
+ module Constants
8
+ # Theme palettes used by the terminal render style system.
9
+ module Themes
10
+ DEFAULT_PALETTE = {
11
+ primary: Terminal::ANSI::WHITE,
12
+ accent: Terminal::ANSI::BRIGHT_CYAN,
13
+ heading: Terminal::ANSI::BRIGHT_GREEN,
14
+ dim: Terminal::ANSI::DIM,
15
+ quote: Terminal::ANSI::LIGHT_GREY,
16
+ code: Terminal::ANSI::YELLOW,
17
+ separator: Terminal::ANSI::GRAY,
18
+ prefix: Terminal::ANSI::GRAY,
19
+ }.freeze
20
+
21
+ THEMES = {
22
+ default: DEFAULT_PALETTE,
23
+ standard: DEFAULT_PALETTE,
24
+ gray: DEFAULT_PALETTE.merge(
25
+ primary: Terminal::ANSI::LIGHT_GREY,
26
+ accent: Terminal::ANSI::BRIGHT_WHITE,
27
+ quote: Terminal::ANSI::GRAY
28
+ ).freeze,
29
+ sepia: DEFAULT_PALETTE.merge(
30
+ primary: Terminal::ANSI::YELLOW,
31
+ accent: Terminal::ANSI::BRIGHT_YELLOW,
32
+ dim: Terminal::ANSI::DIM,
33
+ quote: Terminal::ANSI::BRIGHT_YELLOW
34
+ ).freeze,
35
+ grass: DEFAULT_PALETTE.merge(
36
+ primary: Terminal::ANSI::GREEN,
37
+ accent: Terminal::ANSI::BRIGHT_GREEN,
38
+ quote: Terminal::ANSI::GREEN
39
+ ).freeze,
40
+ cherry: DEFAULT_PALETTE.merge(
41
+ primary: Terminal::ANSI::RED,
42
+ accent: Terminal::ANSI::BRIGHT_RED,
43
+ quote: Terminal::ANSI::BRIGHT_RED
44
+ ).freeze,
45
+ sky: DEFAULT_PALETTE.merge(
46
+ primary: Terminal::ANSI::BLUE,
47
+ accent: Terminal::ANSI::BRIGHT_BLUE,
48
+ quote: Terminal::ANSI::BRIGHT_BLUE
49
+ ).freeze,
50
+ solarized: DEFAULT_PALETTE.merge(
51
+ primary: Terminal::ANSI::CYAN,
52
+ accent: Terminal::ANSI::BRIGHT_CYAN,
53
+ quote: Terminal::ANSI::BRIGHT_CYAN
54
+ ).freeze,
55
+ gruvbox: DEFAULT_PALETTE.merge(
56
+ primary: Terminal::ANSI::YELLOW,
57
+ accent: Terminal::ANSI::BRIGHT_GREEN,
58
+ quote: Terminal::ANSI::BRIGHT_YELLOW
59
+ ).freeze,
60
+ nord: DEFAULT_PALETTE.merge(
61
+ primary: Terminal::ANSI::BRIGHT_BLUE,
62
+ accent: Terminal::ANSI::BRIGHT_CYAN,
63
+ quote: Terminal::ANSI::BRIGHT_CYAN
64
+ ).freeze,
65
+ }.freeze
66
+
67
+ module_function
68
+
69
+ def palette_for(theme)
70
+ theme_key = theme&.to_sym
71
+ base = DEFAULT_PALETTE
72
+ return base unless theme_key
73
+
74
+ THEMES[theme_key] || base
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../terminal/terminal.rb'
4
+
5
+ module Shoko
6
+ module Adapters::Output::Ui
7
+ module Constants
8
+ # Centralized UI color and style definitions
9
+ module UI
10
+ # Dimensions
11
+ MIN_WIDTH = 60
12
+ MIN_HEIGHT = 20
13
+
14
+ # Base Colors
15
+ COLOR_TEXT_PRIMARY = Terminal::ANSI::WHITE
16
+ COLOR_TEXT_SECONDARY = Terminal::ANSI::GRAY
17
+ COLOR_TEXT_DIM = Terminal::ANSI::DIM
18
+ COLOR_TEXT_ACCENT = Terminal::ANSI::BRIGHT_CYAN
19
+ COLOR_TEXT_SUCCESS = Terminal::ANSI::GREEN
20
+ COLOR_TEXT_WARNING = Terminal::ANSI::YELLOW
21
+ COLOR_TEXT_ERROR = Terminal::ANSI::RED
22
+
23
+ # Backgrounds
24
+ BG_PRIMARY = Terminal::ANSI::BG_DARK
25
+ BG_ACCENT = Terminal::ANSI::BG_BRIGHT_YELLOW
26
+
27
+ # Borders & Dividers
28
+ BORDER_PRIMARY = Terminal::ANSI::GRAY
29
+ BORDER_ACCENT = Terminal::ANSI::BRIGHT_CYAN
30
+
31
+ # Selections & Highlights
32
+ SELECTION_POINTER = '▸ '
33
+ SELECTION_FG = Terminal::ANSI::BLACK
34
+ SELECTION_POINTER_COLOR = Terminal::ANSI::BRIGHT_GREEN
35
+ SELECTION_HIGHLIGHT = Terminal::ANSI::BRIGHT_WHITE
36
+
37
+ # Overlay/Highlight backgrounds
38
+ HIGHLIGHT_BG_ACTIVE = Terminal::ANSI::BG_GREY
39
+ HIGHLIGHT_BG_SAVED = Terminal::ANSI::BG_GREY
40
+
41
+ # Popup menu colors
42
+ POPUP_BG_DEFAULT = Terminal::ANSI::BG_SLATE
43
+ POPUP_BG_SELECTED = Terminal::ANSI::BG_SOFT_GREEN
44
+ POPUP_FG_DEFAULT = COLOR_TEXT_PRIMARY
45
+ POPUP_FG_SELECTED = Terminal::ANSI::BLACK
46
+
47
+ # Tooltip menu colors
48
+ TOOLTIP_BG_DEFAULT = POPUP_BG_DEFAULT
49
+ TOOLTIP_BG_SELECTED = "\e[48;2;255;135;135m"
50
+ TOOLTIP_FG_DEFAULT = "\e[38;2;255;135;135m"
51
+ TOOLTIP_FG_SELECTED = "\e[38;2;88;88;88m"
52
+
53
+ # Annotation editor overlay colors
54
+ ANNOTATION_PANEL_BG = "\e[48;2;88;88;88m"
55
+ ANNOTATION_HEADER_FG = "\e[38;2;255;135;135m"
56
+
57
+ # Toast notification colors
58
+ TOAST_ACCENT = "\e[38;2;255;135;135m"
59
+ TOAST_FG = "\e[38;2;255;255;255m"
60
+
61
+ # Icons
62
+ ICON_BOOK = '󰂺'
63
+ ICON_RECENT = '󰁯'
64
+ ICON_ANNOTATION = '󰠮'
65
+ ICON_SETTINGS = ''
66
+ ICON_QUIT = '󰿅'
67
+ ICON_OPEN = '󰷏'
68
+ ICON_TOC = '📖'
69
+ ICON_BOOKMARK = '🔖'
70
+ ICON_HELP = '❓'
71
+ ICON_SEARCH = ''
72
+ ICON_REFRESH = ''
73
+
74
+ SIDEBAR_BG = Terminal::ANSI::BG_DARK
75
+ SIDEBAR_SELECTION_BG = Terminal::ANSI::BG_BLUE
76
+ SIDEBAR_SELECTION_FG = Terminal::ANSI::BRIGHT_WHITE
77
+
78
+ BUTTON_BG_ACTIVE = Terminal::ANSI::BG_BRIGHT_GREEN
79
+ BUTTON_FG_ACTIVE = Terminal::ANSI::BLACK
80
+ BUTTON_BG_INACTIVE = Terminal::ANSI::BG_GREY
81
+ BUTTON_FG_INACTIVE = Terminal::ANSI::WHITE
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Adapters::Output::Ui
5
+ module Rendering
6
+ # Coordinates frame lifecycle and provides a consistent surface + bounds
7
+ # for rendering. Centralizes start_frame/end_frame and terminal size updates.
8
+ class FrameCoordinator
9
+ def initialize(dependencies)
10
+ @dependencies = dependencies
11
+ @terminal_service = @dependencies.resolve(:terminal_service)
12
+ @state = @dependencies.resolve(:global_state)
13
+ end
14
+
15
+ # Yields a prepared [surface, bounds, width, height] within a started frame.
16
+ # Ensures end_frame is called, even on errors.
17
+ def with_frame
18
+ height, width = @terminal_service.size
19
+ @terminal_service.start_frame(width: width, height: height)
20
+ @state.update_terminal_size(width, height)
21
+ surface = @terminal_service.create_surface
22
+ bounds = Shoko::Adapters::Output::Ui::Components::Rect.new(x: 1, y: 1, width: width, height: height)
23
+ yield(surface, bounds, width, height)
24
+ ensure
25
+ @terminal_service.end_frame
26
+ end
27
+
28
+ # Renders the loading overlay component in a standalone frame.
29
+ def render_loading_overlay
30
+ height, width = @terminal_service.size
31
+ @terminal_service.start_frame(width: width, height: height)
32
+ surface = @terminal_service.create_surface
33
+ bounds = Shoko::Adapters::Output::Ui::Components::Rect.new(x: 1, y: 1, width: width, height: height)
34
+ overlay = Shoko::Adapters::Output::Ui::Components::Screens::LoadingOverlayComponent.new(@dependencies)
35
+ overlay.render(surface, bounds)
36
+ ensure
37
+ @terminal_service.end_frame
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../components/header_component'
4
+ require_relative '../components/content_component'
5
+ require_relative '../components/footer_component'
6
+ require_relative '../components/sidebar_panel_component'
7
+ require_relative '../components/layouts/vertical'
8
+ require_relative '../components/layouts/horizontal'
9
+ require_relative '../components/tooltip_overlay_component'
10
+ require_relative '../../../../application/ui/reader_view_model_builder.rb'
11
+
12
+ module Shoko
13
+ module Adapters::Output::Ui
14
+ module Rendering
15
+ # Coordinates render/layout setup and per-frame drawing for the reader.
16
+ class ReaderRenderCoordinator
17
+ Dependencies = Struct.new(
18
+ :controller,
19
+ :state,
20
+ :dependencies,
21
+ :terminal_service,
22
+ :frame_coordinator,
23
+ :render_pipeline,
24
+ :ui_controller,
25
+ :wrapping_service,
26
+ :pagination,
27
+ :doc,
28
+ keyword_init: true
29
+ )
30
+
31
+ RenderComponents = Struct.new(
32
+ :header,
33
+ :content,
34
+ :footer,
35
+ :sidebar,
36
+ :main_layout,
37
+ :root_layout,
38
+ :overlay,
39
+ keyword_init: true
40
+ )
41
+
42
+ def initialize(dependencies:)
43
+ @deps = dependencies
44
+ @components = RenderComponents.new
45
+ end
46
+
47
+ def build_component_layout
48
+ vm_proc = -> { create_view_model }
49
+ components.header = Shoko::Adapters::Output::Ui::Components::HeaderComponent.new(vm_proc)
50
+ components.content = Shoko::Adapters::Output::Ui::Components::ContentComponent.new(deps.controller)
51
+ components.footer = Shoko::Adapters::Output::Ui::Components::FooterComponent.new(vm_proc)
52
+ components.sidebar = Shoko::Adapters::Output::Ui::Components::SidebarPanelComponent.new(deps.state, deps.dependencies)
53
+ components.main_layout = Shoko::Adapters::Output::Ui::Components::Layouts::Vertical.new([
54
+ components.header,
55
+ components.content,
56
+ components.footer,
57
+ ])
58
+
59
+ rebuild_root_layout
60
+ build_overlay
61
+ end
62
+
63
+ def rebuild_root_layout
64
+ components.root_layout = if deps.state.get(%i[reader sidebar_visible])
65
+ Shoko::Adapters::Output::Ui::Components::Layouts::Horizontal.new(components.sidebar, components.main_layout)
66
+ else
67
+ components.main_layout
68
+ end
69
+ end
70
+
71
+ def draw_screen
72
+ height, width = deps.terminal_service.size
73
+ tick_notifications
74
+ handle_resize(width, height) if size_changed?(width, height)
75
+
76
+ deps.frame_coordinator.with_frame do |surface, root_bounds, _w, _h|
77
+ mode = deps.state.get(%i[reader mode])
78
+ mode_component = deps.ui_controller.current_mode
79
+ if mode == :annotation_editor && mode_component
80
+ deps.render_pipeline.render_mode_component(mode_component, surface, root_bounds)
81
+ else
82
+ deps.render_pipeline.render_layout(surface, root_bounds, components.root_layout, components.overlay)
83
+ end
84
+ end
85
+ end
86
+
87
+ def refresh_highlighting
88
+ draw_screen
89
+ end
90
+
91
+ def force_redraw
92
+ components.content&.invalidate
93
+ end
94
+
95
+ def render_loading_overlay
96
+ deps.frame_coordinator.render_loading_overlay
97
+ end
98
+
99
+ def apply_theme_palette
100
+ theme = Shoko::Application::Selectors::ConfigSelectors.theme(deps.state) || :default
101
+ palette = Shoko::Adapters::Output::Ui::Constants::Themes.palette_for(theme)
102
+ Shoko::Adapters::Output::Ui::Components::RenderStyle.configure(palette)
103
+ rescue StandardError
104
+ Shoko::Adapters::Output::Ui::Components::RenderStyle.configure(Shoko::Adapters::Output::Ui::Constants::Themes::DEFAULT_PALETTE)
105
+ end
106
+
107
+ def sidebar_component
108
+ components.sidebar
109
+ end
110
+
111
+ def sidebar_bounds(total_width, total_height)
112
+ sidebar = components.sidebar
113
+ return nil unless sidebar
114
+
115
+ sidebar.sidebar_bounds_for(total_width, total_height)
116
+ end
117
+
118
+ private
119
+
120
+ attr_reader :deps, :components
121
+
122
+ def create_view_model
123
+ builder = Shoko::Application::UI::ReaderViewModelBuilder.new(deps.state, deps.doc)
124
+ builder.build(deps.pagination.page_info)
125
+ end
126
+
127
+ def size_changed?(width, height)
128
+ deps.state.terminal_size_changed?(width, height)
129
+ end
130
+
131
+ def handle_resize(width, height)
132
+ deps.pagination.refresh_after_resize(width: width, height: height)
133
+ clear_wrapping_cache
134
+ end
135
+
136
+ def clear_wrapping_cache
137
+ prior_width = deps.state.get(%i[reader last_width])
138
+ return unless prior_width&.positive?
139
+
140
+ deps.wrapping_service&.clear_cache_for_width(prior_width)
141
+ rescue StandardError
142
+ # best-effort cache clear
143
+ end
144
+
145
+ def build_overlay
146
+ coord = deps.dependencies.resolve(:coordinate_service)
147
+ components.overlay = Shoko::Adapters::Output::Ui::Components::TooltipOverlayComponent.new(
148
+ deps.controller,
149
+ coordinate_service: coord
150
+ )
151
+ end
152
+
153
+ def tick_notifications
154
+ notification_service&.tick(deps.state)
155
+ end
156
+
157
+ def notification_service
158
+ return @notification_service if defined?(@notification_service)
159
+
160
+ @notification_service = begin
161
+ deps.dependencies.resolve(:notification_service)
162
+ rescue StandardError
163
+ nil
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../../application/selectors/reader_selectors.rb'
4
+
5
+ module Shoko
6
+ module Adapters::Output::Ui
7
+ module Rendering
8
+ # RenderPipeline encapsulates the high-level rendering steps for
9
+ # component-driven frames and full-screen mode components.
10
+ class RenderPipeline
11
+ def initialize(dependencies)
12
+ @dependencies = dependencies
13
+ @state = @dependencies.resolve(:global_state)
14
+ end
15
+
16
+ # Render the standard layout + overlay path
17
+ def render_layout(surface, bounds, layout, overlay)
18
+ # Clear rendered lines for the new frame so overlays can rely on state
19
+ @state.dispatch(Shoko::Application::Actions::ClearRenderedLinesAction.new)
20
+
21
+ dim_layout = annotation_overlay_active?
22
+
23
+ if dim_layout
24
+ surface.with_dimmed { layout.render(surface, bounds) }
25
+ else
26
+ layout.render(surface, bounds)
27
+ end
28
+
29
+ overlay.render(surface, bounds)
30
+ end
31
+
32
+ # Render a dedicated full-screen component (e.g., editor)
33
+ def render_mode_component(component, surface, bounds)
34
+ surface.fill(bounds, ' ')
35
+ component.render(surface, bounds)
36
+ end
37
+
38
+ # Generic component render helper for non-reader screens (menu, dialogs)
39
+ def render_component(surface, bounds, component)
40
+ surface.fill(bounds, ' ')
41
+ component.render(surface, bounds)
42
+ end
43
+
44
+ private
45
+
46
+ def annotation_overlay_active?
47
+ overlay = Shoko::Application::Selectors::ReaderSelectors.annotation_editor_overlay(@state)
48
+ overlay.respond_to?(:visible?) && overlay.visible?
49
+ rescue StandardError
50
+ false
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'tempfile'
5
+
6
+ module Shoko
7
+ module Adapters::Storage
8
+ # Provides atomic, fsync-backed file writes to avoid partial/corrupt files.
9
+ class AtomicFileWriter
10
+ def self.write(path, data, binary: false)
11
+ write_using(path, binary:) do |io|
12
+ io.write(data)
13
+ end
14
+ end
15
+
16
+ def self.write_using(path, binary: false)
17
+ dir = File.dirname(path)
18
+ FileUtils.mkdir_p(dir)
19
+ tempfile = Tempfile.new(['shoko', File.basename(path)], dir)
20
+ tempfile.binmode if binary
21
+ yield(tempfile)
22
+ tempfile.flush
23
+ tempfile.fsync
24
+ temp_path = tempfile.path
25
+ tempfile.close
26
+ File.rename(temp_path, path)
27
+ ensure
28
+ if tempfile
29
+ begin
30
+ tempfile.close unless tempfile.closed?
31
+ rescue StandardError
32
+ # ignore cleanup errors
33
+ end
34
+ begin
35
+ tempfile.unlink if tempfile.path && File.exist?(tempfile.path)
36
+ rescue StandardError
37
+ # ignore cleanup errors
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../monitoring/logger.rb'
4
+
5
+ module Shoko
6
+ module Adapters::Storage
7
+ # Single-thread worker with monitored queue and graceful shutdown semantics.
8
+ class BackgroundWorker
9
+ def initialize(name: 'shoko-worker')
10
+ @name = name
11
+ @queue = Queue.new
12
+ @shutdown = false
13
+ @mutex = Mutex.new
14
+ @thread = spawn_thread
15
+ end
16
+
17
+ def submit(&block)
18
+ raise ArgumentError, 'block required' unless block
19
+
20
+ @mutex.synchronize do
21
+ raise WorkerStoppedError, 'worker is shutting down' if @shutdown
22
+
23
+ @queue << block
24
+ end
25
+ end
26
+
27
+ def shutdown(timeout: 2.0)
28
+ thread = nil
29
+ @mutex.synchronize do
30
+ return if @shutdown
31
+
32
+ @shutdown = true
33
+ thread = @thread
34
+ @queue << nil if thread&.alive?
35
+ end
36
+ thread&.join(timeout)
37
+ ensure
38
+ @thread = nil
39
+ end
40
+
41
+ class WorkerStoppedError < StandardError; end
42
+
43
+ private
44
+
45
+ def spawn_thread
46
+ Thread.new do
47
+ Thread.current.name = @name if Thread.current.respond_to?(:name=)
48
+ loop do
49
+ job = @queue.pop
50
+ break if job.nil? && @shutdown
51
+
52
+ next unless job
53
+
54
+ begin
55
+ job.call
56
+ rescue StandardError => e
57
+ Adapters::Monitoring::Logger.error('Background worker job failed',
58
+ worker: @name,
59
+ error: e.message)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end