1+ import dataclasses
12import subprocess
2- from dataclasses import asdict
33from datetime import datetime
44from pathlib import Path
55from typing import Type
1111from textual .widgets import LoadingIndicator
1212
1313from .components .contents import Content
14- from .components .events import DoneLoading , FollowThis , OpenThisImage , Screenshot
15- from .components .windows import Alert , DictDisplay , ToC
14+ from .components .events import DoneLoading , FollowThis , OpenThisImage , Screenshot , SearchSubmitted
15+ from .components .windows import Alert , DictDisplay , SearchInputPrompt , ToC
1616from .config import load_config
1717from .ebooks import Ebook
1818from .exceptions import ImageViewerDoesNotExist
19- from .models import KeyMap , ReadingHistory
19+ from .models import Coordinate , KeyMap , ReadingHistory , SearchMode
2020from .utils .app_resources import get_resource_file
2121from .utils .keys_parser import dispatch_key
2222
@@ -33,6 +33,7 @@ def __init__(self, ebook_path: Path, ebook_class: Type[Ebook]):
3333 # TODO: make reactive and display percentage
3434 # as alternative for scrollbar
3535 self .reading_progress = 0.0
36+ self .search_mode = None
3637
3738 def on_load (self , _ : events .Load ) -> None :
3839 assert self ._loop is not None
@@ -63,8 +64,8 @@ async def on_done_loading(self, event: DoneLoading) -> None:
6364 def init_render () -> None :
6465 # restore reading progress
6566 # make sure to call this after refresh so the screen.max_scroll_y != 0
66- historic_y = self .ebook_state .reading_progress * self .screen .max_scroll_y
67- self .screen .scroll_to (None , historic_y , speed = 0 , animate = False ) # type: ignore
67+ self . reading_progress = self .ebook_state .reading_progress * self .screen .max_scroll_y
68+ self .screen .scroll_to (None , self . reading_progress , duration = 0 , animate = False ) # type: ignore
6869
6970 self .get_widget_by_id ("startup-loader" , LoadingIndicator ).remove ()
7071
@@ -103,7 +104,7 @@ async def on_key(self, event: events.Key) -> None:
103104 keymaps = self .config .keymaps
104105 await dispatch_key (
105106 [
106- KeyMap (keymaps .close , self .action_quit ),
107+ KeyMap (keymaps .close , self .action_cancel_search_or_quit ),
107108 KeyMap (keymaps .scroll_down , self .screen .action_scroll_down ),
108109 KeyMap (keymaps .scroll_up , self .screen .action_scroll_up ),
109110 # KeyMap(keymaps.page_up, self.screen.action_page_up),
@@ -117,8 +118,12 @@ async def on_key(self, event: events.Key) -> None:
117118 KeyMap (keymaps .open_help , self .action_open_help ),
118119 KeyMap (keymaps .toggle_dark , self .action_toggle_dark ),
119120 KeyMap (keymaps .screenshot , lambda : self .post_message (Screenshot ())),
120- # TODO: search feature
121- # KeyMap(["D"], lambda: self.log("baca--->>>", self.content._segments[3]._render_cache.lines[1].text)),
121+ KeyMap (keymaps .search_forward , lambda : self .action_input_search (forward = True )),
122+ KeyMap (keymaps .search_backward , lambda : self .action_input_search (forward = False )),
123+ KeyMap (keymaps .next_match , self .action_search_next ),
124+ KeyMap (keymaps .prev_match , self .action_search_prev ),
125+ KeyMap (keymaps .confirm , self .action_stop_search ),
126+ # KeyMap(["D"], lambda: self.log()),
122127 ],
123128 event ,
124129 )
@@ -133,7 +138,7 @@ async def alert(self, message: str) -> None:
133138 async def action_open_metadata (self ) -> None :
134139 if self .metadata_window is None :
135140 metadata_window = DictDisplay (
136- config = self .config , id = "metadata" , title = "Metadata" , data = asdict (self .ebook .get_meta ())
141+ config = self .config , id = "metadata" , title = "Metadata" , data = dataclasses . asdict (self .ebook .get_meta ())
137142 )
138143 await self .mount (metadata_window )
139144
@@ -147,9 +152,45 @@ def action_page_up(self) -> None:
147152 raise SkipAction ()
148153 self .screen .scroll_page_up (duration = self .config .page_scroll_duration )
149154
155+ async def action_input_search (self , forward : bool ) -> None :
156+ await self .mount (SearchInputPrompt (forward = forward ))
157+
158+ async def action_search_next (self ) -> bool :
159+ if self .search_mode is not None :
160+ new_coord = await self .content .search_next (
161+ self .search_mode .pattern_str ,
162+ self .search_mode .current_coord ,
163+ self .search_mode .forward ,
164+ )
165+ if new_coord is not None :
166+ self .search_mode = dataclasses .replace (self .search_mode , current_coord = new_coord )
167+ return True
168+ else :
169+ # TODO: inconsistent alert window size on initial search
170+ await self .alert (f"Found no match: '{ self .search_mode .pattern_str } '" )
171+
172+ return False
173+
174+ async def action_search_prev (self ) -> None :
175+ if self .search_mode is not None :
176+ new_coord = await self .content .search_next (
177+ self .search_mode .pattern_str ,
178+ self .search_mode .current_coord ,
179+ not self .search_mode .forward ,
180+ )
181+ if new_coord is not None :
182+ self .search_mode = dataclasses .replace (self .search_mode , current_coord = new_coord )
183+
184+ async def action_stop_search (self ) -> None :
185+ if self .search_mode is not None :
186+ self .search_mode = None
187+ await self .content .clear_search ()
188+
150189 async def action_open_help (self ) -> None :
151190 if self .help_window is None :
152- keymap_data = {k .replace ("_" , " " ).title (): "," .join (v ) for k , v in asdict (self .config .keymaps ).items ()}
191+ keymap_data = {
192+ k .replace ("_" , " " ).title (): "," .join (v ) for k , v in dataclasses .asdict (self .config .keymaps ).items ()
193+ }
153194 help_window = DictDisplay (config = self .config , id = "help" , title = "Keymaps" , data = keymap_data )
154195 await self .mount (help_window )
155196
@@ -173,6 +214,26 @@ async def action_open_toc(self) -> None:
173214 # NOTE: await to prevent broken layout
174215 await self .mount (toc )
175216
217+ async def action_cancel_search_or_quit (self ) -> None :
218+ if self .search_mode is not None :
219+ self .screen .scroll_to (
220+ 0 , self .search_mode .saved_position * self .screen .max_scroll_y , duration = self .config .page_scroll_duration
221+ )
222+ await self .action_stop_search ()
223+ else :
224+ await self .action_quit ()
225+
226+ async def on_search_submitted (self , message : SearchSubmitted ) -> None :
227+ self .search_mode = SearchMode (
228+ pattern_str = message .value ,
229+ current_coord = Coordinate (- 1 if message .forward else self .content .size .width , self .screen .scroll_offset .y ),
230+ forward = message .forward ,
231+ saved_position = self .reading_progress ,
232+ )
233+ is_found = await self .action_search_next ()
234+ if not is_found :
235+ self .search_mode = None
236+
176237 async def on_follow_this (self , message : FollowThis ) -> None :
177238 self .content .scroll_to_section (message .nav_point )
178239 # NOTE: remove after refresh so the event get handled
0 commit comments