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,262 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest/sha1'
4
+
5
+ require_relative '../../../terminal/text_metrics.rb'
6
+ require_relative '../../../kitty/kitty_unicode_placeholders.rb'
7
+ require_relative '../render_style'
8
+ require_relative 'config_helpers'
9
+
10
+ module Shoko
11
+ module Adapters::Output::Ui::Components
12
+ module Reading
13
+ # Renders DisplayLine image metadata using Kitty graphics placeholders.
14
+ #
15
+ # This object is created per render frame and uses a per-frame dedupe cache to avoid
16
+ # repeated `prepare_virtual` calls for the same image placement.
17
+ class KittyImageLineRenderer
18
+ RenderRequest = Struct.new(:meta, :render_opts, :src, :cols, :rows, :col_offset, :line_index, :chapter_entry,
19
+ keyword_init: true)
20
+
21
+ def initialize(dependencies:, placed_kitty_images:)
22
+ @dependencies = dependencies
23
+ @placed_kitty_images = placed_kitty_images
24
+ end
25
+
26
+ def kitty_image_line?(line, config:)
27
+ meta = metadata_hash(line)
28
+ return false unless meta
29
+ return false unless enabled?(config)
30
+
31
+ render_opts = image_render_options(meta)
32
+ return false unless render_opts
33
+
34
+ !image_src(meta).to_s.strip.empty?
35
+ rescue StandardError
36
+ false
37
+ end
38
+
39
+ def render(line, context)
40
+ request = render_request_for(line)
41
+ return [nil, 0] unless request
42
+
43
+ placement_id = placement_id_for(render_opts: request.render_opts,
44
+ chapter_entry: request.chapter_entry,
45
+ src: request.src,
46
+ cols: request.cols,
47
+ rows: request.rows)
48
+ placeholder = placeholder_for_request(context, request, placement_id)
49
+ return [placeholder, request.col_offset] if placeholder
50
+
51
+ fallback_for(request.meta, request.cols, request.col_offset)
52
+ rescue StandardError
53
+ [nil, 0]
54
+ end
55
+
56
+ private
57
+
58
+ def render_request_for(line)
59
+ meta, render_opts = meta_and_render_options(line)
60
+ return nil unless meta && render_opts
61
+
62
+ src = src_for_request(meta)
63
+ return nil unless src
64
+
65
+ build_render_request(meta, render_opts, src)
66
+ end
67
+
68
+ def placeholder_for_request(context, request, placement_id)
69
+ prepared_id = prepared_id_for(context: context,
70
+ chapter_entry: request.chapter_entry,
71
+ src: request.src,
72
+ cols: request.cols,
73
+ rows: request.rows,
74
+ placement_id: placement_id)
75
+ return nil unless prepared_id
76
+
77
+ placeholder_for(prepared_id, placement_id, request.cols, request.line_index)
78
+ end
79
+
80
+ def metadata_hash(line)
81
+ return nil unless line.respond_to?(:metadata)
82
+
83
+ meta = line.metadata
84
+ meta.is_a?(Hash) ? meta : nil
85
+ end
86
+
87
+ def enabled?(config)
88
+ store = ConfigHelpers.config_store(config)
89
+ kitty_image_renderer&.enabled?(store)
90
+ rescue StandardError
91
+ false
92
+ end
93
+
94
+ def image_render_options(meta)
95
+ render_opts = meta[:image_render] || meta['image_render']
96
+ render_opts.is_a?(Hash) ? render_opts : nil
97
+ end
98
+
99
+ def image_src(meta)
100
+ image = meta[:image] || meta['image'] || {}
101
+ image[:src] || image['src']
102
+ end
103
+
104
+ def integer_or_zero(value)
105
+ value.to_i
106
+ rescue StandardError
107
+ 0
108
+ end
109
+
110
+ def meta_and_render_options(line)
111
+ meta = metadata_hash(line)
112
+ return [nil, nil] unless meta
113
+
114
+ [meta, image_render_options(meta)]
115
+ end
116
+
117
+ def src_for_request(meta)
118
+ src = image_src(meta).to_s.strip
119
+ src.empty? ? nil : src
120
+ end
121
+
122
+ def build_render_request(meta, render_opts, src)
123
+ RenderRequest.new(
124
+ meta: meta,
125
+ render_opts: render_opts,
126
+ src: src,
127
+ cols: integer_or_zero(render_opts[:cols] || render_opts['cols']),
128
+ rows: integer_or_zero(render_opts[:rows] || render_opts['rows']),
129
+ col_offset: integer_or_zero(render_opts[:col_offset] || render_opts['col_offset']),
130
+ line_index: integer_or_zero(meta[:image_line_index] || meta['image_line_index']),
131
+ chapter_entry: meta[:chapter_source_path] || meta['chapter_source_path']
132
+ )
133
+ end
134
+
135
+ def placement_id_for(render_opts:, chapter_entry:, src:, cols:, rows:)
136
+ raw = integer_or_zero(render_opts[:placement_id] || render_opts['placement_id'])
137
+ return normalize_placement_id(raw) if raw.positive?
138
+
139
+ seed = placement_seed(chapter_entry, src, cols, rows)
140
+ normalize_placement_id(Digest::SHA1.digest(seed).unpack1('N'))
141
+ rescue StandardError
142
+ 1
143
+ end
144
+
145
+ def placement_seed(chapter_entry, src, cols, rows)
146
+ core = core_src(src)
147
+ "#{chapter_entry}|#{core}|#{cols}|#{rows}"
148
+ end
149
+
150
+ def core_src(src)
151
+ src.to_s.split(/[?#]/, 2).first.to_s
152
+ rescue StandardError
153
+ src.to_s
154
+ end
155
+
156
+ def normalize_placement_id(raw)
157
+ value = raw.to_i & 0xFF_FF_FF
158
+ value.zero? ? 1 : value
159
+ rescue StandardError
160
+ 1
161
+ end
162
+
163
+ def prepared_id_for(context:, chapter_entry:, src:, cols:, rows:, placement_id:)
164
+ dedupe = dedupe_key(chapter_entry:, src:, cols:, rows:, placement_id:)
165
+ prepared = cached_prepared_id(dedupe)
166
+ return prepared if prepared
167
+
168
+ prepared = prepare_virtual(context:, chapter_entry:, src:, cols:, rows:, placement_id:)
169
+ cache_prepared_id(dedupe, prepared)
170
+ prepared
171
+ end
172
+
173
+ def dedupe_key(chapter_entry:, src:, cols:, rows:, placement_id:)
174
+ core = core_src(src)
175
+ "#{chapter_entry}|#{core}|#{cols}|#{rows}|p=#{placement_id}"
176
+ rescue StandardError
177
+ nil
178
+ end
179
+
180
+ def cached_prepared_id(dedupe_key)
181
+ return nil unless dedupe_key && @placed_kitty_images.is_a?(Hash)
182
+
183
+ cached = @placed_kitty_images[dedupe_key]
184
+ return cached if cached.is_a?(Integer)
185
+
186
+ nil
187
+ end
188
+
189
+ def cache_prepared_id(dedupe_key, prepared_id)
190
+ return unless dedupe_key && @placed_kitty_images.is_a?(Hash)
191
+
192
+ @placed_kitty_images[dedupe_key] = prepared_id || false
193
+ rescue StandardError
194
+ nil
195
+ end
196
+
197
+ def prepare_virtual(context:, chapter_entry:, src:, cols:, rows:, placement_id:)
198
+ renderer = kitty_image_renderer
199
+ return nil unless renderer
200
+
201
+ renderer.prepare_virtual(
202
+ **prepare_virtual_args(
203
+ context: context,
204
+ chapter_entry: chapter_entry,
205
+ src: src,
206
+ cols: cols,
207
+ rows: rows,
208
+ placement_id: placement_id
209
+ )
210
+ )
211
+ rescue StandardError
212
+ nil
213
+ end
214
+
215
+ def prepare_virtual_args(context:, chapter_entry:, src:, cols:, rows:, placement_id:)
216
+ doc = context&.document
217
+ {
218
+ output: Terminal,
219
+ book_sha: doc.respond_to?(:cache_sha) ? doc.cache_sha : nil,
220
+ epub_path: doc.respond_to?(:canonical_path) ? doc.canonical_path : nil,
221
+ chapter_entry_path: chapter_entry,
222
+ src: src,
223
+ cols: cols,
224
+ rows: rows,
225
+ placement_id: placement_id,
226
+ z: -1,
227
+ }
228
+ end
229
+
230
+ def placeholder_for(prepared_id, placement_id, cols, line_index)
231
+ cols_i = cols.to_i
232
+ line_i = line_index.to_i
233
+ return nil unless cols_i.between?(1, 255) && line_i.between?(0, 255)
234
+
235
+ grid = (line_i << 8) | cols_i
236
+ Shoko::Adapters::Output::Kitty::KittyUnicodePlaceholders.line(
237
+ image_id: prepared_id,
238
+ placement_id: placement_id,
239
+ grid: grid
240
+ )
241
+ rescue StandardError
242
+ nil
243
+ end
244
+
245
+ def fallback_for(meta, cols, col_offset)
246
+ render_line = meta.key?(:image_render_line) ? meta[:image_render_line] : meta['image_render_line']
247
+ return ['', col_offset] unless render_line == true
248
+
249
+ plain = '[Image]'
250
+ clipped = cols.to_i.positive? ? Shoko::Adapters::Output::Terminal::TextMetrics.truncate_to(plain, cols.to_i) : plain
251
+ [Shoko::Adapters::Output::Ui::Components::RenderStyle.dim(clipped), col_offset]
252
+ end
253
+
254
+ def kitty_image_renderer
255
+ @kitty_image_renderer ||= @dependencies.resolve(:kitty_image_renderer)
256
+ rescue StandardError
257
+ nil
258
+ end
259
+ end
260
+ end
261
+ end
262
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../render_style'
4
+ require_relative '../../../terminal/text_metrics.rb'
5
+ require_relative 'inline_segment_highlighter'
6
+ require_relative 'config_helpers'
7
+
8
+ module Shoko
9
+ module Adapters::Output::Ui::Components
10
+ module Reading
11
+ # Composes the plain and ANSI-styled text for a renderable line.
12
+ class LineContentComposer
13
+ def compose(line, width, config_store)
14
+ width_i = width.to_i
15
+ return ['', ''] if width_i <= 0
16
+
17
+ return compose_display_line(line, width_i, config_store) if display_line?(line)
18
+
19
+ compose_plain_line(line, width_i, config_store)
20
+ end
21
+
22
+ private
23
+
24
+ def display_line?(line)
25
+ line.respond_to?(:segments) && line.respond_to?(:text)
26
+ end
27
+
28
+ def compose_plain_line(line, width, store)
29
+ text = Shoko::Adapters::Output::Terminal::TextMetrics.truncate_to(line.to_s, width)
30
+ return [text, Shoko::Adapters::Output::Ui::Components::RenderStyle.primary(text)] unless store
31
+
32
+ text = highlight_keywords(text) if ConfigHelpers.highlight_keywords?(store)
33
+ text = highlight_quotes(text) if ConfigHelpers.highlight_quotes?(store)
34
+ [Shoko::Adapters::Output::Terminal::TextMetrics.strip_ansi(text), Shoko::Adapters::Output::Ui::Components::RenderStyle.primary(text)]
35
+ end
36
+
37
+ def compose_display_line(line, width, store)
38
+ highlight_quotes = ConfigHelpers.highlight_quotes?(store)
39
+ highlight_keywords = ConfigHelpers.highlight_keywords?(store)
40
+
41
+ metadata = display_line_metadata(line, highlight_quotes)
42
+ block_type = metadata[:block_type] || metadata['block_type']
43
+ segments = InlineSegmentHighlighter.apply(Array(line.segments),
44
+ block_type: block_type,
45
+ highlight_quotes: highlight_quotes,
46
+ highlight_keywords: highlight_keywords)
47
+ build_from_segments(line, segments, width, metadata)
48
+ end
49
+
50
+ def display_line_metadata(line, highlight_quotes)
51
+ metadata = (line.metadata || {}).dup
52
+ metadata[:highlight_enabled] = highlight_quotes
53
+ metadata
54
+ end
55
+
56
+ def build_from_segments(line, segments, width, metadata)
57
+ plain = +''
58
+ styled = +''
59
+ remaining = width
60
+
61
+ segments.each do |segment|
62
+ break if remaining <= 0
63
+
64
+ chunk = segment_text_for_width(segment, remaining)
65
+ next if chunk.empty?
66
+
67
+ plain << chunk
68
+ styled << Shoko::Adapters::Output::Ui::Components::RenderStyle.styled_segment(chunk, segment.styles || {},
69
+ metadata: metadata)
70
+ remaining -= Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(chunk)
71
+ end
72
+
73
+ finalize_composed_line(line, width, plain, styled)
74
+ end
75
+
76
+ def segment_text_for_width(segment, remaining)
77
+ raw = segment&.text.to_s
78
+ return '' if raw.empty?
79
+
80
+ visible_len = Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(raw)
81
+ return raw if visible_len <= remaining
82
+
83
+ Shoko::Adapters::Output::Terminal::TextMetrics.truncate_to(raw, remaining)
84
+ end
85
+
86
+ def finalize_composed_line(line, width, plain_builder, styled_builder)
87
+ if styled_builder.empty?
88
+ plain_text = plain_builder.empty? ? line.text.to_s[0, width] : plain_builder
89
+ return [plain_text, Shoko::Adapters::Output::Ui::Components::RenderStyle.primary(plain_text)]
90
+ end
91
+
92
+ plain_text = plain_builder.empty? ? line.text.to_s[0, width] : plain_builder
93
+ [plain_text, styled_builder]
94
+ end
95
+
96
+ def highlight_keywords(line)
97
+ accent = Shoko::Adapters::Output::Ui::Components::RenderStyle.color(:accent)
98
+ base = Shoko::Adapters::Output::Ui::Components::RenderStyle.color(:primary)
99
+ line.gsub(Shoko::Adapters::Output::Ui::Constants::Highlighting::HIGHLIGHT_PATTERNS) do |match|
100
+ accent + match + Terminal::ANSI::RESET + base
101
+ end
102
+ end
103
+
104
+ def highlight_quotes(line)
105
+ quote_color = Shoko::Adapters::Output::Ui::Components::RenderStyle.color(:quote)
106
+ base = Shoko::Adapters::Output::Ui::Components::RenderStyle.color(:primary)
107
+ line.gsub(Shoko::Adapters::Output::Ui::Constants::Highlighting::QUOTE_PATTERNS) do |match|
108
+ quote_color + Terminal::ANSI::ITALIC + match + Terminal::ANSI::RESET + base
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../terminal/text_metrics.rb'
4
+ require_relative 'config_helpers'
5
+ require_relative 'kitty_image_line_renderer'
6
+ require_relative 'line_content_composer'
7
+ require_relative 'line_geometry_builder'
8
+ require_relative 'rendered_lines_recorder'
9
+
10
+ module Shoko
11
+ module Adapters::Output::Ui::Components
12
+ module Reading
13
+ # Draws a single line into a Surface and records its geometry.
14
+ class LineDrawer
15
+ def initialize(dependencies:, rendered_lines_buffer:, placed_kitty_images:, record_geometry: true)
16
+ @dependencies = dependencies
17
+ @record_geometry = record_geometry
18
+ @content_composer = LineContentComposer.new
19
+ if @record_geometry
20
+ @geometry_builder = LineGeometryBuilder.new
21
+ @recorder = RenderedLinesRecorder.new(buffer: rendered_lines_buffer, dependencies: dependencies)
22
+ end
23
+ @kitty_renderer = KittyImageLineRenderer.new(dependencies: dependencies,
24
+ placed_kitty_images: placed_kitty_images)
25
+ end
26
+
27
+ def draw_line(surface:, bounds:, line:, row:, col:, width:, context:, column_id:, line_offset:, page_id:)
28
+ config = context&.config
29
+ store = ConfigHelpers.config_store(config)
30
+
31
+ return if draw_kitty_line(surface: surface, bounds: bounds, line: line, row: row, col: col,
32
+ context: context, store: store)
33
+
34
+ _plain_text, styled_text = @content_composer.compose(line, width, store)
35
+ abs_row, abs_col = absolute_cell(bounds, row, col)
36
+ clipped_styled, clipped_plain = clip_to_bounds(styled_text, width, bounds, abs_col)
37
+
38
+ if @record_geometry
39
+ geometry = @geometry_builder.build(page_id: page_id, column_id: column_id, row: abs_row, col: abs_col,
40
+ line_offset: line_offset, plain_text: clipped_plain,
41
+ styled_text: clipped_styled)
42
+ @recorder.record(geometry)
43
+ end
44
+ surface.write(bounds, row, col, clipped_styled)
45
+ end
46
+
47
+ private
48
+
49
+ def draw_kitty_line(surface:, bounds:, line:, row:, col:, context:, store:)
50
+ return false unless @kitty_renderer.kitty_image_line?(line, config: store)
51
+
52
+ image_text, col_offset = @kitty_renderer.render(line, context)
53
+ return true unless image_text && !image_text.empty?
54
+
55
+ surface.write(bounds, row, col + col_offset.to_i, image_text)
56
+ true
57
+ rescue StandardError
58
+ false
59
+ end
60
+
61
+ def absolute_cell(bounds, row, col)
62
+ [bounds.y + row - 1, bounds.x + col - 1]
63
+ end
64
+
65
+ def clip_to_bounds(styled_text, width, bounds, abs_col)
66
+ max_width = [width.to_i, bounds.right - abs_col + 1].min
67
+ max_width = 0 if max_width.negative?
68
+ start_column = [abs_col - 1, 0].max
69
+
70
+ clipped_styled = clipped_styled_text(styled_text, max_width, start_column)
71
+ clipped_plain = Shoko::Adapters::Output::Terminal::TextMetrics.strip_ansi(clipped_styled)
72
+ [clipped_styled, clipped_plain]
73
+ end
74
+
75
+ def clipped_styled_text(styled_text, max_width, start_column)
76
+ return '' unless max_width.positive?
77
+
78
+ Shoko::Adapters::Output::Terminal::TextMetrics.truncate_to(
79
+ styled_text.to_s,
80
+ max_width,
81
+ start_column: start_column
82
+ )
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../terminal/text_metrics.rb'
4
+ require_relative '../../../rendering/models/line_geometry.rb'
5
+
6
+ module Shoko
7
+ module Adapters::Output::Ui::Components
8
+ module Reading
9
+ # Builds `Shoko::Adapters::Output::Rendering::Models::LineGeometry` objects for selection/highlighting.
10
+ class LineGeometryBuilder
11
+ def build(page_id:, column_id:, row:, col:, line_offset:, plain_text:, styled_text:)
12
+ cell_data = Shoko::Adapters::Output::Terminal::TextMetrics.cell_data_for(plain_text.to_s)
13
+ cells = cell_data.map { |cell| build_cell(cell) }
14
+
15
+ Shoko::Adapters::Output::Rendering::Models::LineGeometry.new(
16
+ page_id: page_id,
17
+ column_id: column_id,
18
+ row: row,
19
+ column_origin: col,
20
+ line_offset: line_offset,
21
+ plain_text: plain_text,
22
+ styled_text: styled_text,
23
+ cells: cells
24
+ )
25
+ end
26
+
27
+ private
28
+
29
+ def build_cell(cell)
30
+ Shoko::Adapters::Output::Rendering::Models::LineCell.new(
31
+ cluster: cell[:cluster],
32
+ char_start: cell[:char_start],
33
+ char_end: cell[:char_end],
34
+ display_width: cell[:display_width],
35
+ screen_x: cell[:screen_x]
36
+ )
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoko
4
+ module Adapters::Output::Ui::Components
5
+ module Reading
6
+ # Records per-line geometry into the state buffer so selection/overlays can
7
+ # use the exact rendered layout.
8
+ class RenderedLinesRecorder
9
+ def initialize(buffer:, dependencies:)
10
+ @buffer = buffer
11
+ @dependencies = dependencies
12
+ end
13
+
14
+ def record(geometry)
15
+ return unless @buffer.is_a?(Hash)
16
+ return if skip_geometry?(geometry)
17
+
18
+ width = geometry.visible_width
19
+ @buffer[geometry.key] = entry_for(geometry, width)
20
+
21
+ dump_geometry(geometry) if geometry_debug_enabled?
22
+ end
23
+
24
+ private
25
+
26
+ def skip_geometry?(geometry)
27
+ width = geometry.visible_width
28
+ width <= 0 && geometry.plain_text.to_s.empty?
29
+ end
30
+
31
+ def entry_for(geometry, width)
32
+ end_col = geometry.column_origin + width - 1
33
+ {
34
+ row: geometry.row,
35
+ col: geometry.column_origin,
36
+ col_end: end_col,
37
+ text: geometry.plain_text,
38
+ width: width,
39
+ geometry: geometry,
40
+ }
41
+ end
42
+
43
+ def geometry_debug_enabled?
44
+ primary = ENV.fetch('SHOKO_DEBUG_GEOMETRY', '').to_s.strip
45
+ primary == '1'
46
+ end
47
+
48
+ def dump_geometry(geometry)
49
+ payload = geometry.to_h
50
+ logger = resolve_logger
51
+ return logger.debug('geometry.line', payload) if logger
52
+
53
+ warn("[geometry] #{payload}")
54
+ end
55
+
56
+ def resolve_logger
57
+ @dependencies.resolve(:logger)
58
+ rescue StandardError
59
+ nil
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end