tasko 0.0.2__tar.gz → 0.0.3__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tasko
3
- Version: 0.0.2
3
+ Version: 0.0.3
4
4
  Summary: Personal task manager
5
5
  Author: pybass
6
6
  License-Expression: MIT
@@ -0,0 +1,121 @@
1
+ [project]
2
+ name = "tasko"
3
+ version = "0.0.3"
4
+ description = "Personal task manager"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.14"
9
+ classifiers = [
10
+ "Environment :: Console",
11
+ "Programming Language :: Python :: 3.14",
12
+ "Topic :: Office/Business",
13
+ ]
14
+ dependencies = [
15
+ "rich~=15.0",
16
+ "textual~=8.2",
17
+ ]
18
+
19
+ [[project.authors]]
20
+ name = "pybass"
21
+
22
+ [project.urls]
23
+ Repository = "https://github.com/pybass/tasko"
24
+ Issues = "https://github.com/pybass/tasko/issues"
25
+
26
+ [project.scripts]
27
+ tasko = "tasko.main:main"
28
+
29
+ [build-system]
30
+ requires = ["uv_build>=0.11,<0.13"]
31
+ build-backend = "uv_build"
32
+
33
+ [dependency-groups]
34
+ dev = [
35
+ "ruff~=0.16.2",
36
+ "mypy~=2.3.0",
37
+ "pytest~=9.1.1",
38
+ "pytest-asyncio~=1.4.0",
39
+ "pytest-cov~=7.1.0",
40
+ "pytest-xdist~=3.8.0",
41
+ "pip-audit~=2.10.1",
42
+ "deptry~=0.25.1",
43
+ "pre-commit~=4.6.2",
44
+ ]
45
+
46
+ [tool.ruff]
47
+ line-length = 130
48
+ target-version = "py314"
49
+
50
+ [tool.ruff.lint]
51
+ select = ["ALL"]
52
+ ignore = [
53
+ "COM812",
54
+ "D203",
55
+ "D213",
56
+ "A005",
57
+ "PLC0414",
58
+ "RET503",
59
+ "G004",
60
+ "PT001",
61
+ "PT006",
62
+ "PT007",
63
+ "PT023",
64
+ "FIX",
65
+ "FBT",
66
+ "EM",
67
+ "CPY",
68
+ "TC",
69
+ "TRY003",
70
+ "C901",
71
+ "PLR0911",
72
+ "PLR0912",
73
+ "PLR0913",
74
+ "PLR2004",
75
+ "ASYNC109",
76
+ ]
77
+
78
+ [tool.ruff.lint.pep8-naming]
79
+ classmethod-decorators = ["pydantic.field_validator"]
80
+
81
+ [tool.ruff.lint.per-file-ignores]
82
+ "tests/**/*.py" = [
83
+ "ANN",
84
+ "S",
85
+ ]
86
+
87
+ [tool.ruff.format]
88
+ quote-style = "double"
89
+ indent-style = "space"
90
+ docstring-code-format = true
91
+
92
+ [tool.mypy]
93
+ python_version = "3.14"
94
+ strict = true
95
+
96
+ [[tool.mypy.overrides]]
97
+ module = "tests.*"
98
+ disallow_untyped_defs = false
99
+ disallow_incomplete_defs = false
100
+ disallow_untyped_decorators = false
101
+ disallow_untyped_calls = false
102
+ warn_unused_ignores = false
103
+
104
+ [tool.codespell]
105
+ skip = "uv.lock,.git,.venv,.mypy_cache,.ruff_cache,.pytest_cache,htmlcov"
106
+
107
+ [tool.pytest.ini_options]
108
+ testpaths = ["tests"]
109
+ addopts = "-ra --strict-markers --strict-config --import-mode=importlib"
110
+ xfail_strict = true
111
+ asyncio_mode = "auto"
112
+ asyncio_default_fixture_loop_scope = "function"
113
+ filterwarnings = ["error"]
114
+
115
+ [tool.coverage.run]
116
+ branch = true
117
+ source = ["src"]
118
+
119
+ [tool.coverage.report]
120
+ show_missing = true
121
+ skip_covered = false
@@ -3,7 +3,7 @@
3
3
  # ============================================================================
4
4
  [project]
5
5
  name = "tasko"
6
- version = "0.0.2"
6
+ version = "0.0.3"
7
7
  description = "Personal task manager"
