From 71d624f4d36b1e58e2bff2bcb78f31c22e58eb2a Mon Sep 17 00:00:00 2001 From: "Y.D.X" <73375426+YDX-2147483647@users.noreply.github.com> Date: Sun, 14 May 2023 16:10:13 +0800 Subject: [PATCH] =?UTF-8?q?feature:=20[tui]=20=E6=94=AF=E6=8C=81=E9=A2=84?= =?UTF-8?q?=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bitroom/tui.css | 21 ++++++++++++++ src/bitroom/tui.py | 70 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 src/bitroom/tui.css diff --git a/src/bitroom/tui.css b/src/bitroom/tui.css new file mode 100644 index 0000000..1f2c862 --- /dev/null +++ b/src/bitroom/tui.css @@ -0,0 +1,21 @@ +BookScreen { + align: center middle; +} + +#book-dialog { + grid-size: 2; + grid-gutter: 1 2; + grid-rows: 1fr 3; + padding: 0 1; + width: 60; + height: 11; + border: thick $background 80%; + background: $surface; +} + +#book-dialog > #question { + column-span: 2; + height: 1fr; + width: 1fr; + content-align: center middle; +} diff --git a/src/bitroom/tui.py b/src/bitroom/tui.py index 25e9c43..e2168c3 100644 --- a/src/bitroom/tui.py +++ b/src/bitroom/tui.py @@ -15,7 +15,9 @@ from more_itertools import chunked from textual import on, work from textual.app import App -from textual.widgets import Footer, Header, Input, OptionList +from textual.containers import Grid +from textual.screen import Screen +from textual.widgets import Button, Footer, Header, Input, Label, OptionList from textual.widgets.option_list import Option from textual.worker import get_current_worker @@ -31,14 +33,19 @@ class RoomApp(App): """App to interact with RoomAPI""" + CSS_PATH = "tui.css" + BINDINGS = [ ("d", "toggle_dark", "Dark/切换深色模式"), ("q", "quit", "Quit/退出"), ("r", "refresh_bookings", "Refresh/刷新数据"), + ("enter", "book", "预约选中的房间"), ] config: Config bookings: list[Booking] + bookings_matched_indices: list[int] + """Search result""" def __init__(self) -> None: super().__init__() @@ -49,6 +56,7 @@ def __init__(self) -> None: with Path("bookings.json").open(encoding="utf-8") as f: self.bookings = list(map(Booking.from_dict, load(f))) + self.bookings_matched_indices = list(range(len(self.bookings))) def compose(self) -> ComposeResult: yield Header() @@ -84,7 +92,11 @@ def _search(self, keyword: str) -> None: self.log(f"Start searching for “{keyword}”…") # todo: More advanced search - result = (b for b in self.bookings if all(k in str(b) for k in keyword.split())) + result = ( + i + for i, b in enumerate(self.bookings) + if all(k in str(b) for k in keyword.split()) + ) option_list = self.query_one("#bookings", OptionList) @@ -92,19 +104,20 @@ def _search(self, keyword: str) -> None: self.log(f"The search for “{keyword}” finished, but had been cancelled.") return + self.bookings_matched_indices = list(result) option_list.clear_options() # `option_list.add_option()` always refreshes the widget, which can be slow. # https://github.com/Textualize/textual/blob/14850d54a3f5fed878cff1ce2f5da08503a02932/src/textual/widgets/_option_list.py#L511-L531 # # Here we add 100 options, refresh, add another 100, refresh, and so on. - for bookings_group in chunked(result, 100): + for bookings_group in chunked(self.bookings_matched_indices, 100): if worker.is_cancelled: self.log(f"The search for “{keyword}” is cancelled halfway.") return for b in bookings_group: - content = option_list._make_content(str(b)) + content = option_list._make_content(str(self.bookings[b])) option_list._contents.append(content) if isinstance(content, Option): option_list._options.append(content) @@ -118,6 +131,12 @@ async def action_refresh_bookings(self) -> None: """刷新 bookings 数据""" self._refresh_bookings() + def on_option_list_option_selected( + self, message: OptionList.OptionSelected + ) -> None: + booking = self.bookings[self.bookings_matched_indices[message.option_index]] + self.push_screen(BookScreen(booking, self.config)) + @work(exclusive=True) async def _refresh_bookings(self) -> None: self.log("Start refreshing bookings…") @@ -134,6 +153,49 @@ async def _refresh_bookings(self) -> None: self._search(self.query_one("#search", Input).value) +class BookScreen(Screen): + booking: Booking + config: Config + + def __init__(self, booking: Booking, config: Config) -> None: + super().__init__() + + self.booking = booking + self.config = config + + def compose(self) -> ComposeResult: + yield Grid( + Label(f"你确定要预约“{self.booking}”吗?", id="question"), + Button("确认", variant="success", id="confirm"), + Button("取消", variant="primary", id="cancel"), + id="book-dialog", + ) + + @on(Button.Pressed, "#confirm") + async def book(self, message: Button.Pressed) -> None: + self.log("Start booking…") + + async with AsyncClient() as client: + await auth(client, self.config.username, self.config.password) + api = await RoomAPI.build(client) + + await api.book( + self.booking, + # todo + tel="13806491023", + applicant="Boltzmann", + ) + + # todo: Visualize result + self.log("Booked successfully.") + + self.app.pop_screen() + + @on(Button.Pressed, "#cancel") + def cancel(self, message: Button.Pressed) -> None: + self.app.pop_screen() + + if __name__ == "__main__": app = RoomApp() app.run()