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,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_screen_component'
4
+ require_relative '../../constants/ui_constants'
5
+ require_relative '../ui/list_helpers'
6
+ require_relative '../ui/text_utils'
7
+
8
+ module Shoko
9
+ module Adapters::Output::Ui::Components
10
+ module Screens
11
+ # LibraryScreenComponent renders the cached library view with
12
+ # sortable columns and paging of visible items.
13
+ class LibraryScreenComponent < BaseScreenComponent
14
+ include UI::TextUtils
15
+
16
+ Item = Struct.new(:title, :authors, :year, :last_accessed, :size_bytes, :open_path, :epub_path,
17
+ keyword_init: true)
18
+ ItemRenderCtx = Struct.new(:row, :width, :book, :index, :selected, keyword_init: true)
19
+
20
+ def initialize(state, dependencies)
21
+ super(dependencies)
22
+ @state = state
23
+ @catalog = dependencies.resolve(:catalog_service)
24
+ @items = nil
25
+ # Observe selection changes to support scrolling
26
+ @state.add_observer(self, %i[menu browse_selected])
27
+ end
28
+
29
+ def state_changed(_path, _old, _new)
30
+ invalidate
31
+ end
32
+
33
+ def do_render(surface, bounds)
34
+ items = load_items
35
+ selected = Shoko::Application::Selectors::MenuSelectors.browse_selected(@state) || 0
36
+
37
+ render_header(surface, bounds)
38
+
39
+ if items.empty?
40
+ render_empty(surface, bounds)
41
+ else
42
+ render_library(surface, bounds, items, selected)
43
+ end
44
+
45
+ render_footer(surface, bounds)
46
+ end
47
+
48
+ private
49
+
50
+ def load_items
51
+ return @items if @items
52
+
53
+ entries = Array(@catalog.cached_library_entries)
54
+ @items = entries.map do |entry|
55
+ Item.new(
56
+ title: entry[:title] || entry['title'],
57
+ authors: entry[:authors] || entry['authors'],
58
+ year: entry[:year] || entry['year'],
59
+ last_accessed: entry[:last_accessed] || entry['last_accessed'],
60
+ size_bytes: entry[:size_bytes] || entry['size_bytes'] || @catalog.size_for(entry[:open_path] || entry['open_path']),
61
+ open_path: entry[:open_path] || entry['open_path'],
62
+ epub_path: entry[:epub_path] || entry['epub_path']
63
+ )
64
+ end
65
+ end
66
+
67
+ def render_header(surface, bounds)
68
+ write_header(surface, bounds, "#{Adapters::Output::Ui::Constants::UI::COLOR_TEXT_ACCENT} Library (Cached)#{Terminal::ANSI::RESET}")
69
+ end
70
+
71
+ def render_empty(surface, bounds)
72
+ write_empty_message(surface, bounds, "#{Adapters::Output::Ui::Constants::UI::COLOR_TEXT_DIM}No cached books yet#{Terminal::ANSI::RESET}")
73
+ end
74
+
75
+ def render_library(surface, bounds, items, selected)
76
+ list_start = 4
77
+ width = bounds.width
78
+ height = bounds.height
79
+ list_height = height - list_start - 2
80
+ return if list_height <= 0
81
+
82
+ draw_list_header(surface, bounds, width, list_start)
83
+ list_start += 2
84
+ list_height -= 2
85
+
86
+ items_per_page = list_height
87
+ start_index, visible_items = UI::ListHelpers.slice_visible(items, items_per_page, selected)
88
+
89
+ current_row = list_start
90
+ visible_items.each_with_index do |book, i|
91
+ break if current_row >= height - 1
92
+
93
+ ctx = ItemRenderCtx.new(row: current_row, width: width, book: book,
94
+ index: start_index + i, selected: selected)
95
+ render_library_item(surface, bounds, ctx)
96
+ current_row += 1
97
+ end
98
+ end
99
+
100
+ def draw_list_header(surface, bounds, width, row)
101
+ dims = compute_column_widths(width)
102
+
103
+ headers = [
104
+ pad_right('Title', dims[:title_w]),
105
+ pad_right('Author(s)', dims[:author_w]),
106
+ pad_right('Year', dims[:year_w]),
107
+ pad_right('Last accessed', dims[:last_w]),
108
+ pad_left('Size', dims[:size_w]),
109
+ ].join(' ' * dims[:gap])
110
+ header_style = Terminal::ANSI::BOLD + Terminal::ANSI::LIGHT_GREY
111
+ header_line = header_style + (' ' * dims[:pointer_w]) + headers + Terminal::ANSI::RESET
112
+ surface.write(bounds, row, 1, header_line)
113
+ divider = '─' * [width - 2, 1].max
114
+ divider_line = Adapters::Output::Ui::Constants::UI::COLOR_TEXT_DIM + divider + Terminal::ANSI::RESET
115
+ surface.write(bounds, row + 1, 1, divider_line)
116
+ end
117
+
118
+ def render_library_item(surface, bounds, ctx)
119
+ is_selected = (ctx.index == ctx.selected)
120
+ dims = compute_column_widths(ctx.width)
121
+
122
+ pointer = is_selected ? '▸ ' : ' '
123
+ book = ctx.book
124
+ t_w = dims[:title_w]
125
+ a_w = dims[:author_w]
126
+ y_w = dims[:year_w]
127
+ l_w = dims[:last_w]
128
+ s_w = dims[:size_w]
129
+ title_col = pad_right(truncate_text((book.title || 'Unknown').to_s, t_w), t_w)
130
+ author_col = pad_right(truncate_text((book.authors || '').to_s, a_w), a_w)
131
+ year_col = pad_right((book.year || '').to_s[0, 4], y_w)
132
+ last_col = pad_right(truncate_text(relative_accessed_label(book.last_accessed), l_w), l_w)
133
+ size_col = pad_left(format_size(book.size_bytes), s_w)
134
+
135
+ line = [title_col, author_col, year_col, last_col, size_col].join(' ' * dims[:gap])
136
+ style = is_selected ? Adapters::Output::Ui::Constants::UI::SELECTION_HIGHLIGHT : Adapters::Output::Ui::Constants::UI::COLOR_TEXT_PRIMARY
137
+ surface.write(bounds, ctx.row, 1, style + pointer + line + Terminal::ANSI::RESET)
138
+ end
139
+
140
+ def compute_column_widths(total_width)
141
+ pointer_w = 2
142
+ gap = 2
143
+ remaining = total_width - pointer_w - (gap * 4)
144
+ year_w = 6
145
+ last_w = 16
146
+ size_w = 8
147
+ author_w = [(remaining * 0.25).to_i, 12].max.clamp(12, remaining - 20 - year_w - last_w - size_w)
148
+ title_w = [remaining - author_w - year_w - last_w - size_w, 20].max
149
+ { pointer_w: pointer_w, gap: gap, title_w: title_w, author_w: author_w,
150
+ year_w: year_w, last_w: last_w, size_w: size_w }
151
+ end
152
+
153
+ def format_size(bytes)
154
+ mb = (bytes.to_f / (1024 * 1024)).round(1)
155
+ format('%.1f MB', mb)
156
+ end
157
+
158
+ def relative_accessed_label(iso)
159
+ return '' unless iso
160
+
161
+ t = begin
162
+ Time.parse(iso)
163
+ rescue StandardError
164
+ nil
165
+ end
166
+ return '' unless t
167
+
168
+ seconds = (Time.now - t).to_i
169
+ minutes = seconds / 60
170
+ hours = seconds / 3600
171
+ days = seconds / 86_400
172
+ weeks = days / 7
173
+
174
+ if hours < 1
175
+ minutes <= 1 ? 'a minute ago' : "#{minutes} minutes ago"
176
+ elsif days < 1
177
+ hours == 1 ? 'an hour ago' : "#{hours} hours ago"
178
+ elsif days == 1
179
+ 'yesterday'
180
+ elsif days < 7
181
+ "#{days} days ago"
182
+ else
183
+ weeks == 1 ? 'a week ago' : "#{weeks} weeks ago"
184
+ end
185
+ end
186
+
187
+ def render_footer(surface, bounds)
188
+ write_footer(surface, bounds,
189
+ "#{Adapters::Output::Ui::Constants::UI::COLOR_TEXT_DIM}↑↓ Navigate • Enter Open • ESC Back#{Terminal::ANSI::RESET}")
190
+ end
191
+
192
+ public
193
+
194
+ # Public accessor for items to avoid reflective access from MainMenu
195
+ def items
196
+ load_items
197
+ end
198
+
199
+ def invalidate_cache!
200
+ @items = nil
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base_component'
4
+ require_relative '../../../terminal/text_metrics.rb'
5
+
6
+ module Shoko
7
+ module Adapters::Output::Ui::Components
8
+ module Screens
9
+ # Simple progress/loading overlay rendered as a component.
10
+ # Draws a single-row progress bar; expects progress in state at [:ui, :loading_progress].
11
+ class LoadingOverlayComponent < BaseComponent
12
+ include Adapters::Output::Ui::Constants::UI
13
+
14
+ def do_render(surface, bounds)
15
+ width = bounds.width
16
+ height = bounds.height
17
+
18
+ state = @dependencies.resolve(:global_state)
19
+ message = state.get(%i[ui loading_message]).to_s.strip
20
+
21
+ message_row = 1
22
+ bar_row = message.empty? ? 2 : message_row + 2
23
+ bar_row = [bar_row, height - 1].min
24
+ bar_col = 2
25
+ bar_width = (width - (bar_col + 1)).clamp(10, width - bar_col)
26
+
27
+ progress = (state.get(%i[ui loading_progress]) || 0.0).to_f
28
+ progress = progress.clamp(0.0, 1.0)
29
+ filled = (bar_width * progress).round
30
+
31
+ unless message.empty?
32
+ label = Shoko::Adapters::Output::Terminal::TextMetrics.truncate_to(message, width - 2)
33
+ label_col = [(width - Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(label)) / 2, 1].max
34
+ surface.write(bounds, message_row, label_col, "#{COLOR_TEXT_DIM}#{label}#{Terminal::ANSI::RESET}")
35
+ end
36
+
37
+ track = if bar_width.positive?
38
+ (Terminal::ANSI::BRIGHT_GREEN + ('━' * filled)) +
39
+ (Terminal::ANSI::GRAY + ('━' * (bar_width - filled))) +
40
+ Terminal::ANSI::RESET
41
+ else
42
+ ''
43
+ end
44
+ surface.write(bounds, bar_row, bar_col, track)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_screen_component'
4
+ require_relative '../../constants/ui_constants'
5
+
6
+ module Shoko
7
+ module Adapters::Output::Ui::Components
8
+ module Screens
9
+ # Component-based renderer for the main menu screen
10
+ class MenuScreenComponent < BaseScreenComponent
11
+ MenuItemCtx = Struct.new(:row, :item, :index, :selected, :indent, keyword_init: true)
12
+
13
+ def initialize(state)
14
+ super()
15
+ @state = state
16
+ @state.add_observer(self, %i[menu selected])
17
+ end
18
+
19
+ MENU_ITEMS = [
20
+ { icon: '', label: 'Browse Library' },
21
+ { icon: '', label: 'Library' },
22
+ { icon: '', label: 'Annotations' },
23
+ { icon: '', label: 'Download Books' },
24
+ { icon: '', label: 'Settings' },
25
+ { icon: '', label: 'Quit' },
26
+ ].freeze
27
+
28
+ def do_render(surface, bounds)
29
+ selected = Shoko::Application::Selectors::MenuSelectors.selected(@state) || 0
30
+
31
+ render_menu_items(surface, bounds, selected)
32
+ end
33
+
34
+ private
35
+
36
+ def render_menu_items(surface, bounds, selected)
37
+ metrics = layout_metrics(bounds)
38
+
39
+ MENU_ITEMS.each_with_index do |item, index|
40
+ row = metrics[:start_row] + (index * metrics[:row_height])
41
+ break if row >= metrics[:max_row]
42
+
43
+ ctx = MenuItemCtx.new(row: row, item: item, index: index,
44
+ selected: selected, indent: metrics[:indent])
45
+ render_menu_item(surface, bounds, ctx)
46
+ end
47
+ end
48
+
49
+ def render_menu_item(surface, bounds, ctx)
50
+ item = ctx.item
51
+ row = ctx.row
52
+ indent = ctx.indent
53
+
54
+ colors = row_colors(ctx.index == ctx.selected)
55
+
56
+ surface.write(bounds, row, indent,
57
+ formatted_row(item[:icon], item[:label], colors))
58
+ end
59
+
60
+ def layout_metrics(bounds)
61
+ height = bounds.height
62
+ width = bounds.width
63
+ content_width = menu_content_width
64
+ indent = ((width - content_width) / 2).floor
65
+ indent = indent.clamp(2, [width - content_width, 0].max)
66
+ row_height = 2
67
+ {
68
+ indent: indent,
69
+ row_height: row_height,
70
+ start_row: [(height - (MENU_ITEMS.size * row_height)) / 2, 4].max,
71
+ max_row: height - 4,
72
+ }
73
+ end
74
+
75
+ def formatted_row(icon, label, colors)
76
+ icon_col = icon.to_s
77
+ text = label
78
+ "#{colors[:prefix]}#{colors[:fg]}#{icon_col} #{text}#{Terminal::ANSI::RESET}"
79
+ end
80
+
81
+ def row_colors(selected)
82
+ if selected
83
+ {
84
+ prefix: Terminal::ANSI::BOLD,
85
+ fg: Adapters::Output::Ui::Constants::UI::COLOR_TEXT_ACCENT,
86
+ }
87
+ else
88
+ {
89
+ prefix: '',
90
+ fg: Adapters::Output::Ui::Constants::UI::COLOR_TEXT_PRIMARY,
91
+ }
92
+ end
93
+ end
94
+
95
+ def menu_content_width
96
+ max_label = MENU_ITEMS.map { |item| display_width(item[:label]) }.max
97
+ icon_width = MENU_ITEMS.map { |item| display_width(item[:icon]) }.max
98
+ icon_width + 2 + max_label
99
+ end
100
+
101
+ def display_width(text)
102
+ Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(text.to_s)
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base_component'
4
+ require_relative '../../constants/ui_constants'
5
+ require_relative '../../../terminal/text_metrics.rb'
6
+
7
+ module Shoko
8
+ module Adapters::Output::Ui::Components
9
+ module Screens
10
+ # Settings screen component for configuration management
11
+ class SettingsScreenComponent < BaseComponent
12
+ include Adapters::Output::Ui::Constants::UI
13
+
14
+ SettingsItem = Struct.new(:action, :icon, :label, keyword_init: true)
15
+
16
+ SETTINGS_ITEMS = [
17
+ SettingsItem.new(action: :back_to_menu, icon: '', label: 'Go Back'),
18
+ SettingsItem.new(action: :toggle_view_mode, icon: '', label: 'View Mode'),
19
+ SettingsItem.new(action: :cycle_line_spacing, icon: '', label: 'Line Spacing'),
20
+ SettingsItem.new(action: :toggle_page_numbering_mode, icon: '', label: 'Page Numbering Mode'),
21
+ SettingsItem.new(action: :toggle_page_numbers, icon: '', label: 'Page Numbers'),
22
+ SettingsItem.new(action: :toggle_highlight_quotes, icon: '', label: 'Text Highlighting'),
23
+ SettingsItem.new(action: :toggle_kitty_images, icon: '', label: 'Kitty Images'),
24
+ SettingsItem.new(action: :wipe_cache, icon: '', label: 'Wipe Cache'),
25
+ ].freeze
26
+
27
+ ItemCtx = Struct.new(:row, :item, :value_text, :value_color, :index, :selected, :indent, keyword_init: true)
28
+
29
+ def initialize(state, catalog_service = nil)
30
+ super()
31
+ @state = state
32
+ @catalog = catalog_service
33
+ end
34
+
35
+ def do_render(surface, bounds)
36
+ surface.write(bounds, 1, 2, "#{COLOR_TEXT_ACCENT}Settings#{Terminal::ANSI::RESET}")
37
+
38
+ selected = @state.get(%i[menu settings_selected]) || 1
39
+ text_values = setting_value_map
40
+ render_settings(surface, bounds, selected, text_values)
41
+ end
42
+
43
+ def preferred_height(_available_height)
44
+ :fill
45
+ end
46
+
47
+ private
48
+
49
+ def render_settings(surface, bounds, selected, text_values)
50
+ metrics = layout_metrics(bounds, text_values)
51
+ indent = metrics[:indent]
52
+ max_index = SETTINGS_ITEMS.length - 1
53
+ cursor = selected.clamp(0, max_index)
54
+ row = metrics[:start_row]
55
+ insert_toggle_gap = false
56
+
57
+ SETTINGS_ITEMS.each_with_index do |item, index|
58
+ break if row >= metrics[:max_row]
59
+
60
+ action = item.action
61
+ is_selected = cursor == index
62
+ case action
63
+ when :toggle_view_mode
64
+ row = render_button_group(surface, bounds, item, row, indent, is_selected,
65
+ current_view_mode, view_mode_buttons)
66
+ insert_toggle_gap = false
67
+ when :cycle_line_spacing
68
+ row = render_button_group(surface, bounds, item, row, indent, is_selected,
69
+ current_line_spacing, line_spacing_buttons)
70
+ insert_toggle_gap = false
71
+ when :toggle_page_numbering_mode
72
+ row = render_button_group(surface, bounds, item, row, indent, is_selected,
73
+ current_page_numbering_mode, page_numbering_buttons)
74
+ insert_toggle_gap = true
75
+ else
76
+ if toggled_action?(action) && insert_toggle_gap
77
+ row += 1
78
+ insert_toggle_gap = false
79
+ end
80
+ value_text, value_color = text_values[action]
81
+ ctx = ItemCtx.new(row: row, item: item, value_text: value_text, value_color: value_color,
82
+ index: index, selected: is_selected, indent: indent)
83
+ row = render_text_item(surface, bounds, ctx)
84
+ end
85
+ end
86
+ end
87
+
88
+ def render_text_item(surface, bounds, ctx)
89
+ text = formatted_row(ctx.item, ctx.value_text, ctx.value_color, ctx.selected)
90
+ row = ctx.row
91
+ surface.write(bounds, row, ctx.indent, text)
92
+ row + 2
93
+ end
94
+
95
+ def formatted_row(item, value_text, value_color, selected)
96
+ label = label_text(item)
97
+ colors = row_colors(selected)
98
+ line = "#{colors[:prefix]}#{colors[:fg]}#{label}"
99
+ line = "#{line}#{Terminal::ANSI::RESET} #{value_color}#{value_text}" if value_text && !value_text.to_s.empty?
100
+ "#{line}#{Terminal::ANSI::RESET}"
101
+ end
102
+
103
+ def row_colors(selected)
104
+ if selected
105
+ { prefix: Terminal::ANSI::BOLD, fg: COLOR_TEXT_ACCENT }
106
+ else
107
+ { prefix: '', fg: COLOR_TEXT_PRIMARY }
108
+ end
109
+ end
110
+
111
+ def label_text(item)
112
+ "#{item.icon} #{item.label}"
113
+ end
114
+
115
+ def layout_metrics(bounds, text_values)
116
+ height = bounds.height
117
+ width = bounds.width
118
+ label_width = SETTINGS_ITEMS.map { |item| display_width(label_text(item)) }.max || 0
119
+ text_value_width = text_values.values.map { |value| display_width(Array(value).first) }.max || 0
120
+ button_width = [
121
+ button_group_width(view_mode_buttons),
122
+ button_group_width(line_spacing_buttons),
123
+ button_group_width(page_numbering_buttons),
124
+ ].max || 0
125
+ content_width = label_width + 2 + [text_value_width, button_width].max
126
+ available = width - content_width
127
+ indent = (available / 2).floor
128
+ indent = indent.clamp(2, [available, 0].max)
129
+ {
130
+ indent: indent,
131
+ start_row: [(height - 16) / 2, 4].max,
132
+ max_row: height - 3,
133
+ }
134
+ end
135
+
136
+ def display_width(text)
137
+ Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(text.to_s)
138
+ end
139
+
140
+ def setting_value_map
141
+ {
142
+ back_to_menu: ['Return to main menu', COLOR_TEXT_DIM],
143
+ toggle_page_numbers: toggle_page_number_value,
144
+ toggle_highlight_quotes: toggle_highlight_value,
145
+ toggle_kitty_images: toggle_kitty_images_value,
146
+ wipe_cache: ['Removes EPUB + scan caches', COLOR_TEXT_WARNING],
147
+ }
148
+ end
149
+
150
+ def render_button_group(surface, bounds, item, row, indent, selected, current_value, buttons)
151
+ colors = row_colors(selected)
152
+ label = "#{colors[:prefix]}#{colors[:fg]}#{label_text(item)}#{Terminal::ANSI::RESET}"
153
+ surface.write(bounds, row, indent, label)
154
+ buttons_line = button_row(buttons, current_value)
155
+ next_row = row + 1
156
+ surface.write(bounds, next_row, indent, buttons_line) if next_row < bounds.height
157
+ row + 3
158
+ end
159
+
160
+ def button_row(buttons, current_value)
161
+ buttons.map { |value, label| button_string(label, value == current_value) }.join(' ')
162
+ end
163
+
164
+ def button_string(label, active)
165
+ bg = active ? BUTTON_BG_ACTIVE : BUTTON_BG_INACTIVE
166
+ fg = active ? BUTTON_FG_ACTIVE : BUTTON_FG_INACTIVE
167
+ "#{bg}#{fg} #{label} #{Terminal::ANSI::RESET}"
168
+ end
169
+
170
+ def button_group_width(buttons)
171
+ buttons.sum do |_value, label|
172
+ Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(label) + 2
173
+ end + (buttons.length - 1)
174
+ end
175
+
176
+ def toggled_action?(action)
177
+ %i[toggle_page_numbers toggle_highlight_quotes toggle_kitty_images].include?(action)
178
+ end
179
+
180
+ def view_mode_buttons
181
+ [[:split, 'Split'], [:single, 'Single']]
182
+ end
183
+
184
+ def line_spacing_buttons
185
+ [[:normal, 'Normal'], [:relaxed, 'Relaxed'], [:compact, 'Compact']]
186
+ end
187
+
188
+ def page_numbering_buttons
189
+ [[:absolute, 'Absolute'], [:dynamic, 'Dynamic']]
190
+ end
191
+
192
+ def current_view_mode
193
+ @state.get(%i[config view_mode]) || :split
194
+ end
195
+
196
+ def current_line_spacing
197
+ @state.get(%i[config line_spacing]) || :compact
198
+ end
199
+
200
+ def current_page_numbering_mode
201
+ @state.get(%i[config page_numbering_mode]) || :dynamic
202
+ end
203
+
204
+ def toggle_page_number_value
205
+ text = format_page_numbers
206
+ color = text == 'Enabled' ? COLOR_TEXT_SUCCESS : COLOR_TEXT_WARNING
207
+ [text, color]
208
+ end
209
+
210
+ def toggle_highlight_value
211
+ text = format_highlight_quotes
212
+ color = text == 'On' ? COLOR_TEXT_SUCCESS : COLOR_TEXT_WARNING
213
+ [text, color]
214
+ end
215
+
216
+ def format_page_numbers
217
+ @state.get(%i[config show_page_numbers]) ? 'Enabled' : 'Disabled'
218
+ end
219
+
220
+ def format_highlight_quotes
221
+ value = @state.get(%i[config highlight_quotes])
222
+ if value.nil? || !!value
223
+ 'On'
224
+ else
225
+ 'Off'
226
+ end
227
+ end
228
+
229
+ def toggle_kitty_images_value
230
+ enabled = !@state.get(%i[config kitty_images]).nil?
231
+ text = enabled ? 'Enabled' : 'Disabled'
232
+ color = enabled ? COLOR_TEXT_SUCCESS : COLOR_TEXT_DIM
233
+ [text, color]
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end