voidcli 0.1.0__py3-none-any.whl

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.
void/views/activity.py ADDED
@@ -0,0 +1,247 @@
1
+ from typing import override
2
+
3
+ from textual import on
4
+ from textual.app import ComposeResult
5
+ from textual.containers import Horizontal, Vertical
6
+ from textual.message import Message
7
+ from textual.widgets import Button, DataTable, Input, Label, Select, Static
8
+
9
+ from void.controllers.activity import ActivityController
10
+ from void.controllers.category import CategoryController
11
+
12
+
13
+ class ActivitiesView(Static):
14
+
15
+ class Changed(Message):
16
+ """Posted when the activity list changes, so NoteForm can re-render."""
17
+
18
+ DEFAULT_CSS = """
19
+ #category_module{
20
+ height: 1fr;
21
+ padding: 0 2 0 0;
22
+ width: 30%;
23
+ border-right: solid $primary;
24
+ }
25
+
26
+ #activity_module{
27
+ padding: 0 0 0 2;
28
+ height: 1fr;
29
+ width: 70%;
30
+ }
31
+
32
+ .field_label{
33
+ margin: 1 0 0 0;
34
+ }
35
+
36
+ #add_category{
37
+ margin: 1 0;
38
+ }
39
+
40
+ #activity_form{
41
+ height: auto;
42
+ margin: 0 0 1 0;
43
+ }
44
+
45
+ #activity_form_cta_container{
46
+ height: auto;
47
+ margin: 1 0 0 0;
48
+ }
49
+
50
+ #activity_form_cta_container Button{
51
+ margin: 0 1 0 0;
52
+ }
53
+
54
+ #activities_table{
55
+ height: 1fr;
56
+ }
57
+
58
+ #activity_id{
59
+ display: none;
60
+ }
61
+
62
+ """
63
+
64
+ @override
65
+ def __init__(self) -> None:
66
+ super().__init__()
67
+ self.ctrl = ActivityController()
68
+ self.categoriesCtrl = CategoryController()
69
+
70
+ @override
71
+ def compose(self) -> ComposeResult:
72
+ with Vertical(classes="header"):
73
+ yield Label("VOID ACTIVITIES", classes="module_title")
74
+ yield Label(self.counter_text(), id="counter")
75
+ with Horizontal(classes="main_container"):
76
+ with Vertical(id="category_module"):
77
+ yield Label("CATEGORIES", classes="module_title")
78
+ yield Label("New Category", classes="field_label")
79
+ yield Input(placeholder="Add a new category", id="new_category")
80
+ yield Button(label="ADD", flat=True, id="add_category")
81
+ yield DataTable(id="categories_table")
82
+ with Vertical(id="activity_module"):
83
+ with Vertical(id="activity_form"):
84
+ yield Label("ACTIVITIES", classes="module_title")
85
+ yield Label("Activity", classes="field_label")
86
+ yield Input(placeholder="Add a new activity", id="new_activity")
87
+ yield Label("Select a category", classes="field_label")
88
+ yield Select([], type_to_search=True, id="category_select")
89
+ yield Input(id="activity_id", compact=True)
90
+ with Horizontal(id="activity_form_cta_container"):
91
+ yield Button(label="ADD", flat=True, id="add_activity", variant="success")
92
+ yield Button(label="UPDATE", flat=True, id="update_activity",variant="warning", disabled=True)
93
+ yield Button(label="SUSPEND", flat=True, id="suspend_activity", variant="error", disabled=True)
94
+ yield Button(label="CLEAR", flat=True, id="clear_activity_form", variant="default")
95
+ yield DataTable(id="activities_table", cursor_type="row")
96
+
97
+ def on_mount(self) -> None:
98
+ # Category inputs
99
+ self.add_category_input = self.query_one("#new_category", Input)
100
+ self.add_category_btn = self.query_one("#add_category", Button)
101
+ self.categories_table = self.query_one("#categories_table", DataTable)
102
+
103
+ # Activity inputs
104
+ self.add_activity_input = self.query_one("#new_activity", Input)
105
+ self.category_selector = self.query_one("#category_select", Select)
106
+ self.activity_id = self.query_one("#activity_id", Input)
107
+ self.add_activity_btn = self.query_one("#add_activity", Button)
108
+ self.update_activity_btn = self.query_one("#update_activity", Button)
109
+ self.suspend_activity_btn = self.query_one("#suspend_activity", Button)
110
+ self.clear_activity_form_btn = self.query_one("#clear_activity_form", Button)
111
+ self.activities_table = self.query_one("#activities_table", DataTable)
112
+
113
+ self.init_activities_table()
114
+ self.init_categories_selector()
115
+ self.init_categories_table()
116
+
117
+ def counter_text(self) -> str:
118
+ activities = len(self.ctrl.get_activities())
119
+ categories = len(self.categoriesCtrl.get_categories())
120
+ return f"{activities} activities in {categories} categories"
121
+
122
+ def update_counter(self) -> None:
123
+ self.query_one("#counter", Label).update(self.counter_text())
124
+
125
+ def init_activities_table(self):
126
+ activities = self.ctrl.get_activities()
127
+ self.activities_table.add_columns("ID","ACTIVITY", "CATEGORY")
128
+ self.activities_table.add_rows(activities)
129
+
130
+ def init_categories_selector(self):
131
+ self.category_selector.clear()
132
+ self.category_selector.set_options(self.categoriesCtrl.get_categories_for_activities())
133
+
134
+ def init_categories_table(self):
135
+ categories = self.categoriesCtrl.get_categories()
136
+ self.categories_table.add_columns("CATEGORY")
137
+ self.categories_table.add_rows(categories)
138
+
139
+ def update_activities(self):
140
+ activities = self.ctrl.get_activities()
141
+ self.activities_table.clear()
142
+ self.activities_table.add_rows(activities)
143
+ self.update_counter()
144
+
145
+ def update_categories(self):
146
+ categories = self.categoriesCtrl.get_categories()
147
+ self.categories_table.clear()
148
+ self.categories_table.add_rows(categories)
149
+ self.init_categories_selector()
150
+ self.update_counter()
151
+
152
+
153
+ @on(Button.Pressed, "#clear_activity_form")
154
+ def on_clear_activity_form(self) -> None:
155
+ self.clear_activity_form()
156
+
157
+ @on(Button.Pressed, "#add_activity")
158
+ def on_add_activity(self) -> None:
159
+ activity = self.add_activity_input.value
160
+ category_id = self.category_selector.value
161
+
162
+ if not activity:
163
+ self.notify("Activity must be set", severity="error")
164
+ return
165
+ if not category_id:
166
+ self.notify("Category must be set", severity="error")
167
+ return
168
+
169
+ if not self.ctrl.create_activity(activity, category_id):
170
+ self.notify(f"'{activity}' already exists in that category", severity="error")
171
+ return
172
+
173
+ self.add_activity_input.value = ""
174
+ self.category_selector.clear()
175
+ self.update_activities()
176
+ self.post_message(self.Changed())
177
+
178
+ @on(Button.Pressed, "#update_activity")
179
+ def on_update_activity(self) -> None:
180
+ activity = self.add_activity_input.value
181
+ activity_id = self.activity_id.value
182
+ category_id = self.category_selector.value
183
+
184
+ if not activity:
185
+ self.notify("Activity must be set", severity="error")
186
+ return
187
+ if not category_id:
188
+ self.notify("Category must be set", severity="error")
189
+ return
190
+
191
+ if not self.ctrl.update_activity(activity_id, activity, category_id):
192
+ self.notify(f"'{activity}' already exists in that category", severity="error")
193
+ return
194
+
195
+ self.update_activities()
196
+ self.clear_activity_form()
197
+ self.post_message(self.Changed())
198
+
199
+ @on(Button.Pressed, "#suspend_activity")
200
+ def on_suspend_activity(self) -> None:
201
+ activity_id = self.activity_id.value
202
+ self.ctrl.suspend_activity(activity_id)
203
+ self.clear_activity_form()
204
+ self.update_activities()
205
+ self.post_message(self.Changed())
206
+
207
+
208
+
209
+ @on(Button.Pressed, "#add_category")
210
+ def on_add_category(self) -> None:
211
+ category = self.add_category_input.value
212
+ if not category:
213
+ self.notify("Category must be set", severity="error")
214
+ return
215
+
216
+ if not self.categoriesCtrl.create_category(category):
217
+ self.notify(f"Category '{category}' already exists", severity="error")
218
+ return
219
+
220
+ self.add_category_input.value = ""
221
+ self.update_categories()
222
+
223
+ @on(DataTable.RowSelected, "#activities_table")
224
+ def on_activity_row_selected(self, event: DataTable.RowSelected) -> None:
225
+ row = event.data_table.get_row(event.row_key)
226
+ ac_id = row[0]
227
+ ac_name = row[1]
228
+ ac_category = row[2]
229
+
230
+ self.add_activity_input.value = ac_name
231
+ self.activity_id.value = str(ac_id)
232
+
233
+ categories = self.categoriesCtrl.get_categories_for_activities()
234
+ category_id = next((cid for name, cid in categories if name == ac_category), Select.BLANK)
235
+ self.category_selector.value = category_id
236
+
237
+ self.update_activity_btn.disabled = False
238
+ self.add_activity_btn.disabled = True
239
+ self.suspend_activity_btn.disabled = False
240
+
241
+ def clear_activity_form(self):
242
+ self.add_activity_input.value = ""
243
+ self.activity_id.value = ""
244
+ self.category_selector.clear()
245
+ self.update_activity_btn.disabled = True
246
+ self.add_activity_btn.disabled = False
247
+ self.suspend_activity_btn.disabled = True
@@ -0,0 +1,52 @@
1
+ from datetime import date
2
+ from itertools import groupby
3
+ from typing import override
4
+
5
+ from textual.app import ComposeResult
6
+ from textual.containers import Grid, Vertical, VerticalScroll
7
+ from textual.widgets import Label, Static
8
+
9
+ from void.controllers.note import NoteController
10
+
11
+
12
+ class CollectionView(Static):
13
+
14
+ DEFAULT_CSS = """
15
+ .note_grid{
16
+ grid-size: 3;
17
+ grid-gutter: 1;
18
+ }
19
+ .note_card_date{
20
+ color: $text-muted;
21
+ padding: 0 0 1 0;
22
+ }
23
+ """
24
+
25
+ @override
26
+ def __init__(self) -> None:
27
+ super().__init__()
28
+ self.ctrl = NoteController()
29
+
30
+ @override
31
+ def compose(self) -> ComposeResult:
32
+ notes = self.ctrl.get_notes()
33
+ with Vertical(classes="header"):
34
+ yield Label("VOID COLLECTION", classes="module_title")
35
+ with VerticalScroll(classes="main_container"):
36
+ if not notes:
37
+ yield Label("No VOID NOTE saved yet.")
38
+ return
39
+ with Grid(classes="card_grid note_grid"):
40
+ for note_date, rows in groupby(notes, key=lambda row: row["note_date"]):
41
+ with Vertical(classes="note_card"):
42
+ yield Label(self.format_date(note_date), classes="note_card_date")
43
+ for row in rows:
44
+ with Vertical(classes="entry_card"):
45
+ yield Label(row["activity"], classes="entry_title")
46
+ if row["notes"]:
47
+ yield Label(row["notes"], classes="entry_note")
48
+ else:
49
+ yield Label("No notes written.", classes="entry_empty")
50
+
51
+ def format_date(self, date_str):
52
+ return date.fromisoformat(date_str).strftime("%B %d, %Y").upper()
void/views/day_note.py ADDED
@@ -0,0 +1,101 @@
1
+ from collections import Counter
2
+ from datetime import date
3
+ from itertools import groupby
4
+ from typing import override
5
+
6
+ from textual import on
7
+ from textual.app import ComposeResult
8
+ from textual.containers import Center, Horizontal, Vertical, VerticalScroll
9
+ from textual.widgets import Button, Label, Static, TabbedContent
10
+
11
+ from void.controllers.note import NoteController
12
+
13
+
14
+ class DayNote(Static):
15
+
16
+ DEFAULT_CSS = """
17
+ .day_logged{
18
+ width: 2fr;
19
+ }
20
+ .day_pending{
21
+ width: 1fr;
22
+ padding: 0 0 0 2;
23
+ }
24
+ .day_pending Label{
25
+ width: 100%;
26
+ }
27
+ .pending_category{
28
+ color: $text-muted;
29
+ padding: 1 0 0 3;
30
+ }
31
+ .pending_item{
32
+ color: $text-disabled;
33
+ padding: 0 0 0 2;
34
+ border-left: thick $primary;
35
+ }
36
+ """
37
+
38
+ @override
39
+ def __init__(self) -> None:
40
+ super().__init__()
41
+ self.ctrl = NoteController()
42
+
43
+ self.today = date.today() # noqa: DTZ011
44
+ self.date_str = self.today.strftime("%B %d, %Y").upper()
45
+
46
+ @override
47
+ def compose(self) -> ComposeResult:
48
+ day_note_data = self.ctrl.get_day_note(self.today.isoformat())
49
+ activities = self.ctrl.get_activities()
50
+ per_category = Counter(category for _, _, category in activities)
51
+
52
+ logged_ids = {row["activity_id"] for row in day_note_data}
53
+ pending = sorted(
54
+ (category, name)
55
+ for activity_id, name, category in activities
56
+ if activity_id not in logged_ids
57
+ )
58
+
59
+ with Vertical(classes="header"):
60
+ yield Label(self.date_str, classes="module_title")
61
+ yield Label(self.counter_text(len(day_note_data), len(activities)), id="counter")
62
+
63
+ if not day_note_data:
64
+ with Vertical(classes="state_screen"), Vertical(classes="state_card"):
65
+ yield Label("NOTHING LOGGED TODAY", classes="state_title")
66
+ yield Label("Write today's note to fill the void.", classes="state_hint")
67
+ with Center():
68
+ yield Button("WRITE VOID NOTE", id="go_to_note", variant="primary", flat=True)
69
+ return
70
+
71
+ with Horizontal(classes="main_container"):
72
+ with VerticalScroll(classes="day_logged"):
73
+ for category, group in groupby(day_note_data, key=lambda row: row["category"]):
74
+ rows = list(group)
75
+ yield Label(
76
+ f"── {category.upper()} · {len(rows)} of {per_category[category]} ",
77
+ classes="section_title",
78
+ )
79
+ for row in rows:
80
+ with Vertical(classes="entry_card"):
81
+ yield Label(row["activity"], classes="entry_title")
82
+ if row["notes"]:
83
+ yield Label(row["notes"], classes="entry_note")
84
+ else:
85
+ yield Label("No notes written.", classes="entry_empty")
86
+
87
+ if pending:
88
+ with VerticalScroll(classes="day_pending"):
89
+ yield Label(f"── NOT LOGGED · {len(pending)} ", classes="section_title")
90
+ for category, group in groupby(pending, key=lambda item: item[0]):
91
+ yield Label(category.upper(), classes="pending_category")
92
+ for _, name in group:
93
+ yield Label(name, classes="pending_item")
94
+
95
+ def counter_text(self, logged: int, total: int) -> str:
96
+ activities = "activity" if total == 1 else "activities"
97
+ return f"{logged} of {total} {activities} logged"
98
+
99
+ @on(Button.Pressed, "#go_to_note")
100
+ def on_go_to_note(self) -> None:
101
+ self.app.query_one(TabbedContent).active = "void_note"
void/views/note.py ADDED
@@ -0,0 +1,47 @@
1
+ from datetime import date
2
+ from typing import override
3
+
4
+ from textual import on
5
+ from textual.app import ComposeResult
6
+ from textual.containers import Center, Vertical
7
+ from textual.widgets import Button, Label, Static, TabbedContent
8
+
9
+ from void.controllers.note import NoteController
10
+ from void.views.note_form import NoteForm
11
+
12
+
13
+ class NoteView(Static):
14
+
15
+
16
+ @override
17
+ def __init__(self) -> None:
18
+ super().__init__()
19
+ self.ctrl = NoteController()
20
+ self.today = date.today() # noqa: DTZ011
21
+ self.date_str = self.today.strftime("%B %d, %Y").upper()
22
+
23
+
24
+ @override
25
+ def compose(self) -> ComposeResult:
26
+ created_note_of_the_day = self.ctrl.get_day_note(self.today.isoformat())
27
+ if created_note_of_the_day:
28
+ with Vertical(classes="state_screen"), Vertical(classes="state_card state_card_success"):
29
+ yield Label(self.date_str, classes="state_muted")
30
+ yield Label("VOID NOTE SAVED", classes="state_title state_success")
31
+ yield Label(self.hint_text(len(created_note_of_the_day)), classes="state_hint")
32
+ with Center():
33
+ yield Button("SEE VOID DAY", id="go_to_day_note", variant="success", flat=True)
34
+ else:
35
+ yield NoteForm()
36
+
37
+ def hint_text(self, logged: int) -> str:
38
+ activities = "activity" if logged == 1 else "activities"
39
+ return f"{logged} {activities} logged today"
40
+
41
+ @on(Button.Pressed, "#go_to_day_note")
42
+ def on_go_to_day_note(self) -> None:
43
+ self.app.query_one(TabbedContent).active = "void_day_note"
44
+
45
+ @on(NoteForm.Saved)
46
+ async def on_note_saved(self) -> None:
47
+ await self.recompose()
@@ -0,0 +1,144 @@
1
+ from datetime import date
2
+ from typing import override
3
+
4
+ from textual import on
5
+ from textual.app import ComposeResult
6
+ from textual.binding import Binding
7
+ from textual.containers import Grid, Horizontal, Vertical, VerticalScroll
8
+ from textual.message import Message
9
+ from textual.widgets import Button, Checkbox, Input, Label, Static, TextArea
10
+
11
+ from void.controllers.note import NoteController
12
+
13
+
14
+ class NoteForm(Static):
15
+
16
+ class Saved(Message):
17
+ """Posted once the day's note is stored, so NoteView can swap to DayNote."""
18
+
19
+ BINDINGS = [Binding("ctrl+s", "save", "SAVE VOID NOTE", priority=True)]
20
+
21
+ DEFAULT_CSS = """
22
+ NoteForm{
23
+ layout: vertical;
24
+ }
25
+ .actions{
26
+ dock: bottom;
27
+ height: auto;
28
+ padding: 0 2 1 2;
29
+ align-horizontal: right;
30
+ }
31
+ #date_str{
32
+ display: none;
33
+ }
34
+
35
+ .category_block{
36
+ height: auto;
37
+ padding: 0 0 1 0;
38
+ }
39
+ .category_title{
40
+ text-style: bold;
41
+ color: $accent;
42
+ padding: 0 0 1 0;
43
+ }
44
+ .activity_grid{
45
+ grid-size: 2;
46
+ grid-gutter: 1 2;
47
+ }
48
+ .activity_card{
49
+ height: auto;
50
+ padding: 1 1;
51
+ }
52
+ .activity_card Checkbox{
53
+ width: 100%;
54
+ }
55
+ .activity_card TextArea{
56
+ height: 6;
57
+ }
58
+ .activity_handle{
59
+ display: none;
60
+ }
61
+ """
62
+
63
+ @override
64
+ def __init__(self) -> None:
65
+ super().__init__()
66
+ self.ctrl = NoteController()
67
+ self.activities_by_category_data = self.ctrl.get_activities_by_category()
68
+ self.activity_total = self.ctrl.get_activity_total()
69
+
70
+ self.today = date.today() # noqa: DTZ011
71
+ self.date_str = self.today.strftime("%B %d, %Y").upper()
72
+ self.date_iso = self.today.isoformat()
73
+
74
+ day_note_data = self.ctrl.get_day_note(self.today.isoformat())
75
+
76
+ self.disable_add_note_btn = False
77
+ if day_note_data:
78
+ self.disable_add_note_btn = True
79
+
80
+
81
+
82
+ @override
83
+ def compose(self) -> ComposeResult:
84
+ with Vertical(classes="header"):
85
+ yield Label(self.date_str, classes="module_title")
86
+ yield Label(self.counter_text(0), id="counter")
87
+ yield Input(value=self.date_iso, id="date_str")
88
+ with Horizontal(classes="actions"):
89
+ yield Button("SAVE VOID NOTE", id="save_note_btn", variant="success", flat=True, disabled=self.disable_add_note_btn)
90
+ with VerticalScroll(classes="main_container"):
91
+ for _, category, activities in self.activities_by_category_data:
92
+ if activities:
93
+ with Vertical(classes="category_block"):
94
+ yield Label(f"── {category} ", classes="category_title")
95
+ with Grid(classes="card_grid activity_grid"):
96
+ for id, name in activities:
97
+ handle = f"{id}::{self.handleize(name)}"
98
+ with Vertical(classes="activity_card"):
99
+ yield Checkbox(label=name)
100
+ yield Input(value=handle, classes="activity_handle")
101
+ yield TextArea()
102
+
103
+ @on(Checkbox.Changed)
104
+ def on_activity_toggled(self) -> None:
105
+ self.query_one("#counter", Label).update(self.counter_text(self.count_check_elements()))
106
+
107
+ def counter_text(self, checked: int) -> str:
108
+ return f"{checked} of {self.activity_total} activities logged"
109
+
110
+ def action_save(self) -> None:
111
+ if not self.disable_add_note_btn:
112
+ self.on_save_note()
113
+
114
+ @on(Button.Pressed, "#save_note_btn")
115
+ def on_save_note(self) -> None:
116
+ if self.count_check_elements() == 0:
117
+ self.notify("Please check at least one activity to save", severity="warning")
118
+ return
119
+
120
+ checked_activities = []
121
+ for checkbox in self.query(Checkbox):
122
+ if checkbox.value:
123
+ parent_container = checkbox.query_ancestor(Vertical)
124
+ handle = parent_container.query_one(Input).value
125
+ note = parent_container.query_one(TextArea).text
126
+ checked_activities.append((handle, note))
127
+
128
+ self.save_note(checked_activities)
129
+
130
+ def save_note(self, activities):
131
+ date_str = self.query_one("#date_str", Input).value.strip()
132
+ self.ctrl.save_note(date_str, activities)
133
+ self.notify("VOID NOTE successfully saved", severity="information")
134
+ self.post_message(self.Saved())
135
+
136
+ def count_check_elements(self) -> int:
137
+ counter = 0
138
+ for checkbox in self.query(Checkbox):
139
+ if checkbox.value:
140
+ counter += 1
141
+ return counter
142
+
143
+ def handleize(self, string_to_handle):
144
+ return string_to_handle.lower().replace("'","").replace(" ", "-")
void/views/welcome.py ADDED
@@ -0,0 +1,72 @@
1
+ from typing import override
2
+
3
+ from textual.app import ComposeResult
4
+ from textual.containers import Vertical
5
+ from textual.events import Key
6
+ from textual.screen import Screen
7
+ from textual.widgets import Label, Rule
8
+
9
+ LOGO = """\
10
+ █████ █████ ███████ █████ ██████████
11
+ ░░███ ░░███ ███░░░░░███ ░░███ ░░███░░░░███
12
+ ░███ ░███ ███ ░░███ ░███ ░███ ░░███
13
+ ░███ ░███ ░███ ░███ ░███ ░███ ░███
14
+ ░░███ ███ ░███ ░███ ░███ ░███ ░███
15
+ ░░░█████░ ░░███ ███ ░███ ░███ ███
16
+ ░░███ ░░░███████░ █████ ██████████
17
+ ░░░ ░░░░░░░ ░░░░░ ░░░░░░░░░░\
18
+ """
19
+
20
+
21
+ AUTO_DISMISS = 1.75
22
+
23
+
24
+ class WelcomeScreen(Screen[None]):
25
+
26
+ DEFAULT_CSS = """
27
+ .welcome_card{
28
+ width: 59;
29
+ max-width: 100%;
30
+ height: auto;
31
+ padding: 2 4;
32
+ border: round $primary;
33
+ }
34
+ .welcome_card Label{
35
+ width: 100%;
36
+ text-align: center;
37
+ }
38
+ .welcome_logo{
39
+ color: $primary;
40
+ text-style: bold;
41
+ }
42
+ .welcome_card Rule{
43
+ color: $primary;
44
+ }
45
+ .welcome_tagline{
46
+ padding: 0 0 1 0;
47
+ color: $text-muted;
48
+ }
49
+ .welcome_hint{
50
+ color: $accent;
51
+ text-style: italic;
52
+ }
53
+ """
54
+
55
+ @override
56
+ def compose(self) -> ComposeResult:
57
+ with Vertical(classes="state_screen"), Vertical(classes="welcome_card"):
58
+ yield Label(LOGO, classes="welcome_logo")
59
+ yield Rule(line_style="heavy")
60
+ yield Label("VITAL OFFLINE INFORMATION DIARY", classes="welcome_tagline")
61
+ yield Label("press any key to skip", classes="welcome_hint")
62
+
63
+ def on_mount(self) -> None:
64
+ self.set_timer(AUTO_DISMISS, self.close)
65
+
66
+ def on_key(self, _: Key) -> None:
67
+ self.close()
68
+
69
+ def close(self) -> None:
70
+ # returns None on purpose: awaiting dismiss() from a handler is an error
71
+ if self.app.screen is self:
72
+ self.dismiss()