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.
- termcore/__init__.py +6 -0
- termcore/cli/__init__.py +3 -0
- termcore/cli/output.py +70 -0
- termcore/io/__init__.py +7 -0
- termcore/io/app_state_storage.py +185 -0
- termcore/io/database.py +655 -0
- termcore/io/errors.py +71 -0
- termcore/io/file.py +263 -0
- termcore/io/textfile.py +92 -0
- termcore/py.typed +0 -0
- termcore/tui/__init__.py +9 -0
- termcore/tui/custom_bindings.py +551 -0
- termcore/tui/custom_widgets/__init__.py +1 -0
- termcore/tui/custom_widgets/custom_checkbox.py +41 -0
- termcore/tui/custom_widgets/custom_data_table.py +247 -0
- termcore/tui/custom_widgets/custom_selection.py +80 -0
- termcore/tui/custom_widgets/multiline_footer.py +240 -0
- termcore/tui/question_screen.py +107 -0
- termcore/tui/theme_loader.py +495 -0
- termcore/tui/themes/__init__.py +2 -0
- termcore/tui/themes/classic-black-saturated/style.css +57 -0
- termcore/tui/themes/classic-black-saturated/theme.py +22 -0
- termcore/tui/themes/classic-black-v1/style.css +57 -0
- termcore/tui/themes/classic-black-v1/theme.py +22 -0
- termcore/tui/themes/classic-black-v2/style.css +57 -0
- termcore/tui/themes/classic-black-v2/theme.py +24 -0
- termcore/tui/themes/classic-blue/theme.py +21 -0
- termcore/tui/themes/compact-gray/style.tcss +175 -0
- termcore/tui/themes/compact-gray/theme.py +22 -0
- termcore/tui/themes/mnml-black/style.css +57 -0
- termcore/tui/themes/mnml-black/theme.py +21 -0
- termcore/tui/themes/mnml-deepblack/style.css +57 -0
- termcore/tui/themes/mnml-deepblack/theme.py +21 -0
- termcore/tui/themes/pure-amber/theme.py +18 -0
- termcore/tui/themes/pure-black/theme.py +18 -0
- termcore/tui/themes/pure-blue/theme.py +18 -0
- termcore/tui/themes/pure-green/theme.py +18 -0
- termcore/tui/themes/pure-sweet16/theme.py +20 -0
- termcore/tui/themes/xplore-black/theme.py +18 -0
- termcore/tui/themes/xplore-blue/theme.py +18 -0
- termcore/tui/themes/xplore-blue-muted/theme.py +18 -0
- termcore/tui/themes/xplore-teal/theme.py +18 -0
- termcore/util/__init__.py +10 -0
- termcore/util/datetime.py +173 -0
- termcore/util/debug.py +196 -0
- termcore/util/index.py +112 -0
- termcore/util/logger.py +36 -0
- termcore/util/singleton.py +46 -0
- termcore/util/string.py +263 -0
- termcore/util/validation.py +31 -0
- termcore/util/version.py +56 -0
- termcore-0.2.0.dist-info/METADATA +504 -0
- termcore-0.2.0.dist-info/RECORD +55 -0
- termcore-0.2.0.dist-info/WHEEL +4 -0
- termcore-0.2.0.dist-info/licenses/LICENSE +21 -0
termcore/__init__.py
ADDED
termcore/cli/__init__.py
ADDED
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()
|
termcore/io/__init__.py
ADDED
|
@@ -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()
|