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.
- checksums.yaml +7 -0
- data/.bundle/config +4 -0
- data/.bundle/config.bak +3 -0
- data/.rspec_status +42 -0
- data/.rubocop.yml +124 -0
- data/Gemfile +19 -0
- data/LICENSE +21 -0
- data/README.md +82 -0
- data/Rakefile +29 -0
- data/bin/start +15 -0
- data/lib/shoko/adapters/book_sources/document_service.rb +201 -0
- data/lib/shoko/adapters/book_sources/download_service.rb +95 -0
- data/lib/shoko/adapters/book_sources/epub/epub_resource_loader.rb +137 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/html_processor.rb +151 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/metadata_extractor.rb +53 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/entry_reader.rb +77 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/metadata_extractor.rb +67 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_context.rb +86 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_document_index.rb +75 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_document_scanner.rb +47 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_extractor.rb +46 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_label_resolver.rb +83 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_list_item.rb +55 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_result.rb +8 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_selector.rb +100 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_source_locator.rb +93 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_traversal.rb +103 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_walker.rb +56 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf_processor.rb +102 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/xhtml_content_parser.rb +661 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/xml_text_normalizer.rb +41 -0
- data/lib/shoko/adapters/book_sources/epub_document.rb +253 -0
- data/lib/shoko/adapters/book_sources/epub_finder/directory_scanner.rb +134 -0
- data/lib/shoko/adapters/book_sources/epub_finder/scanner_context.rb +28 -0
- data/lib/shoko/adapters/book_sources/epub_finder.rb +161 -0
- data/lib/shoko/adapters/book_sources/epub_importer.rb +268 -0
- data/lib/shoko/adapters/book_sources/gutendex_client.rb +150 -0
- data/lib/shoko/adapters/book_sources/library_scanner.rb +93 -0
- data/lib/shoko/adapters/book_sources/source_fingerprint.rb +57 -0
- data/lib/shoko/adapters/input/annotations/mouse_handler.rb +84 -0
- data/lib/shoko/adapters/input/command_bridge.rb +148 -0
- data/lib/shoko/adapters/input/command_factory.rb +255 -0
- data/lib/shoko/adapters/input/commands.rb +60 -0
- data/lib/shoko/adapters/input/dispatcher.rb +69 -0
- data/lib/shoko/adapters/input/input_controller.rb +250 -0
- data/lib/shoko/adapters/input/key_definitions.rb +108 -0
- data/lib/shoko/adapters/input/validators/file_path_validator.rb +81 -0
- data/lib/shoko/adapters/input/validators/terminal_size_validator.rb +76 -0
- data/lib/shoko/adapters/monitoring/logger.rb +150 -0
- data/lib/shoko/adapters/monitoring/perf_tracer.rb +183 -0
- data/lib/shoko/adapters/monitoring/performance_monitor.rb +110 -0
- data/lib/shoko/adapters/output/clipboard/clipboard_service.rb +125 -0
- data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/image_builder.rb +149 -0
- data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/text_wrapper.rb +149 -0
- data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/tokenizer.rb +91 -0
- data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler.rb +144 -0
- data/lib/shoko/adapters/output/formatting/formatting_service/plain_lines_builder.rb +54 -0
- data/lib/shoko/adapters/output/formatting/formatting_service.rb +247 -0
- data/lib/shoko/adapters/output/formatting/wrapping_service.rb +228 -0
- data/lib/shoko/adapters/output/instrumentation_service.rb +52 -0
- data/lib/shoko/adapters/output/kitty/image_transcoder.rb +71 -0
- data/lib/shoko/adapters/output/kitty/kitty_graphics.rb +114 -0
- data/lib/shoko/adapters/output/kitty/kitty_image_renderer.rb +239 -0
- data/lib/shoko/adapters/output/kitty/kitty_unicode_placeholders.rb +139 -0
- data/lib/shoko/adapters/output/kitty/kitty_unicode_placeholders_diacritic_codepoints.txt +26 -0
- data/lib/shoko/adapters/output/notification_service.rb +58 -0
- data/lib/shoko/adapters/output/render_registry.rb +45 -0
- data/lib/shoko/adapters/output/rendering/models/line_geometry.rb +60 -0
- data/lib/shoko/adapters/output/rendering/models/page_rendering_context.rb +22 -0
- data/lib/shoko/adapters/output/rendering/models/render_params.rb +28 -0
- data/lib/shoko/adapters/output/rendering/models/rendering_context.rb +58 -0
- data/lib/shoko/adapters/output/terminal/buffer.rb +275 -0
- data/lib/shoko/adapters/output/terminal/constants/terminal_defaults.rb +11 -0
- data/lib/shoko/adapters/output/terminal/input/decoder.rb +347 -0
- data/lib/shoko/adapters/output/terminal/input.rb +161 -0
- data/lib/shoko/adapters/output/terminal/output.rb +105 -0
- data/lib/shoko/adapters/output/terminal/terminal.rb +167 -0
- data/lib/shoko/adapters/output/terminal/terminal_sanitizer.rb +243 -0
- data/lib/shoko/adapters/output/terminal/terminal_service.rb +138 -0
- data/lib/shoko/adapters/output/terminal/text_metrics.rb +273 -0
- data/lib/shoko/adapters/output/ui/builders/page_setup_builder.rb +47 -0
- data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay/footer_renderer.rb +80 -0
- data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay/geometry.rb +61 -0
- data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay/note_renderer.rb +86 -0
- data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay_component.rb +234 -0
- data/lib/shoko/adapters/output/ui/components/annotations_overlay/list_renderer.rb +142 -0
- data/lib/shoko/adapters/output/ui/components/annotations_overlay_component.rb +185 -0
- data/lib/shoko/adapters/output/ui/components/base_component.rb +110 -0
- data/lib/shoko/adapters/output/ui/components/component_interface.rb +80 -0
- data/lib/shoko/adapters/output/ui/components/content_component.rb +61 -0
- data/lib/shoko/adapters/output/ui/components/enhanced_popup_menu.rb +191 -0
- data/lib/shoko/adapters/output/ui/components/footer_component.rb +120 -0
- data/lib/shoko/adapters/output/ui/components/header_component.rb +46 -0
- data/lib/shoko/adapters/output/ui/components/layouts/horizontal.rb +63 -0
- data/lib/shoko/adapters/output/ui/components/layouts/vertical.rb +73 -0
- data/lib/shoko/adapters/output/ui/components/main_menu_component.rb +103 -0
- data/lib/shoko/adapters/output/ui/components/reading/base_view_renderer.rb +199 -0
- data/lib/shoko/adapters/output/ui/components/reading/config_helpers.rb +42 -0
- data/lib/shoko/adapters/output/ui/components/reading/help_renderer.rb +62 -0
- data/lib/shoko/adapters/output/ui/components/reading/inline_segment_highlighter.rb +144 -0
- data/lib/shoko/adapters/output/ui/components/reading/kitty_image_line_renderer.rb +262 -0
- data/lib/shoko/adapters/output/ui/components/reading/line_content_composer.rb +114 -0
- data/lib/shoko/adapters/output/ui/components/reading/line_drawer.rb +87 -0
- data/lib/shoko/adapters/output/ui/components/reading/line_geometry_builder.rb +41 -0
- data/lib/shoko/adapters/output/ui/components/reading/rendered_lines_recorder.rb +64 -0
- data/lib/shoko/adapters/output/ui/components/reading/single_view_renderer.rb +156 -0
- data/lib/shoko/adapters/output/ui/components/reading/split_view_renderer.rb +221 -0
- data/lib/shoko/adapters/output/ui/components/reading/view_renderer_factory.rb +20 -0
- data/lib/shoko/adapters/output/ui/components/reading/wrapped_lines_fetcher.rb +139 -0
- data/lib/shoko/adapters/output/ui/components/rect.rb +15 -0
- data/lib/shoko/adapters/output/ui/components/render_style.rb +84 -0
- data/lib/shoko/adapters/output/ui/components/screen_component.rb +24 -0
- data/lib/shoko/adapters/output/ui/components/screens/annotation_detail_screen_component.rb +175 -0
- data/lib/shoko/adapters/output/ui/components/screens/annotation_edit_screen_component.rb +221 -0
- data/lib/shoko/adapters/output/ui/components/screens/annotation_editor_screen_component.rb +205 -0
- data/lib/shoko/adapters/output/ui/components/screens/annotation_rendering_helpers.rb +190 -0
- data/lib/shoko/adapters/output/ui/components/screens/annotations_screen_component.rb +266 -0
- data/lib/shoko/adapters/output/ui/components/screens/base_screen_component.rb +49 -0
- data/lib/shoko/adapters/output/ui/components/screens/browse_screen_component.rb +319 -0
- data/lib/shoko/adapters/output/ui/components/screens/download_books_screen_component.rb +340 -0
- data/lib/shoko/adapters/output/ui/components/screens/library_screen_component.rb +205 -0
- data/lib/shoko/adapters/output/ui/components/screens/loading_overlay_component.rb +49 -0
- data/lib/shoko/adapters/output/ui/components/screens/menu_screen_component.rb +107 -0
- data/lib/shoko/adapters/output/ui/components/screens/settings_screen_component.rb +238 -0
- data/lib/shoko/adapters/output/ui/components/sidebar/annotations_tab_renderer.rb +159 -0
- data/lib/shoko/adapters/output/ui/components/sidebar/bookmarks_tab_renderer.rb +139 -0
- data/lib/shoko/adapters/output/ui/components/sidebar/tab_header_component.rb +157 -0
- data/lib/shoko/adapters/output/ui/components/sidebar/toc_tab_renderer.rb +111 -0
- data/lib/shoko/adapters/output/ui/components/sidebar/toc_tab_support.rb +1606 -0
- data/lib/shoko/adapters/output/ui/components/sidebar_panel_component.rb +217 -0
- data/lib/shoko/adapters/output/ui/components/surface.rb +88 -0
- data/lib/shoko/adapters/output/ui/components/tooltip_overlay_component.rb +224 -0
- data/lib/shoko/adapters/output/ui/components/ui/box_drawer.rb +32 -0
- data/lib/shoko/adapters/output/ui/components/ui/list_helpers.rb +33 -0
- data/lib/shoko/adapters/output/ui/components/ui/overlay_layout.rb +79 -0
- data/lib/shoko/adapters/output/ui/components/ui/text_utils.rb +46 -0
- data/lib/shoko/adapters/output/ui/constants/highlighting.rb +21 -0
- data/lib/shoko/adapters/output/ui/constants/messages.rb +12 -0
- data/lib/shoko/adapters/output/ui/constants/themes.rb +79 -0
- data/lib/shoko/adapters/output/ui/constants/ui_constants.rb +85 -0
- data/lib/shoko/adapters/output/ui/rendering/frame_coordinator.rb +42 -0
- data/lib/shoko/adapters/output/ui/rendering/reader_render_coordinator.rb +169 -0
- data/lib/shoko/adapters/output/ui/rendering/render_pipeline.rb +55 -0
- data/lib/shoko/adapters/storage/atomic_file_writer.rb +43 -0
- data/lib/shoko/adapters/storage/background_worker.rb +66 -0
- data/lib/shoko/adapters/storage/book_cache_pipeline.rb +653 -0
- data/lib/shoko/adapters/storage/cache/epub/memory_cache.rb +99 -0
- data/lib/shoko/adapters/storage/cache/epub/persistence.rb +131 -0
- data/lib/shoko/adapters/storage/cache/epub/serializer/deserialize.rb +225 -0
- data/lib/shoko/adapters/storage/cache/epub/serializer/helpers.rb +63 -0
- data/lib/shoko/adapters/storage/cache/epub/serializer/serialize.rb +83 -0
- data/lib/shoko/adapters/storage/cache/epub/serializer.rb +5 -0
- data/lib/shoko/adapters/storage/cache/epub/source_reference.rb +58 -0
- data/lib/shoko/adapters/storage/cache_paths.rb +21 -0
- data/lib/shoko/adapters/storage/cache_pointer_manager.rb +60 -0
- data/lib/shoko/adapters/storage/config_paths.rb +30 -0
- data/lib/shoko/adapters/storage/epub_cache.rb +195 -0
- data/lib/shoko/adapters/storage/file_writer_service.rb +47 -0
- data/lib/shoko/adapters/storage/json_cache_store/chapters.rb +141 -0
- data/lib/shoko/adapters/storage/json_cache_store/layouts.rb +67 -0
- data/lib/shoko/adapters/storage/json_cache_store/manifest.rb +42 -0
- data/lib/shoko/adapters/storage/json_cache_store/payload_helpers.rb +113 -0
- data/lib/shoko/adapters/storage/json_cache_store/resources.rb +84 -0
- data/lib/shoko/adapters/storage/json_cache_store.rb +167 -0
- data/lib/shoko/adapters/storage/lazy_file_string.rb +65 -0
- data/lib/shoko/adapters/storage/pagination_cache.rb +127 -0
- data/lib/shoko/adapters/storage/recent_files.rb +78 -0
- data/lib/shoko/adapters/storage/repositories/annotation_repository.rb +182 -0
- data/lib/shoko/adapters/storage/repositories/base_repository.rb +81 -0
- data/lib/shoko/adapters/storage/repositories/bookmark_repository.rb +132 -0
- data/lib/shoko/adapters/storage/repositories/cached_library_repository.rb +129 -0
- data/lib/shoko/adapters/storage/repositories/config_repository.rb +262 -0
- data/lib/shoko/adapters/storage/repositories/progress_repository.rb +166 -0
- data/lib/shoko/adapters/storage/repositories/storage/annotation_file_store.rb +128 -0
- data/lib/shoko/adapters/storage/repositories/storage/bookmark_file_store.rb +109 -0
- data/lib/shoko/adapters/storage/repositories/storage/file_store_utils.rb +20 -0
- data/lib/shoko/adapters/storage/repositories/storage/progress_file_store.rb +59 -0
- data/lib/shoko/application/annotation_editor_overlay_session.rb +138 -0
- data/lib/shoko/application/cli.rb +134 -0
- data/lib/shoko/application/controllers/menu/input_controller.rb +189 -0
- data/lib/shoko/application/controllers/menu/state_controller.rb +642 -0
- data/lib/shoko/application/controllers/menu_controller.rb +469 -0
- data/lib/shoko/application/controllers/mouseable_reader.rb +377 -0
- data/lib/shoko/application/controllers/reader_controller.rb +449 -0
- data/lib/shoko/application/controllers/state_controller.rb +410 -0
- data/lib/shoko/application/controllers/ui_controller.rb +782 -0
- data/lib/shoko/application/dependency_container.rb +301 -0
- data/lib/shoko/application/infrastructure/event_bus.rb +80 -0
- data/lib/shoko/application/infrastructure/observer_state_store.rb +136 -0
- data/lib/shoko/application/infrastructure/state_store.rb +413 -0
- data/lib/shoko/application/main_menu/menu_progress_presenter.rb +83 -0
- data/lib/shoko/application/pending_jump_handler.rb +122 -0
- data/lib/shoko/application/reader_lifecycle.rb +65 -0
- data/lib/shoko/application/reader_startup_orchestrator.rb +113 -0
- data/lib/shoko/application/selectors/config_selectors.rb +62 -0
- data/lib/shoko/application/selectors/menu_selectors.rb +62 -0
- data/lib/shoko/application/selectors/reader_selectors.rb +186 -0
- data/lib/shoko/application/state/actions/base_action.rb +24 -0
- data/lib/shoko/application/state/actions/quit_to_menu_action.rb +16 -0
- data/lib/shoko/application/state/actions/switch_reader_mode_action.rb +22 -0
- data/lib/shoko/application/state/actions/toggle_view_mode_action.rb +31 -0
- data/lib/shoko/application/state/actions/update_annotation_editor_overlay_action.rb +27 -0
- data/lib/shoko/application/state/actions/update_annotations_action.rb +20 -0
- data/lib/shoko/application/state/actions/update_annotations_overlay_action.rb +27 -0
- data/lib/shoko/application/state/actions/update_bookmarks_action.rb +20 -0
- data/lib/shoko/application/state/actions/update_chapter_action.rb +24 -0
- data/lib/shoko/application/state/actions/update_config_action.rb +22 -0
- data/lib/shoko/application/state/actions/update_field_helpers.rb +26 -0
- data/lib/shoko/application/state/actions/update_menu_action.rb +21 -0
- data/lib/shoko/application/state/actions/update_message_action.rb +35 -0
- data/lib/shoko/application/state/actions/update_page_action.rb +21 -0
- data/lib/shoko/application/state/actions/update_pagination_state_action.rb +21 -0
- data/lib/shoko/application/state/actions/update_popup_menu_action.rb +27 -0
- data/lib/shoko/application/state/actions/update_reader_meta_action.rb +21 -0
- data/lib/shoko/application/state/actions/update_reader_mode_action.rb +20 -0
- data/lib/shoko/application/state/actions/update_rendered_lines_action.rb +40 -0
- data/lib/shoko/application/state/actions/update_selection_action.rb +27 -0
- data/lib/shoko/application/state/actions/update_selections_action.rb +21 -0
- data/lib/shoko/application/state/actions/update_sidebar_action.rb +34 -0
- data/lib/shoko/application/state/actions/update_ui_loading_action.rb +23 -0
- data/lib/shoko/application/ui/reader_view_model_builder.rb +74 -0
- data/lib/shoko/application/ui/view_models/reader_view_model.rb +177 -0
- data/lib/shoko/application/unified_application.rb +48 -0
- data/lib/shoko/application/use_cases/catalog_service.rb +117 -0
- data/lib/shoko/application/use_cases/commands/annotation_editor_commands.rb +105 -0
- data/lib/shoko/application/use_cases/commands/application_commands.rb +208 -0
- data/lib/shoko/application/use_cases/commands/base_command.rb +166 -0
- data/lib/shoko/application/use_cases/commands/bookmark_commands.rb +114 -0
- data/lib/shoko/application/use_cases/commands/conditional_navigation_commands.rb +57 -0
- data/lib/shoko/application/use_cases/commands/menu_commands.rb +170 -0
- data/lib/shoko/application/use_cases/commands/navigation_commands.rb +183 -0
- data/lib/shoko/application/use_cases/commands/reader_commands.rb +46 -0
- data/lib/shoko/application/use_cases/commands/sidebar_commands.rb +55 -0
- data/lib/shoko/application/use_cases/settings_service.rb +123 -0
- data/lib/shoko/core/events/annotation_events.rb +94 -0
- data/lib/shoko/core/events/base_domain_event.rb +169 -0
- data/lib/shoko/core/events/bookmark_events.rb +41 -0
- data/lib/shoko/core/events/domain_event_bus.rb +163 -0
- data/lib/shoko/core/events/progress_events.rb +108 -0
- data/lib/shoko/core/models/bookmark.rb +36 -0
- data/lib/shoko/core/models/bookmark_data.rb +10 -0
- data/lib/shoko/core/models/chapter.rb +25 -0
- data/lib/shoko/core/models/content_block.rb +44 -0
- data/lib/shoko/core/models/reader_settings.rb +20 -0
- data/lib/shoko/core/models/selection_anchor.rb +73 -0
- data/lib/shoko/core/models/toc_entry.rb +14 -0
- data/lib/shoko/core/ports/annotation_repository.rb +0 -0
- data/lib/shoko/core/ports/book_repository.rb +0 -0
- data/lib/shoko/core/ports/book_source.rb +0 -0
- data/lib/shoko/core/ports/bookmark_repository.rb +0 -0
- data/lib/shoko/core/ports/cache.rb +0 -0
- data/lib/shoko/core/ports/input_handler.rb +0 -0
- data/lib/shoko/core/ports/renderer.rb +0 -0
- data/lib/shoko/core/ports/storage.rb +0 -0
- data/lib/shoko/core/services/annotation_service.rb +102 -0
- data/lib/shoko/core/services/base_service.rb +60 -0
- data/lib/shoko/core/services/bookmark_service.rb +267 -0
- data/lib/shoko/core/services/coordinate_service.rb +265 -0
- data/lib/shoko/core/services/layout_service.rb +95 -0
- data/lib/shoko/core/services/navigation/absolute_change_applier.rb +96 -0
- data/lib/shoko/core/services/navigation/absolute_layout.rb +101 -0
- data/lib/shoko/core/services/navigation/absolute_strategy.rb +179 -0
- data/lib/shoko/core/services/navigation/context_builder.rb +52 -0
- data/lib/shoko/core/services/navigation/context_helpers.rb +63 -0
- data/lib/shoko/core/services/navigation/dynamic_change_applier.rb +50 -0
- data/lib/shoko/core/services/navigation/dynamic_strategy.rb +51 -0
- data/lib/shoko/core/services/navigation/image_offset_snapper.rb +150 -0
- data/lib/shoko/core/services/navigation/nav_context.rb +27 -0
- data/lib/shoko/core/services/navigation/state_updater.rb +29 -0
- data/lib/shoko/core/services/navigation/strategy_factory.rb +20 -0
- data/lib/shoko/core/services/navigation_service.rb +150 -0
- data/lib/shoko/core/services/page_calculator_service.rb +242 -0
- data/lib/shoko/core/services/pagination/internal/absolute_page_map_builder.rb +28 -0
- data/lib/shoko/core/services/pagination/internal/chapter_cache.rb +60 -0
- data/lib/shoko/core/services/pagination/internal/dynamic_page_map_builder.rb +157 -0
- data/lib/shoko/core/services/pagination/internal/layout_metrics_calculator.rb +73 -0
- data/lib/shoko/core/services/pagination/internal/page_hydrator.rb +145 -0
- data/lib/shoko/core/services/pagination/internal/pagination_workflow.rb +152 -0
- data/lib/shoko/core/services/pagination/page_info_calculator.rb +247 -0
- data/lib/shoko/core/services/pagination/pagination_cache_preloader.rb +173 -0
- data/lib/shoko/core/services/pagination/pagination_coordinator.rb +202 -0
- data/lib/shoko/core/services/pagination/pagination_orchestrator.rb +291 -0
- data/lib/shoko/core/services/pagination.rb +10 -0
- data/lib/shoko/core/services/progress_helper.rb +22 -0
- data/lib/shoko/core/services/selection_service.rb +126 -0
- data/lib/shoko/core/validator.rb +76 -0
- data/lib/shoko/shared/errors.rb +97 -0
- data/lib/shoko/shared/version.rb +5 -0
- data/lib/shoko/test_support/terminal_double.rb +175 -0
- data/lib/shoko/test_support/test_mode.rb +78 -0
- data/lib/shoko.rb +279 -0
- data/lib/zip.rb +732 -0
- data/zip.rb +5 -0
- metadata +370 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shoko
|
|
4
|
+
module Adapters
|
|
5
|
+
module Input
|
|
6
|
+
# Handles all input processing: key handling, popup management, mode switching
|
|
7
|
+
class InputController
|
|
8
|
+
def initialize(state, dependencies)
|
|
9
|
+
@state = state
|
|
10
|
+
@dependencies = dependencies
|
|
11
|
+
@dispatcher = nil
|
|
12
|
+
@modal_mode_stack = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def setup_input_dispatcher(reader_controller)
|
|
16
|
+
@dispatcher = Adapters::Input::Dispatcher.new(reader_controller)
|
|
17
|
+
setup_consolidated_reader_bindings(reader_controller)
|
|
18
|
+
@dispatcher.activate_stack([:read])
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def handle_key(key)
|
|
22
|
+
@dispatcher&.handle_key(key)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Enhanced popup navigation handlers for direct key routing
|
|
26
|
+
def handle_popup_navigation(key)
|
|
27
|
+
with_popup_menu do |menu|
|
|
28
|
+
res = menu.handle_key(key)
|
|
29
|
+
next :pass unless res
|
|
30
|
+
|
|
31
|
+
:handled
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def handle_popup_action_key(key)
|
|
36
|
+
with_popup_menu do |menu|
|
|
37
|
+
res = menu.handle_key(key) || { type: :noop }
|
|
38
|
+
process_popup_result(res)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def handle_popup_cancel(key)
|
|
43
|
+
with_popup_menu do |menu|
|
|
44
|
+
res = menu.handle_key(key) || { type: :noop }
|
|
45
|
+
process_popup_result(res)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def handle_popup_menu_input(keys)
|
|
50
|
+
popup_menu = Shoko::Application::Selectors::ReaderSelectors.popup_menu(@state)
|
|
51
|
+
return unless popup_menu
|
|
52
|
+
|
|
53
|
+
ui_controller = @dependencies.resolve(:ui_controller)
|
|
54
|
+
keys.each do |key|
|
|
55
|
+
res = popup_menu.handle_key(key) || { type: :noop }
|
|
56
|
+
process_popup_result(res, ui_controller)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def handle_annotations_overlay_input(keys)
|
|
61
|
+
overlay = Shoko::Application::Selectors::ReaderSelectors.annotations_overlay(@state)
|
|
62
|
+
return unless overlay
|
|
63
|
+
|
|
64
|
+
ui_controller = @dependencies.resolve(:ui_controller)
|
|
65
|
+
keys.each do |key|
|
|
66
|
+
result = overlay.handle_key(key)
|
|
67
|
+
next unless result
|
|
68
|
+
|
|
69
|
+
case result[:type]
|
|
70
|
+
when :selection_change
|
|
71
|
+
index = result[:index]
|
|
72
|
+
@state.dispatch(Shoko::Application::Actions::UpdateSidebarAction.new(
|
|
73
|
+
annotations_selected: index,
|
|
74
|
+
sidebar_annotations_selected: index
|
|
75
|
+
))
|
|
76
|
+
when :open
|
|
77
|
+
if ui_controller.respond_to?(:open_annotation_from_overlay)
|
|
78
|
+
ui_controller.open_annotation_from_overlay(result[:annotation])
|
|
79
|
+
end
|
|
80
|
+
when :edit
|
|
81
|
+
if ui_controller.respond_to?(:edit_annotation_from_overlay)
|
|
82
|
+
ui_controller.edit_annotation_from_overlay(result[:annotation])
|
|
83
|
+
end
|
|
84
|
+
when :delete
|
|
85
|
+
if ui_controller.respond_to?(:delete_annotation_from_overlay)
|
|
86
|
+
ui_controller.delete_annotation_from_overlay(result[:annotation])
|
|
87
|
+
end
|
|
88
|
+
when :close
|
|
89
|
+
ui_controller.close_annotations_overlay if ui_controller.respond_to?(:close_annotations_overlay)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def with_popup_menu
|
|
97
|
+
popup_menu = Shoko::Application::Selectors::ReaderSelectors.popup_menu(@state)
|
|
98
|
+
return :pass unless popup_menu
|
|
99
|
+
|
|
100
|
+
yield popup_menu
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def process_popup_result(result, ui_controller = @dependencies.resolve(:ui_controller))
|
|
104
|
+
case result[:type]
|
|
105
|
+
when :selection_change
|
|
106
|
+
# Selection change handled by popup itself
|
|
107
|
+
:handled
|
|
108
|
+
when :action
|
|
109
|
+
ui_controller.handle_popup_action(result)
|
|
110
|
+
:handled
|
|
111
|
+
when :cancel
|
|
112
|
+
ui_controller.cleanup_popup_state
|
|
113
|
+
ui_controller.switch_mode(:read)
|
|
114
|
+
:handled
|
|
115
|
+
else
|
|
116
|
+
:pass
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def setup_consolidated_reader_bindings(reader_controller)
|
|
121
|
+
# Register reader mode bindings using Adapters::Input::CommandFactory patterns
|
|
122
|
+
register_read_bindings(reader_controller)
|
|
123
|
+
register_popup_menu_bindings(reader_controller)
|
|
124
|
+
|
|
125
|
+
# Keep legacy bindings for modes not yet converted
|
|
126
|
+
register_help_bindings_new(reader_controller)
|
|
127
|
+
register_annotation_editor_bindings_new(reader_controller)
|
|
128
|
+
register_library_bindings_new(reader_controller)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def register_read_bindings(_reader_controller)
|
|
132
|
+
bindings = Adapters::Input::CommandFactory.reader_navigation_commands
|
|
133
|
+
bindings.merge!(Adapters::Input::CommandFactory.reader_control_commands)
|
|
134
|
+
|
|
135
|
+
# When sidebar is visible, redirect up/down/enter to sidebar handlers
|
|
136
|
+
nav_down = Adapters::Input::KeyDefinitions::NAVIGATION[:down]
|
|
137
|
+
nav_down.each do |key|
|
|
138
|
+
bindings[key] = :conditional_down
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
nav_up = Adapters::Input::KeyDefinitions::NAVIGATION[:up]
|
|
142
|
+
nav_up.each do |key|
|
|
143
|
+
bindings[key] = :conditional_up
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
confirm_keys = Adapters::Input::KeyDefinitions::ACTIONS[:confirm]
|
|
147
|
+
confirm_keys.each do |key|
|
|
148
|
+
bindings[key] = :conditional_select
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Ensure TOC toggle is bound explicitly and marked handled
|
|
152
|
+
%w[t T].each do |key|
|
|
153
|
+
bindings[key] = :open_toc
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
@dispatcher.register_mode(:read, bindings)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def register_popup_menu_bindings(_reader_controller)
|
|
160
|
+
# Popup menu navigation is now handled directly in main_loop via handle_popup_menu_input
|
|
161
|
+
bindings = {}
|
|
162
|
+
bindings.merge!(Adapters::Input::CommandFactory.menu_selection_commands)
|
|
163
|
+
bindings.merge!(Adapters::Input::CommandFactory.exit_commands(:exit_popup_menu))
|
|
164
|
+
@dispatcher.register_mode(:popup_menu, bindings)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def register_help_bindings_new(_reader_controller)
|
|
168
|
+
bindings = { __default__: :exit_help }
|
|
169
|
+
@dispatcher.register_mode(:help, bindings)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def register_library_bindings_new(_reader_controller)
|
|
173
|
+
# Keys are registered in MainMenu#register_library_bindings; this hook ensures mode exists
|
|
174
|
+
# No-op here as dispatcher registration happens in MainMenu.
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def register_annotation_editor_bindings_new(_reader_controller)
|
|
178
|
+
bindings = {}
|
|
179
|
+
|
|
180
|
+
cancel_cmd = Shoko::Application::Commands::AnnotationEditorCommandFactory.cancel
|
|
181
|
+
save_cmd = Shoko::Application::Commands::AnnotationEditorCommandFactory.save
|
|
182
|
+
back_cmd = Shoko::Application::Commands::AnnotationEditorCommandFactory.backspace
|
|
183
|
+
enter_cmd = Shoko::Application::Commands::AnnotationEditorCommandFactory.enter
|
|
184
|
+
insert_cmd = Shoko::Application::Commands::AnnotationEditorCommandFactory.insert_char
|
|
185
|
+
|
|
186
|
+
# Cancel editor
|
|
187
|
+
bindings["\e"] = cancel_cmd
|
|
188
|
+
|
|
189
|
+
# Save: Ctrl+S and 'S'
|
|
190
|
+
bindings["\x13"] = save_cmd
|
|
191
|
+
bindings['S'] = save_cmd
|
|
192
|
+
|
|
193
|
+
# Backspace (both variants)
|
|
194
|
+
bindings["\x7F"] = back_cmd
|
|
195
|
+
bindings["\b"] = back_cmd
|
|
196
|
+
|
|
197
|
+
# Enter (CR and LF)
|
|
198
|
+
confirm_keys = Adapters::Input::KeyDefinitions::ACTIONS[:confirm]
|
|
199
|
+
confirm_keys.each { |k| bindings[k] = enter_cmd }
|
|
200
|
+
|
|
201
|
+
# Default: insert printable characters
|
|
202
|
+
bindings[:__default__] = insert_cmd
|
|
203
|
+
|
|
204
|
+
@dispatcher.register_mode(:annotation_editor, bindings)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
public
|
|
208
|
+
|
|
209
|
+
# Switch active bindings according to mode
|
|
210
|
+
def activate_for_mode(mode)
|
|
211
|
+
return unless @dispatcher
|
|
212
|
+
|
|
213
|
+
@modal_mode_stack.clear
|
|
214
|
+
case mode
|
|
215
|
+
when :annotation_editor
|
|
216
|
+
@dispatcher.activate(:annotation_editor)
|
|
217
|
+
when :help
|
|
218
|
+
@dispatcher.activate(:help)
|
|
219
|
+
else
|
|
220
|
+
@dispatcher.activate_stack([:read])
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def enter_modal_mode(mode)
|
|
225
|
+
return unless @dispatcher
|
|
226
|
+
|
|
227
|
+
current_stack = @dispatcher.mode_stack
|
|
228
|
+
return if current_stack.last == mode
|
|
229
|
+
|
|
230
|
+
@modal_mode_stack << current_stack
|
|
231
|
+
new_stack = current_stack.empty? ? [mode] : current_stack + [mode]
|
|
232
|
+
@dispatcher.activate_stack(new_stack)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def exit_modal_mode(_mode)
|
|
236
|
+
return unless @dispatcher
|
|
237
|
+
|
|
238
|
+
previous_stack = @modal_mode_stack.pop
|
|
239
|
+
if previous_stack&.any?
|
|
240
|
+
@dispatcher.activate_stack(previous_stack)
|
|
241
|
+
else
|
|
242
|
+
activate_for_mode(@state.get(%i[reader mode]) || :read)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Removed reader annotations list bindings; annotations are managed via the sidebar
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shoko
|
|
4
|
+
module Adapters::Input
|
|
5
|
+
# Centralized key definitions to eliminate duplication and inconsistencies
|
|
6
|
+
# across the codebase. All input handling should reference these definitions.
|
|
7
|
+
module KeyDefinitions
|
|
8
|
+
# Navigation keys
|
|
9
|
+
NAVIGATION = {
|
|
10
|
+
up: ['k', "\e[A", "\eOA"].freeze,
|
|
11
|
+
down: ['j', "\e[B", "\eOB"].freeze,
|
|
12
|
+
left: ['h', "\e[D", "\eOD"].freeze,
|
|
13
|
+
right: ['l', "\e[C", "\eOC"].freeze,
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
# Action keys
|
|
17
|
+
ACTIONS = {
|
|
18
|
+
confirm: ["\r", "\n"].freeze,
|
|
19
|
+
cancel: ["\e", "\x1B"].freeze,
|
|
20
|
+
quit: ['q'].freeze,
|
|
21
|
+
force_quit: ['Q'].freeze,
|
|
22
|
+
space: [' '].freeze,
|
|
23
|
+
backspace: ['\b', "\x7F", "\x08"].freeze,
|
|
24
|
+
delete: ["\e[3~"].freeze,
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
# Reader-specific keys
|
|
28
|
+
READER = {
|
|
29
|
+
next_page: ['l', ' ', "\e[C", "\eOC"].freeze,
|
|
30
|
+
prev_page: ['h', "\e[D", "\eOD"].freeze,
|
|
31
|
+
scroll_down: ['j', "\e[B", "\eOB"].freeze,
|
|
32
|
+
scroll_up: ['k', "\e[A", "\eOA"].freeze,
|
|
33
|
+
next_chapter: %w[n N].freeze,
|
|
34
|
+
prev_chapter: ['p'].freeze,
|
|
35
|
+
go_to_start: ['g'].freeze,
|
|
36
|
+
go_to_end: ['G'].freeze,
|
|
37
|
+
toggle_view: %w[v V].freeze,
|
|
38
|
+
toggle_page_mode: ['P'].freeze,
|
|
39
|
+
increase_spacing: ['+'].freeze,
|
|
40
|
+
decrease_spacing: ['-'].freeze,
|
|
41
|
+
show_toc: %w[t T].freeze,
|
|
42
|
+
add_bookmark: ['b'].freeze,
|
|
43
|
+
show_bookmarks: ['B'].freeze,
|
|
44
|
+
show_annotations_tab: ['A'].freeze,
|
|
45
|
+
show_help: ['?'].freeze,
|
|
46
|
+
show_annotations: ["\u0001"].freeze,
|
|
47
|
+
rebuild_pagination: ['R'].freeze,
|
|
48
|
+
invalidate_pagination: ['I'].freeze,
|
|
49
|
+
}.freeze
|
|
50
|
+
|
|
51
|
+
# Menu navigation keys
|
|
52
|
+
MENU = {
|
|
53
|
+
browse: ['f'].freeze,
|
|
54
|
+
download_books: ['d'].freeze,
|
|
55
|
+
settings: ['s'].freeze,
|
|
56
|
+
search: ['S'].freeze,
|
|
57
|
+
refresh_scan: ['r'].freeze,
|
|
58
|
+
}.freeze
|
|
59
|
+
|
|
60
|
+
# Utility methods for checking key membership
|
|
61
|
+
module Helpers
|
|
62
|
+
def self.navigation_key?(key)
|
|
63
|
+
NAVIGATION.values.flatten.include?(key)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.up_key?(key)
|
|
67
|
+
NAVIGATION[:up].include?(key)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.down_key?(key)
|
|
71
|
+
NAVIGATION[:down].include?(key)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.confirm_key?(key)
|
|
75
|
+
ACTIONS[:confirm].include?(key)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.cancel_key?(key)
|
|
79
|
+
ACTIONS[:cancel].include?(key)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.quit_key?(key)
|
|
83
|
+
ACTIONS[:quit].include?(key)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def self.backspace_key?(key)
|
|
87
|
+
ACTIONS[:backspace].include?(key)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def self.escape_key?(key)
|
|
91
|
+
ACTIONS[:cancel].include?(key)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Check if key matches any keys in a definition
|
|
95
|
+
def self.matches_keys?(key, key_list)
|
|
96
|
+
key_list.include?(key)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Create binding hash from key list and command
|
|
100
|
+
def self.create_bindings(key_list, command)
|
|
101
|
+
key_list.each_with_object({}) do |k, bindings|
|
|
102
|
+
bindings[k] = command
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../../core/validator.rb'
|
|
4
|
+
require_relative '../../storage/epub_cache.rb'
|
|
5
|
+
|
|
6
|
+
module Shoko
|
|
7
|
+
module Adapters::Input::Validators
|
|
8
|
+
# Validates file paths for EPUB files.
|
|
9
|
+
# Ensures paths exist, are readable, and have correct extension.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# validator = FilePathValidator.new
|
|
13
|
+
# if validator.validate?("/path/to/book.epub")
|
|
14
|
+
# # Path is valid
|
|
15
|
+
# else
|
|
16
|
+
# puts validator.errors
|
|
17
|
+
# end
|
|
18
|
+
class FilePathValidator < Core::Validator
|
|
19
|
+
# Validate a file path
|
|
20
|
+
#
|
|
21
|
+
# @param path [String] File path to validate
|
|
22
|
+
# @return [Boolean] Validation result
|
|
23
|
+
def validate?(path)
|
|
24
|
+
clear_errors
|
|
25
|
+
|
|
26
|
+
presence_valid?(path, :path) &&
|
|
27
|
+
exists?(path) &&
|
|
28
|
+
readable?(path) &&
|
|
29
|
+
extension_valid?(path) &&
|
|
30
|
+
not_empty?(path)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# Check if file exists
|
|
36
|
+
#
|
|
37
|
+
# @param path [String] File path
|
|
38
|
+
# @return [Boolean] true if exists
|
|
39
|
+
def exists?(path)
|
|
40
|
+
return true if File.exist?(path)
|
|
41
|
+
|
|
42
|
+
add_error(:path, "file does not exist: #{path}")
|
|
43
|
+
false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Check if file is readable
|
|
47
|
+
#
|
|
48
|
+
# @param path [String] File path
|
|
49
|
+
# @return [Boolean] true if readable
|
|
50
|
+
def readable?(path)
|
|
51
|
+
return true if File.readable?(path)
|
|
52
|
+
|
|
53
|
+
add_error(:path, "file is not readable: #{path}")
|
|
54
|
+
false
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Check if file has EPUB extension
|
|
58
|
+
#
|
|
59
|
+
# @param path [String] File path
|
|
60
|
+
# @return [Boolean] true if EPUB
|
|
61
|
+
def extension_valid?(path)
|
|
62
|
+
return true if path.downcase.end_with?('.epub')
|
|
63
|
+
return true if Shoko::Adapters::Storage::EpubCache.cache_file?(path)
|
|
64
|
+
|
|
65
|
+
add_error(:path, 'file must have .epub or .cache extension')
|
|
66
|
+
false
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Check if file is not empty
|
|
70
|
+
#
|
|
71
|
+
# @param path [String] File path
|
|
72
|
+
# @return [Boolean] true if not empty
|
|
73
|
+
def not_empty?(path)
|
|
74
|
+
return true if File.size(path).positive?
|
|
75
|
+
|
|
76
|
+
add_error(:path, 'file is empty')
|
|
77
|
+
false
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../../core/validator.rb'
|
|
4
|
+
|
|
5
|
+
module Shoko
|
|
6
|
+
module Adapters::Input::Validators
|
|
7
|
+
# Validates terminal dimensions for proper display.
|
|
8
|
+
# Ensures terminal is large enough for the reader interface.
|
|
9
|
+
class TerminalSizeValidator < Core::Validator
|
|
10
|
+
# Minimum terminal dimensions
|
|
11
|
+
MIN_WIDTH = Adapters::Output::Ui::Constants::UI::MIN_WIDTH
|
|
12
|
+
MIN_HEIGHT = Adapters::Output::Ui::Constants::UI::MIN_HEIGHT
|
|
13
|
+
|
|
14
|
+
# Recommended terminal dimensions for optimal experience
|
|
15
|
+
RECOMMENDED_WIDTH = 80
|
|
16
|
+
RECOMMENDED_HEIGHT = 24
|
|
17
|
+
|
|
18
|
+
# Validate terminal size
|
|
19
|
+
#
|
|
20
|
+
# @param width [Integer] Terminal width
|
|
21
|
+
# @param height [Integer] Terminal height
|
|
22
|
+
# @return [Boolean] Validation result
|
|
23
|
+
def validate?(width, height)
|
|
24
|
+
clear_errors
|
|
25
|
+
|
|
26
|
+
validate_minimum_width?(width) &
|
|
27
|
+
validate_minimum_height?(height)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Check if terminal meets recommended size
|
|
31
|
+
#
|
|
32
|
+
# @param width [Integer] Terminal width
|
|
33
|
+
# @param height [Integer] Terminal height
|
|
34
|
+
# @return [Boolean] true if recommended size or larger
|
|
35
|
+
def recommended_size?(width, height)
|
|
36
|
+
width >= RECOMMENDED_WIDTH && height >= RECOMMENDED_HEIGHT
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Get size recommendations
|
|
40
|
+
#
|
|
41
|
+
# @param width [Integer] Current width
|
|
42
|
+
# @param height [Integer] Current height
|
|
43
|
+
# @return [Hash] Recommendations
|
|
44
|
+
def recommendations(width, height)
|
|
45
|
+
{
|
|
46
|
+
current: { width:, height: },
|
|
47
|
+
minimum: { width: MIN_WIDTH, height: MIN_HEIGHT },
|
|
48
|
+
recommended: { width: RECOMMENDED_WIDTH, height: RECOMMENDED_HEIGHT },
|
|
49
|
+
needs_resize: !recommended_size?(width, height),
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def validate_minimum_width?(width)
|
|
56
|
+
context = RangeValidationContext.new(
|
|
57
|
+
width,
|
|
58
|
+
MIN_WIDTH..Float::INFINITY,
|
|
59
|
+
:width,
|
|
60
|
+
"Terminal width must be at least #{MIN_WIDTH} columns"
|
|
61
|
+
)
|
|
62
|
+
range_valid?(context)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def validate_minimum_height?(height)
|
|
66
|
+
context = RangeValidationContext.new(
|
|
67
|
+
height,
|
|
68
|
+
MIN_HEIGHT..Float::INFINITY,
|
|
69
|
+
:height,
|
|
70
|
+
"Terminal height must be at least #{MIN_HEIGHT} rows"
|
|
71
|
+
)
|
|
72
|
+
range_valid?(context)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'time'
|
|
5
|
+
|
|
6
|
+
module Shoko
|
|
7
|
+
module Adapters::Monitoring
|
|
8
|
+
# Centralized logging system for the Shoko application.
|
|
9
|
+
# Provides structured logging with different severity levels and
|
|
10
|
+
# contextual information for debugging and monitoring.
|
|
11
|
+
#
|
|
12
|
+
# @example Basic usage
|
|
13
|
+
# Logger.info("Application started")
|
|
14
|
+
# Logger.error("Failed to parse EPUB", error: e, path: epub_path)
|
|
15
|
+
#
|
|
16
|
+
# @example With context
|
|
17
|
+
# Logger.with_context(user_id: 123) do
|
|
18
|
+
# Logger.info("Opening book", book_id: 456)
|
|
19
|
+
# end
|
|
20
|
+
class Logger
|
|
21
|
+
LEVELS = {
|
|
22
|
+
debug: 0,
|
|
23
|
+
info: 1,
|
|
24
|
+
warn: 2,
|
|
25
|
+
error: 3,
|
|
26
|
+
fatal: 4,
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
# Current logging level (default: info)
|
|
31
|
+
attr_accessor :level
|
|
32
|
+
|
|
33
|
+
# Output destination (default: STDERR)
|
|
34
|
+
attr_accessor :output
|
|
35
|
+
|
|
36
|
+
# Thread-local context storage
|
|
37
|
+
def context
|
|
38
|
+
Thread.current[:logger_context] ||= {}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Add context for a block of code
|
|
42
|
+
#
|
|
43
|
+
# @param ctx [Hash] Context to add
|
|
44
|
+
# @yield Block to execute with added context
|
|
45
|
+
def with_context(ctx)
|
|
46
|
+
old_context = context.dup
|
|
47
|
+
context.merge!(ctx)
|
|
48
|
+
yield
|
|
49
|
+
ensure
|
|
50
|
+
Thread.current[:logger_context] = old_context
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Log at debug level
|
|
54
|
+
#
|
|
55
|
+
# @param message [String] Log message
|
|
56
|
+
# @param metadata [Hash] Additional metadata
|
|
57
|
+
def debug(message, **metadata)
|
|
58
|
+
log(:debug, message, metadata)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Log at info level
|
|
62
|
+
#
|
|
63
|
+
# @param message [String] Log message
|
|
64
|
+
# @param metadata [Hash] Additional metadata
|
|
65
|
+
def info(message, **metadata)
|
|
66
|
+
log(:info, message, metadata)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Log at warn level
|
|
70
|
+
#
|
|
71
|
+
# @param message [String] Log message
|
|
72
|
+
# @param metadata [Hash] Additional metadata
|
|
73
|
+
def warn(message, **metadata)
|
|
74
|
+
log(:warn, message, metadata)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Log at error level
|
|
78
|
+
#
|
|
79
|
+
# @param message [String] Log message
|
|
80
|
+
# @param metadata [Hash] Additional metadata
|
|
81
|
+
def error(message, **metadata)
|
|
82
|
+
log(:error, message, metadata)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Log at fatal level
|
|
86
|
+
#
|
|
87
|
+
# @param message [String] Log message
|
|
88
|
+
# @param metadata [Hash] Additional metadata
|
|
89
|
+
def fatal(message, **metadata)
|
|
90
|
+
log(:fatal, message, metadata)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def log(severity, message, metadata)
|
|
96
|
+
return if LEVELS[severity] < LEVELS[@level || :info]
|
|
97
|
+
|
|
98
|
+
entry = build_log_entry(severity, message, metadata)
|
|
99
|
+
(@output || $stderr).puts(entry)
|
|
100
|
+
rescue StandardError
|
|
101
|
+
# Logging should never crash the application
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def build_log_entry(severity, message, metadata)
|
|
105
|
+
{
|
|
106
|
+
timestamp: Time.now.iso8601,
|
|
107
|
+
severity: severity.upcase,
|
|
108
|
+
message: normalize_string(message),
|
|
109
|
+
context: sanitize_payload(context),
|
|
110
|
+
metadata: sanitize_payload(metadata),
|
|
111
|
+
thread_id: Thread.current.object_id,
|
|
112
|
+
}.to_json
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def sanitize_payload(value)
|
|
116
|
+
case value
|
|
117
|
+
when String
|
|
118
|
+
normalize_string(value)
|
|
119
|
+
when Hash
|
|
120
|
+
value.each_with_object({}) do |(key, val), acc|
|
|
121
|
+
safe_key = key.is_a?(String) ? normalize_string(key) : key
|
|
122
|
+
acc[safe_key] = sanitize_payload(val)
|
|
123
|
+
end
|
|
124
|
+
when Array
|
|
125
|
+
value.map { |item| sanitize_payload(item) }
|
|
126
|
+
else
|
|
127
|
+
value
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def normalize_string(value)
|
|
132
|
+
str = value.to_s
|
|
133
|
+
return str if str.encoding == Encoding::UTF_8 && str.valid_encoding?
|
|
134
|
+
|
|
135
|
+
str.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: '?')
|
|
136
|
+
rescue StandardError
|
|
137
|
+
value.to_s
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Clear logger state (used in tests)
|
|
141
|
+
def clear
|
|
142
|
+
@output = nil
|
|
143
|
+
@level = nil
|
|
144
|
+
Thread.current[:logger_context] = {}
|
|
145
|
+
end
|
|
146
|
+
public :clear
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|