8
8
  readme = "README.md"
9
9
  authors = [{ name = "pybass" }]
@@ -35,7 +35,7 @@ tasko = "tasko.main:main"
35
35
  # uv.lock does not cover, so an open-ended requirement would let the same tag build differently
36
36
  # over time.
37
37
  [build-system]
38
- requires = ["uv_build>=0.11,<0.12"]
38
+ requires = ["uv_build>=0.11,<0.13"]
39
39
  build-backend = "uv_build"
40
40
 
41
41
 
@@ -45,7 +45,7 @@ build-backend = "uv_build"
45
45
  [dependency-groups]
46
46
  dev = [
47
47
  # Linting & formatting
48
- "ruff~=0.15.22",
48
+ "ruff~=0.16.2",
49
49
  # Type checking
50
50
  "mypy~=2.3.0",
51
51
  # Testing & coverage
@@ -58,7 +58,7 @@ dev = [
58
58
  # Dependency hygiene — imports vs declared deps
59
59
  "deptry~=0.25.1",
60
60
  # Git pre-commit hooks
61
- "pre-commit~=4.6.0",
61
+ "pre-commit~=4.6.2",
62
62
  ]
63
63
 
64
64
 
@@ -74,7 +74,6 @@ select = ["ALL"]
74
74
  ignore = [
75
75
  # --- Ecosystem / tool conflicts ---
76
76
  "COM812", # trailing-comma — handled by ruff formatter, must be off here
77
- "ISC001", # single-line-implicit-string-concat — conflicts with ruff formatter
78
77
  "D203", # one-blank-line-before-class — conflicts with D211
79
78
  "D213", # multi-line-summary-second-line — conflicts with D212
80
79
 
@@ -94,6 +93,8 @@ ignore = [
94
93
  "FIX", # flake8-fixme — TODOs are fine; tracked elsewhere
95
94
  "FBT", # boolean-trap — too restrictive
96
95
  "EM", # error-message strings — inline messages accepted
96
+ "CPY", # flake8-copyright — no copyright headers: internal-only code, ships no license by design (README)
97
+ "TC", # flake8-type-checking — TYPE_CHECKING blocks save import time we don't care about; cost: import noise + broken runtime annotations (pydantic)
97
98
  "TRY003", # raise-vanilla-args — too noisy
98
99
  "C901", # complex-structure — judgment call
99
100
 
@@ -103,14 +104,13 @@ ignore = [
103
104
  "PLR0913", # too-many-arguments
104
105
  "PLR2004", # magic-value-comparison
105
106
 
106
- # --- TODO metadata (optional) ---
107
- "TD002", # missing-todo-author
108
- "TD003", # missing-todo-link
109
-
110
107
  # --- Async edge case ---
111
108
  "ASYNC109", # async-function-with-timeout — debatable, off
112
109
  ]
113
110
 
111
+ [tool.ruff.lint.pep8-naming]
112
+ classmethod-decorators = ["pydantic.field_validator"] # N805: @field_validator implies classmethod — cls is correct without a stacked @classmethod
113
+
114
114
  [tool.ruff.lint.per-file-ignores]
115
115
  "tests/**/*.py" = ["ANN", "S"] # tests skip type-annotation (ANN) and assert/security (S) rules
116
116
 
@@ -2,15 +2,12 @@
2
2
 
3
3
  import sqlite3
4
4
  import time
5
- from typing import TYPE_CHECKING
5
+ from pathlib import Path
6
6
 
7
7
  from tasko.core.errors import AppError
8
8
  from tasko.core.migrations import MIGRATIONS
9
9
  from tasko.core.models import AppState, Priority, Project, Status, Task
10
10
 
11
- if TYPE_CHECKING:
12
- from pathlib import Path
13
-
14
11
 
15
12
  def _now() -> int:
16
13
  """Return the current unix time in whole seconds."""
@@ -82,28 +79,24 @@ class Core:
82
79
  raise AppError(f"Task #{task_id} not found.")
83
80
  return Task.from_row(row)
84
81
 
85
- def list_tasks(
86
- self, *, project_id: int | None = None, status: Status | None = None, include_done: bool = False
87
- ) -> list[Task]:
88
- """Return tasks in working order, optionally filtered by project id and/or status; a None filter means "any".
82
+ def list_tasks(self, *, project_id: int | None = None, include_done: bool = False) -> list[Task]:
83
+ """Return tasks in working order, optionally filtered to one project; done tasks appear only with include_done.
89
84
 
90
85
  Working order: doing first, then by priority, then recently updated; done tasks
91
- sink to the bottom (recently finished first, since done_at == updated_at). They
92
- are excluded by default; asking for status DONE explicitly implies them.
86
+ sink to the bottom (recently finished first, since done_at == updated_at).
93
87
  """
94
88
  rows = self._conn.execute(
95
89
  """
96
90
  SELECT t.*, p.name AS project_name
97
91
  FROM tasks t JOIN projects p ON p.id = t.project_id
98
92
  WHERE (:project_id IS NULL OR t.project_id = :project_id)
99
- AND (:status IS NULL OR t.status = :status)
100
93
  AND (:include_done OR t.status != 'done')
101
94
  ORDER BY t.status = 'done',
102
95
  t.status = 'doing' DESC,
103
96
  CASE t.priority WHEN 'high' THEN 0 WHEN 'medium' THEN 1 ELSE 2 END,
104
97
  t.updated_at DESC
105
98
  """,
106
- {"project_id": project_id, "status": status, "include_done": include_done or status is Status.DONE},
99
+ {"project_id": project_id, "include_done": include_done},
107
100
  ).fetchall()
108
101
  return [Task.from_row(row) for row in rows]
109
102
 
@@ -1,7 +1,7 @@
1
1
  """Database schema migrations, applied in order by Core."""
2
2
 
3
- # Schema v1 (see docs/data-model.md). IF NOT EXISTS / OR IGNORE predate the atomic runner
4
- # in Core._migrate and are harmless; new migrations don't need to be rerun-safe.
3
+ # Schema v1 (see docs/data-model.md). IF NOT EXISTS / OR IGNORE make a rerun a no-op;
4
+ # with the atomic runner in Core._migrate, new migrations don't need to be rerun-safe.
5
5
  _MIGRATION_V1 = """
6
6
  CREATE TABLE IF NOT EXISTS projects (
7
7
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -1,11 +1,9 @@
1
1
  """Domain models: projects, tasks, and their enums."""
2
2
 
3
+ import sqlite3
3
4
  from dataclasses import dataclass
4
5
  from enum import StrEnum
5
- from typing import TYPE_CHECKING, Any, Self
6
-
7
- if TYPE_CHECKING:
8
- import sqlite3
6
+ from typing import Any, Self
9
7
 
10
8
 
11
9
  class Status(StrEnum):
@@ -1,19 +1,16 @@
1
1
  """Textual application: a thin shell that mounts the main screen."""
2
2
 
3
- from typing import TYPE_CHECKING, ClassVar
3
+ from typing import ClassVar
4
4
 
5
5
  from textual.app import App
6
+ from textual.binding import BindingType
6
7
 
8
+ from tasko.core.core import Core
7
9
  from tasko.tui.screens.dialogs import HelpDialog
8
10
  from tasko.tui.screens.projects import ProjectsScreen
9
11
  from tasko.tui.screens.task_detail import TaskDetailScreen
10
12
  from tasko.tui.screens.task_list import TaskListScreen
11
13
 
12
- if TYPE_CHECKING:
13
- from textual.binding import BindingType
14
-
15
- from tasko.core.core import Core
16
-
17
14
 
18
15
  class TaskoApp(App[None]):
19
16
  """Interactive TUI for managing tasks."""
@@ -1,11 +1,13 @@
1
- """Reusable modal dialogs: one-line text input, yes/no confirmation, option picking, and key help."""
1
+ """Reusable modal dialogs: one-line and multi-line text input, yes/no confirmation, option picking, and key help."""
2
2
 
3
+ from collections.abc import Sequence
3
4
  from importlib.metadata import version
4
- from typing import TYPE_CHECKING, ClassVar, Final
5
+ from typing import ClassVar, Final
5
6
 
6
7
  from rich.style import Style
7
8
  from rich.table import Table
8
9
  from rich.text import Text
10
+ from textual.app import ComposeResult
9
11
  from textual.binding import Binding, BindingType
10
12
  from textual.containers import Vertical
11
13
  from textual.fuzzy import FuzzySearch
@@ -13,19 +15,11 @@ from textual.screen import ModalScreen
13
15
  from textual.widgets import Input, Label, OptionList, Static, TextArea
14
16
  from textual.widgets.option_list import Option
15
17
 
16
- if TYPE_CHECKING:
17
- from collections.abc import Sequence
18
-
19
- from textual.app import ComposeResult
20
-
21
18
  _MATCH_STYLE: Final = Style(bold=True, underline=True) # characters of a label the fuzzy query matched
22
19
 
23
20
 
24
21
  class Dialog[ResultT](ModalScreen[ResultT]):
25
- """Base modal dialog: a centered bordered panel over the dimmed screen; Escape cancels.
26
-
27
- Subclasses define action_cancel with their own "cancelled" result.
28
- """
22
+ """Base modal dialog: a centered bordered panel over the dimmed screen; Escape cancels with None."""
29
23
 
30
24
  DEFAULT_CSS = """
31
25
  Dialog {
@@ -46,6 +40,10 @@ class Dialog[ResultT](ModalScreen[ResultT]):
46
40
 
47
41
  BINDINGS: ClassVar[list[BindingType]] = [("escape", "cancel", "Cancel")]
48
42
 
43
+ def action_cancel(self) -> None:
44
+ """Cancel the dialog without a result."""
45
+ self.dismiss(None)
46
+
49
47
 
50
48
  class InputDialog(Dialog[str | None]):
51
49
  """Prompt for one line of text; dismisses with the value, or None on cancel."""
@@ -58,7 +56,7 @@ class InputDialog(Dialog[str | None]):
58
56
  """
59
57
 
60
58
  def __init__(self, title: str, value: str = "") -> None:
61
- """Set the prompt title and the optional pre-filled value (used by rename)."""
59
+ """Set the prompt title and the optional pre-filled value."""
62
60
  super().__init__()
63
61
  self._title = title # prompt shown above the input
64
62
  self._value = value # initial input content
@@ -77,10 +75,6 @@ class InputDialog(Dialog[str | None]):
77
75
  """Enter accepts the value."""
78
76
  self.dismiss(event.value)
79
77
 
80
- def action_cancel(self) -> None:
81
- """Escape cancels without a value."""
82
- self.dismiss(None)
83
-
84
78
 
85
79
  class TextDialog(Dialog[str | None]):
86
80
  """Edit multi-line text; dismisses with the text on Ctrl+S, or None on cancel.
@@ -135,10 +129,6 @@ class TextDialog(Dialog[str | None]):
135
129
  """Ctrl+A selects the whole text, e.g. to replace or delete a long body at once."""
136
130
  self.query_one(TextArea).select_all()
137
131
 
138
- def action_cancel(self) -> None:
139
- """Escape cancels without a value."""
140
- self.dismiss(None)
141
-
142
132
 
143
133
  class ConfirmDialog(Dialog[bool]):
144
134
  """Ask a yes/no question; dismisses with True only on explicit confirmation."""
@@ -170,10 +160,6 @@ class ConfirmDialog(Dialog[bool]):
170
160
  """Confirm the action."""
171
161
  self.dismiss(True)
172
162
 
173
- def action_cancel(self) -> None:
174
- """Cancel the action."""
175
- self.dismiss(False)
176
-
177
163
 
178
164
  class HelpDialog(Dialog[None]):
179
165
  """Show the current screen's keys as a two-column list; Escape, ? or i closes it."""
@@ -207,10 +193,6 @@ class HelpDialog(Dialog[None]):
207
193
  yield Static(grid, id="keys")
208
194
  yield Label(f"tasko v{version('tasko')}", id="about")
209
195
 
210
- def action_cancel(self) -> None:
211
- """Close the help."""
212
- self.dismiss(None)
213
-
214
196
 
215
197
  class SelectDialog(Dialog[str | None]):
216
198
  """Pick one option with arrows and Enter; dismisses with the option id, or None on cancel.
@@ -336,7 +318,3 @@ class SelectDialog(Dialog[str | None]):
336
318
  def action_last(self) -> None:
337
319
  """Jump the list highlight to the last option without leaving the input."""
338
320
  self.query_one(OptionList).action_last()
339
-
340
- def action_cancel(self) -> None:
341
- """Escape cancels without a choice."""
342
- self.dismiss(None)
@@ -1,28 +1,24 @@
1
1
  """Projects screen: a table of projects with management actions."""
2
2
 
3
- from typing import TYPE_CHECKING, ClassVar, Final
3
+ from collections.abc import Callable
4
+ from typing import ClassVar, Final
4
5
 
5
6
  from rich.text import Text
7
+ from textual.app import ComposeResult
8
+ from textual.binding import BindingType
6
9
  from textual.screen import Screen
7
10
  from textual.widgets import DataTable
8
11
 
12
+ from tasko.core.core import Core
9
13
  from tasko.core.errors import AppError
10
14
  from tasko.tui.screens.dialogs import ConfirmDialog, InputDialog
11
15
  from tasko.tui.widgets import StatusBar
12
16
 
13
- if TYPE_CHECKING:
14
- from collections.abc import Callable
15
-
16
- from textual.app import ComposeResult
17
- from textual.binding import BindingType
18
-
19
- from tasko.core.core import Core
20
-
21
17
  _DEFAULT_CELL: Final = Text("default", style="green") # marks the default project; space is not scarce here
22
18
 
23
19
 
24
20
  class ProjectsScreen(Screen[None]):
25
- """Project management — rarely visited, reached from the task screen with P."""
21
+ """Project management — rarely visited, reached from the task list with P."""
26
22
 
27
23
  BINDINGS: ClassVar[list[BindingType]] = [
28
24
  ("j", "cursor_down", "Down"),
@@ -1,32 +1,28 @@
1
1
  """Task detail screen: full details of one task with per-field editing."""
2
2
 
3
+ from collections.abc import Callable
3
4
  from datetime import UTC, datetime
4
- from typing import TYPE_CHECKING, ClassVar, Final
5
+ from typing import ClassVar, Final
5
6
 
6
7
  from rich.table import Table
7
8
  from rich.text import Text
9
+ from textual.app import ComposeResult
10
+ from textual.binding import BindingType
8
11
  from textual.containers import VerticalScroll
9
12
  from textual.screen import Screen
10
13
  from textual.widgets import Static
11
14
 
15
+ from tasko.core.core import Core
12
16
  from tasko.core.errors import AppError
13
17
  from tasko.core.models import Priority, Status, Task
14
18
  from tasko.tui.screens.dialogs import ConfirmDialog, InputDialog, SelectDialog, TextDialog
15
19
  from tasko.tui.widgets import StatusBar
16
20
 
17
- if TYPE_CHECKING:
18
- from collections.abc import Callable
19
-
20
- from textual.app import ComposeResult
21
- from textual.binding import BindingType
22
-
23
- from tasko.core.core import Core
24
-
25
21
  # Same glyphs and colors as the table on the main screen, spelled out with the word.
26
22
  _STATUS_TEXT: Final = {
27
- Status.TODO: Text("○ todo", style="dim"),
28
- Status.DOING: Text("◐ doing", style="yellow"),
29
- Status.DONE: Text("● done", style="green"),
23
+ Status.TODO: Text("· todo", style="dim"),
24
+ Status.DOING: Text("» doing", style="yellow"),
25
+ Status.DONE: Text("✓ done", style="green"),
30
26
  }
31
27
  _PRIORITY_TEXT: Final = {
32
28
  Priority.HIGH: Text("high", style="red"),
@@ -123,7 +119,7 @@ class TaskDetailScreen(Screen[None]):
123
119
  self.query_one(StatusBar).set_context(Text.assemble(f"task #{task.id}", (f" · {task.project_name}", "dim")))
124
120
 
125
121
  self.query_one("#title", Static).update(Text(task.title))
126
- # Two column groups: live fields on the left (main-table order), dates on the right, all dim.
122
+ # Two column groups: live fields on the left (main-table order), dates on the right; all dim but the field values.
127
123
  meta = Table.grid(padding=(0, 2))
128
124
  meta.add_column(style="dim")
129
125
  meta.add_column()
@@ -1,12 +1,15 @@
1
1
  """Main screen: the task table."""
2
2
 
3
- from typing import TYPE_CHECKING, ClassVar, Final
3
+ from typing import ClassVar, Final
4
4
 
5
5
  from rich.cells import cell_len
6
6
  from rich.text import Text
7
+ from textual.app import ComposeResult
8
+ from textual.binding import BindingType
7
9
  from textual.screen import Screen
8
10
  from textual.widgets import DataTable
9
11
 
12
+ from tasko.core.core import Core
10
13
  from tasko.core.errors import AppError
11
14
  from tasko.core.models import Priority, Status
12
15
  from tasko.tui.screens.dialogs import ConfirmDialog, InputDialog, SelectDialog
@@ -14,17 +17,11 @@ from tasko.tui.screens.projects import ProjectsScreen
14
17
  from tasko.tui.screens.task_detail import TaskDetailScreen
15
18
  from tasko.tui.widgets import StatusBar
16
19
 
17
- if TYPE_CHECKING:
18
- from textual.app import ComposeResult
19
- from textual.binding import BindingType
20
-
21
- from tasko.core.core import Core
22
-
23
- # One-character, color-coded cells: the S/P/B columns must not waste width.
20
+ # One-character, color-coded cells: the status, priority and body columns must not waste width.
24
21
  _STATUS_CELLS: Final = {
25
- Status.TODO: Text("○", style="dim"),
26
- Status.DOING: Text("◐", style="yellow"),
27
- Status.DONE: Text("●", style="green"),
22
+ Status.TODO: Text("·", style="dim"),
23
+ Status.DOING: Text("»", style="yellow"),
24
+ Status.DONE: Text("✓", style="green"),
28
25
  }
29
26
  _PRIORITY_CELLS: Final = {
30
27
  Priority.HIGH: Text("↑", style="red"),
@@ -164,14 +161,8 @@ class TaskListScreen(Screen[None]):
164
161
  falls back to the selected, then the default project. Wrong target? Escape, move
165
162
  the cursor to a row of the right project, or change the project on the task later.
166
163
  """
167
- task_id = self._cursor_task_id()
168
- if task_id is not None:
169
- task = self._core.get_task(task_id)
170
- project_id, name = task.project_id, task.project_name
171
- else:
172
- state = self._core.app_state()
173
- project_id = state.selected_project_id or state.default_project_id
174
- name = self._core.get_project(project_id).name
164
+ project_id = self._add_target_project_id()
165
+ name = self._core.get_project(project_id).name
175
166
 
176
167
  def on_result(title: str | None) -> None:
177
168
  if not title:
@@ -192,12 +183,7 @@ class TaskListScreen(Screen[None]):
192
183
  """
193
184
  options: list[tuple[str, str | Text]] = [(str(p.id), p.name) for p in self._core.list_projects()]
194
185
  # Preselect the project plain 'a' would target, so Enter-Enter behaves like 'a'.
195
- task_id = self._cursor_task_id()
196
- if task_id is not None:
197
- current = str(self._core.get_task(task_id).project_id)
198
- else:
199
- state = self._core.app_state()
200
- current = str(state.selected_project_id or state.default_project_id)
186
+ current = str(self._add_target_project_id())
201
187
 
202
188
  def on_project(choice: str | None) -> None:
203
189
  if choice is None:
@@ -223,6 +209,14 @@ class TaskListScreen(Screen[None]):
223
209
 
224
210
  self.app.push_screen(SelectDialog("Add task to project:", options, current), on_project)
225
211
 
212
+ def _add_target_project_id(self) -> int:
213
+ """Project a plain add targets: the cursor row's project, else the selected, else the default one."""
214
+ task_id = self._cursor_task_id()
215
+ if task_id is not None:
216
+ return self._core.get_task(task_id).project_id
217
+ state = self._core.app_state()
218
+ return state.selected_project_id or state.default_project_id
219
+
226
220
  def _cursor_task_id(self) -> int | None:
227
221
  """Return the id of the task under the cursor, or None on an empty table.
228
222
 
@@ -1,15 +1,11 @@
1
1
  """Shared TUI widgets."""
2
2
 
3
- from typing import TYPE_CHECKING
4
-
5
3
  from rich.text import Text
4
+ from textual.app import ComposeResult
6
5
  from textual.containers import Horizontal
6
+ from textual.visual import VisualType
7
7
  from textual.widgets import Label
8
8
 
9
- if TYPE_CHECKING:
10
- from textual.app import ComposeResult
11
- from textual.visual import VisualType
12
-
13
9
 
14
10
  class StatusBar(Horizontal):
15
11
  """One-line bottom bar: the screen's context on the left, the help key hint on the right.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes