termcore 0.2.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.
Files changed (55) hide show
  1. termcore/__init__.py +6 -0
  2. termcore/cli/__init__.py +3 -0
  3. termcore/cli/output.py +70 -0
  4. termcore/io/__init__.py +7 -0
  5. termcore/io/app_state_storage.py +185 -0
  6. termcore/io/database.py +655 -0
  7. termcore/io/errors.py +71 -0
  8. termcore/io/file.py +263 -0
  9. termcore/io/textfile.py +92 -0
  10. termcore/py.typed +0 -0
  11. termcore/tui/__init__.py +9 -0
  12. termcore/tui/custom_bindings.py +551 -0
  13. termcore/tui/custom_widgets/__init__.py +1 -0
  14. termcore/tui/custom_widgets/custom_checkbox.py +41 -0
  15. termcore/tui/custom_widgets/custom_data_table.py +247 -0
  16. termcore/tui/custom_widgets/custom_selection.py +80 -0
  17. termcore/tui/custom_widgets/multiline_footer.py +240 -0
  18. termcore/tui/question_screen.py +107 -0
  19. termcore/tui/theme_loader.py +495 -0
  20. termcore/tui/themes/__init__.py +2 -0
  21. termcore/tui/themes/classic-black-saturated/style.css +57 -0
  22. termcore/tui/themes/classic-black-saturated/theme.py +22 -0
  23. termcore/tui/themes/classic-black-v1/style.css +57 -0
  24. termcore/tui/themes/classic-black-v1/theme.py +22 -0
  25. termcore/tui/themes/classic-black-v2/style.css +57 -0
  26. termcore/tui/themes/classic-black-v2/theme.py +24 -0
  27. termcore/tui/themes/classic-blue/theme.py +21 -0
  28. termcore/tui/themes/compact-gray/style.tcss +175 -0
  29. termcore/tui/themes/compact-gray/theme.py +22 -0
  30. termcore/tui/themes/mnml-black/style.css +57 -0
  31. termcore/tui/themes/mnml-black/theme.py +21 -0
  32. termcore/tui/themes/mnml-deepblack/style.css +57 -0
  33. termcore/tui/themes/mnml-deepblack/theme.py +21 -0
  34. termcore/tui/themes/pure-amber/theme.py +18 -0
  35. termcore/tui/themes/pure-black/theme.py +18 -0
  36. termcore/tui/themes/pure-blue/theme.py +18 -0
  37. termcore/tui/themes/pure-green/theme.py +18 -0
  38. termcore/tui/themes/pure-sweet16/theme.py +20 -0
  39. termcore/tui/themes/xplore-black/theme.py +18 -0
  40. termcore/tui/themes/xplore-blue/theme.py +18 -0
  41. termcore/tui/themes/xplore-blue-muted/theme.py +18 -0
  42. termcore/tui/themes/xplore-teal/theme.py +18 -0
  43. termcore/util/__init__.py +10 -0
  44. termcore/util/datetime.py +173 -0
  45. termcore/util/debug.py +196 -0
  46. termcore/util/index.py +112 -0
  47. termcore/util/logger.py +36 -0
  48. termcore/util/singleton.py +46 -0
  49. termcore/util/string.py +263 -0
  50. termcore/util/validation.py +31 -0
  51. termcore/util/version.py +56 -0
  52. termcore-0.2.0.dist-info/METADATA +504 -0
  53. termcore-0.2.0.dist-info/RECORD +55 -0
  54. termcore-0.2.0.dist-info/WHEEL +4 -0
  55. termcore-0.2.0.dist-info/licenses/LICENSE +21 -0
termcore/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Terminal utilities for CLI, TUI, IO and general use."""
2
+
3
+ from .cli import *
4
+ from .io import *
5
+ from .tui import *
6
+ from .util import *
@@ -0,0 +1,3 @@
1
+ """Styled console output for command line applications."""
2
+
3
+ from .output import *
termcore/cli/output.py ADDED
@@ -0,0 +1,70 @@
1
+ """
2
+ Provides utility functions for styled CLI output using Rich.
3
+
4
+ Includes helpers for printing color-coded panels (error, warning, success and
5
+ info) and a function to clear previously printed lines from the terminal.
6
+ """
7
+ import sys
8
+
9
+ from rich.console import Console
10
+ from rich.panel import Panel
11
+
12
+ __all__ = [
13
+ "clear_lines",
14
+ "console",
15
+ "get_console",
16
+ "print_custom_panel",
17
+ "print_error",
18
+ "print_info",
19
+ "print_panel",
20
+ "print_success",
21
+ "print_warning",
22
+ ]
23
+
24
+ console = Console()
25
+
26
+
27
+ def get_console() -> Console:
28
+ """Returns the Rich Console instance."""
29
+ return console
30
+
31
+
32
+ def print_error(message: str) -> None:
33
+ """Prints an error message in a red panel using Rich."""
34
+ print_panel(f"✗ {message}", "red")
35
+
36
+
37
+ def print_warning(message: str) -> None:
38
+ """Prints a warning message in a yellow panel using Rich."""
39
+ print_panel(f"⚠ {message}", "yellow")
40
+
41
+
42
+ def print_success(message: str) -> None:
43
+ """Prints a success message in a green panel using Rich."""
44
+ print_panel(f"✓ {message}", "green")
45
+
46
+
47
+ def print_info(message: str) -> None:
48
+ """Prints an info message in a cyan panel using Rich."""
49
+ print_panel(f"ℹ {message}", "cyan") # noqa: RUF001 - the glyph is the icon
50
+
51
+
52
+ def print_panel(message: str, color: str) -> None:
53
+ """Prints a message in a red panel with the given color using Rich."""
54
+ formatted_message = f"[{color} bold]{message}[/{color} bold]"
55
+ print_custom_panel(formatted_message, color)
56
+
57
+
58
+ def print_custom_panel(formatted_message: str, panel_color: str) -> None:
59
+ """Prints a pre-formatted message in a coloured Rich panel."""
60
+ console.print(Panel(
61
+ formatted_message,
62
+ border_style=panel_color,
63
+ padding=(1, 2)
64
+ ))
65
+
66
+
67
+ def clear_lines(count: int) -> None:
68
+ """Move cursor up `count` lines and clear everything below."""
69
+ _ = sys.stdout.write(f"\033[{count}A\033[0J")
70
+ _ = sys.stdout.flush()
@@ -0,0 +1,7 @@
1
+ """Storage adapters: SQLite, JSON app state and text files."""
2
+
3
+ from .app_state_storage import *
4
+ from .database import *
5
+ from .errors import *
6
+ from .file import *
7
+ from .textfile import *
@@ -0,0 +1,185 @@
1
+ """
2
+ A JSON-backed store for key-value pairs of application state.
3
+
4
+ Useful for persisting things like the last scroll position or the command
5
+ history. `AppStateStorage` follows the singleton pattern, so one instance
6
+ serves the whole application.
7
+
8
+ Features:
9
+
10
+ - Reads a JSON file and loads its content into a dictionary.
11
+ - Provides methods to retrieve (`get`) and update (`set`) key-value pairs.
12
+ - Supports modifying lists within the JSON file, including:
13
+ - Inserting elements at a specific index (`list_insert`)
14
+ - Editing individual elements within a list (`edit_list_item`)
15
+ - Deleting elements from a list (`delete_list_item`)
16
+ - Moving elements within a list (`move_list_item`)
17
+ - Ensures data persistence by saving changes to the JSON file automatically.
18
+ - Manages file creation and error handling for missing or corrupted JSON files.
19
+ """
20
+ import json
21
+ from pathlib import Path
22
+ from typing import cast
23
+
24
+ from termcore.io.errors import StateFileError
25
+ from termcore.util.singleton import Singleton
26
+
27
+ __all__ = [
28
+ "AppStateStorage",
29
+ ]
30
+
31
+
32
+ class AppStateStorage(metaclass=Singleton):
33
+ """
34
+ Keeps the content of a JSON file in a dictionary.
35
+
36
+ The values are reachable with `get()`.
37
+
38
+ Changed values or new key-value pairs can be stored within the Dictionary
39
+ and the JSON file with the method `set()`.
40
+
41
+ Attributes
42
+ ----------
43
+ _json_file : Path or str or None
44
+ Path to the JSON file.
45
+ _json_dict : dict[str, object]
46
+ Content of the JSON file as a dictionary.
47
+ """
48
+
49
+ _json_file_path: Path | str | None
50
+ _json_dict: dict[str, object]
51
+
52
+
53
+ def __init__(
54
+ self,
55
+ package_name: str | None = None,
56
+ state_file_path: Path | str | None = None
57
+ ) -> None:
58
+ """
59
+ Opens the JSON file and stores the key-value pairs in `self.json_dict`.
60
+
61
+ Raises
62
+ ------
63
+ Exception
64
+ If no JSON file is given and the class instance is None.
65
+ """
66
+ if state_file_path is None and package_name is not None:
67
+ state_dir = Path.home() / ".local" / "state" / package_name
68
+ state_dir.mkdir(parents=True, exist_ok=True)
69
+ state_file_path = state_dir / "state.json"
70
+ elif state_file_path is None and AppStateStorage.instance is None:
71
+ raise StateFileError(
72
+ "Either state_file_path or package_name must be given."
73
+ )
74
+ self._json_file_path = state_file_path
75
+ self._json_dict = {}
76
+ self._read_json_file()
77
+
78
+ def _read_json_file(self) -> None:
79
+ """
80
+ Opens the JSON file and stores the content in `self.json_dict`.
81
+
82
+ Raises
83
+ ------
84
+ FileNotFoundError
85
+ If the JSON file cannot be found and cannot be created.
86
+ json.JSONDecodeError
87
+ If the JSON file contains invalid JSON.
88
+ """
89
+ if self._json_file_path is None:
90
+ raise StateFileError("No JSON file given.")
91
+ json_path = Path(self._json_file_path)
92
+ abs_path = json_path.resolve()
93
+
94
+ try:
95
+ with json_path.open(encoding="utf-8") as file:
96
+ self._json_dict = json.load(file)
97
+ except FileNotFoundError:
98
+ # File not found -> try to create a new one
99
+ try:
100
+ with json_path.open("w", encoding="utf-8") as file:
101
+ _ = file.write("{}")
102
+ except OSError as error:
103
+ raise StateFileError(
104
+ f"Could not find or create \"{abs_path}\"."
105
+ ) from error
106
+ except json.JSONDecodeError as error:
107
+ raise StateFileError(
108
+ f"File \"{abs_path}\" contains invalid JSON."
109
+ ) from error
110
+
111
+ def _save_json_file(self) -> None:
112
+ """Stores the content of self.json_dict in the JSON file."""
113
+ if self._json_file_path is None:
114
+ raise StateFileError("No JSON file given.")
115
+ json_path = Path(self._json_file_path)
116
+
117
+ try:
118
+ with json_path.open("w", encoding="utf-8") as file:
119
+ json.dump(self._json_dict, file, indent=4)
120
+ except OSError as error:
121
+ raise StateFileError(
122
+ f"File \"{json_path.resolve()}\" could not be written."
123
+ ) from error
124
+
125
+ def get(self, key: str, default_value: object = None) -> object | None:
126
+ """
127
+ Returns a value from the JSON file.
128
+
129
+ If the key cannot be found, the given default value is returned.
130
+ """
131
+ if key in self._json_dict:
132
+ return self._json_dict[key]
133
+ return default_value # type: ignore
134
+
135
+ def set(self, key: str, value: object) -> None:
136
+ """Saves a key-value pair in the JSON file."""
137
+ self._json_dict.update({key: value})
138
+ self._save_json_file()
139
+
140
+ def list_insert(
141
+ self, list_name: str, list_index: int, value: object
142
+ ) -> None:
143
+ """Adds a new entry to a list at the given index."""
144
+ # Create list if it doesn't exist
145
+ if list_name not in self._json_dict:
146
+ self._json_dict.update({list_name: []})
147
+
148
+ # Create empty list if value is empty
149
+ if self._json_dict[list_name] is None:
150
+ self._json_dict[list_name] = []
151
+
152
+ # Add given data to dict and write to JSON file
153
+ lst = self._json_dict[list_name]
154
+ if isinstance(lst, list):
155
+ cast("list[object]", lst).insert(list_index, value)
156
+ self._json_dict[list_name] = lst
157
+ self._save_json_file()
158
+ else:
159
+ raise TypeError(f"Value of \"{list_name}\" is not a list.")
160
+
161
+ def edit_list_item( # noqa: PLR0913 - addressing one cell needs all four
162
+ self, list_name: str, list_index: int, dict_key: str, value: object
163
+ ) -> None:
164
+ """Changes the value of a list item."""
165
+ lst = cast("list[dict[str, object]]", self._json_dict[list_name])
166
+ lst[list_index][dict_key] = value
167
+ self._json_dict[list_name] = lst
168
+ self._save_json_file()
169
+
170
+ def delete_list_item(self, list_name: str, list_index: int) -> None:
171
+ """Deletes an element of a list."""
172
+ lst = cast("list[dict[str, object]]", self._json_dict[list_name])
173
+ del lst[list_index]
174
+ self._save_json_file()
175
+
176
+ def move_list_item(
177
+ self, list_name: str, list_index: int, list_index_new: int
178
+ ) -> None:
179
+ """Moves a list item by deleting and re-inserting it elsewhere."""
180
+ lst = cast("list[object]", self._json_dict[list_name])
181
+ list_element: object = lst[list_index]
182
+ del lst[list_index]
183
+ lst.insert(list_index_new, list_element)
184
+ self._json_dict[list_name] = lst
185
+ self._save_json_file()