bear-utils 0.0.1__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.
- bear_utils/__init__.py +51 -0
- bear_utils/__main__.py +14 -0
- bear_utils/_internal/__init__.py +0 -0
- bear_utils/_internal/_version.py +1 -0
- bear_utils/_internal/cli.py +119 -0
- bear_utils/_internal/debug.py +174 -0
- bear_utils/ai/__init__.py +30 -0
- bear_utils/ai/ai_helpers/__init__.py +136 -0
- bear_utils/ai/ai_helpers/_common.py +19 -0
- bear_utils/ai/ai_helpers/_config.py +24 -0
- bear_utils/ai/ai_helpers/_parsers.py +194 -0
- bear_utils/ai/ai_helpers/_types.py +15 -0
- bear_utils/cache/__init__.py +131 -0
- bear_utils/cli/__init__.py +22 -0
- bear_utils/cli/_args.py +12 -0
- bear_utils/cli/_get_version.py +207 -0
- bear_utils/cli/commands.py +105 -0
- bear_utils/cli/prompt_helpers.py +186 -0
- bear_utils/cli/shell/__init__.py +1 -0
- bear_utils/cli/shell/_base_command.py +81 -0
- bear_utils/cli/shell/_base_shell.py +430 -0
- bear_utils/cli/shell/_common.py +19 -0
- bear_utils/cli/typer_bridge.py +90 -0
- bear_utils/config/__init__.py +13 -0
- bear_utils/config/config_manager.py +229 -0
- bear_utils/config/dir_manager.py +69 -0
- bear_utils/config/settings_manager.py +179 -0
- bear_utils/constants/__init__.py +90 -0
- bear_utils/constants/_exceptions.py +8 -0
- bear_utils/constants/_exit_code.py +60 -0
- bear_utils/constants/_http_status_code.py +37 -0
- bear_utils/constants/_lazy_typing.py +15 -0
- bear_utils/constants/_meta.py +196 -0
- bear_utils/constants/date_related.py +25 -0
- bear_utils/constants/time_related.py +24 -0
- bear_utils/database/__init__.py +8 -0
- bear_utils/database/_db_manager.py +98 -0
- bear_utils/events/__init__.py +18 -0
- bear_utils/events/events_class.py +52 -0
- bear_utils/events/events_module.py +74 -0
- bear_utils/extras/__init__.py +28 -0
- bear_utils/extras/_async_helpers.py +67 -0
- bear_utils/extras/_tools.py +185 -0
- bear_utils/extras/_zapper.py +399 -0
- bear_utils/extras/platform_utils.py +57 -0
- bear_utils/extras/responses/__init__.py +5 -0
- bear_utils/extras/responses/function_response.py +451 -0
- bear_utils/extras/wrappers/__init__.py +1 -0
- bear_utils/extras/wrappers/add_methods.py +100 -0
- bear_utils/extras/wrappers/string_io.py +46 -0
- bear_utils/files/__init__.py +6 -0
- bear_utils/files/file_handlers/__init__.py +5 -0
- bear_utils/files/file_handlers/_base_file_handler.py +107 -0
- bear_utils/files/file_handlers/file_handler_factory.py +280 -0
- bear_utils/files/file_handlers/json_file_handler.py +71 -0
- bear_utils/files/file_handlers/log_file_handler.py +40 -0
- bear_utils/files/file_handlers/toml_file_handler.py +76 -0
- bear_utils/files/file_handlers/txt_file_handler.py +76 -0
- bear_utils/files/file_handlers/yaml_file_handler.py +64 -0
- bear_utils/files/ignore_parser.py +293 -0
- bear_utils/graphics/__init__.py +6 -0
- bear_utils/graphics/bear_gradient.py +145 -0
- bear_utils/graphics/font/__init__.py +13 -0
- bear_utils/graphics/font/_raw_block_letters.py +463 -0
- bear_utils/graphics/font/_theme.py +31 -0
- bear_utils/graphics/font/_utils.py +220 -0
- bear_utils/graphics/font/block_font.py +192 -0
- bear_utils/graphics/font/glitch_font.py +63 -0
- bear_utils/graphics/image_helpers.py +45 -0
- bear_utils/gui/__init__.py +8 -0
- bear_utils/gui/gui_tools/__init__.py +10 -0
- bear_utils/gui/gui_tools/_settings.py +36 -0
- bear_utils/gui/gui_tools/_types.py +12 -0
- bear_utils/gui/gui_tools/qt_app.py +150 -0
- bear_utils/gui/gui_tools/qt_color_picker.py +130 -0
- bear_utils/gui/gui_tools/qt_file_handler.py +130 -0
- bear_utils/gui/gui_tools/qt_input_dialog.py +303 -0
- bear_utils/logger_manager/__init__.py +109 -0
- bear_utils/logger_manager/_common.py +63 -0
- bear_utils/logger_manager/_console_junk.py +135 -0
- bear_utils/logger_manager/_log_level.py +50 -0
- bear_utils/logger_manager/_styles.py +95 -0
- bear_utils/logger_manager/logger_protocol.py +42 -0
- bear_utils/logger_manager/loggers/__init__.py +1 -0
- bear_utils/logger_manager/loggers/_console.py +223 -0
- bear_utils/logger_manager/loggers/_level_sin.py +61 -0
- bear_utils/logger_manager/loggers/_logger.py +19 -0
- bear_utils/logger_manager/loggers/base_logger.py +244 -0
- bear_utils/logger_manager/loggers/base_logger.pyi +51 -0
- bear_utils/logger_manager/loggers/basic_logger/__init__.py +5 -0
- bear_utils/logger_manager/loggers/basic_logger/logger.py +80 -0
- bear_utils/logger_manager/loggers/basic_logger/logger.pyi +19 -0
- bear_utils/logger_manager/loggers/buffer_logger.py +57 -0
- bear_utils/logger_manager/loggers/console_logger.py +278 -0
- bear_utils/logger_manager/loggers/console_logger.pyi +50 -0
- bear_utils/logger_manager/loggers/fastapi_logger.py +333 -0
- bear_utils/logger_manager/loggers/file_logger.py +151 -0
- bear_utils/logger_manager/loggers/simple_logger.py +98 -0
- bear_utils/logger_manager/loggers/sub_logger.py +105 -0
- bear_utils/logger_manager/loggers/sub_logger.pyi +23 -0
- bear_utils/monitoring/__init__.py +13 -0
- bear_utils/monitoring/_common.py +28 -0
- bear_utils/monitoring/host_monitor.py +346 -0
- bear_utils/time/__init__.py +59 -0
- bear_utils-0.0.1.dist-info/METADATA +305 -0
- bear_utils-0.0.1.dist-info/RECORD +107 -0
- bear_utils-0.0.1.dist-info/WHEEL +4 -0
@@ -0,0 +1,192 @@
|
|
1
|
+
"""A dictionary containing ASCII art representations of letters and symbols in a block font style."""
|
2
|
+
|
3
|
+
from rich.align import Align
|
4
|
+
from rich.console import Console
|
5
|
+
|
6
|
+
from bear_utils.graphics.font import FontStyle
|
7
|
+
from bear_utils.graphics.font._raw_block_letters import (
|
8
|
+
ASTERISK,
|
9
|
+
AT,
|
10
|
+
BACKWARD_SLASH,
|
11
|
+
COMMA,
|
12
|
+
DASH,
|
13
|
+
DOLLAR,
|
14
|
+
DOT,
|
15
|
+
EIGHT,
|
16
|
+
EQUALS,
|
17
|
+
EXCLAMATION,
|
18
|
+
FIVE,
|
19
|
+
FORWARD_SLASH,
|
20
|
+
FOUR,
|
21
|
+
HASH,
|
22
|
+
NINE,
|
23
|
+
ONE,
|
24
|
+
PLUS,
|
25
|
+
QUESTION,
|
26
|
+
SEVEN,
|
27
|
+
SIX,
|
28
|
+
SPACE,
|
29
|
+
THREE,
|
30
|
+
TWO,
|
31
|
+
UNDERSCORE,
|
32
|
+
ZERO,
|
33
|
+
A,
|
34
|
+
B,
|
35
|
+
C,
|
36
|
+
D,
|
37
|
+
E,
|
38
|
+
F,
|
39
|
+
G,
|
40
|
+
H,
|
41
|
+
I,
|
42
|
+
J,
|
43
|
+
K,
|
44
|
+
L,
|
45
|
+
M,
|
46
|
+
N,
|
47
|
+
O,
|
48
|
+
P,
|
49
|
+
Q,
|
50
|
+
R,
|
51
|
+
S,
|
52
|
+
T,
|
53
|
+
U,
|
54
|
+
V,
|
55
|
+
W,
|
56
|
+
X,
|
57
|
+
Y,
|
58
|
+
Z,
|
59
|
+
)
|
60
|
+
from bear_utils.graphics.font._theme import CyberTheme as Theme
|
61
|
+
from bear_utils.graphics.font._utils import random_style
|
62
|
+
|
63
|
+
BLOCK_LETTERS: dict[str, list[str]] = {
|
64
|
+
"A": A,
|
65
|
+
"B": B,
|
66
|
+
"C": C,
|
67
|
+
"D": D,
|
68
|
+
"E": E,
|
69
|
+
"F": F,
|
70
|
+
"G": G,
|
71
|
+
"H": H,
|
72
|
+
"I": I,
|
73
|
+
"J": J,
|
74
|
+
"K": K,
|
75
|
+
"L": L,
|
76
|
+
"M": M,
|
77
|
+
"N": N,
|
78
|
+
"O": O,
|
79
|
+
"P": P,
|
80
|
+
"Q": Q,
|
81
|
+
"R": R,
|
82
|
+
"S": S,
|
83
|
+
"T": T,
|
84
|
+
"U": U,
|
85
|
+
"V": V,
|
86
|
+
"W": W,
|
87
|
+
"X": X,
|
88
|
+
"Y": Y,
|
89
|
+
"Z": Z,
|
90
|
+
"0": ZERO,
|
91
|
+
"1": ONE,
|
92
|
+
"2": TWO,
|
93
|
+
"3": THREE,
|
94
|
+
"4": FOUR,
|
95
|
+
"5": FIVE,
|
96
|
+
"6": SIX,
|
97
|
+
"7": SEVEN,
|
98
|
+
"8": EIGHT,
|
99
|
+
"9": NINE,
|
100
|
+
" ": SPACE,
|
101
|
+
"!": EXCLAMATION,
|
102
|
+
"?": QUESTION,
|
103
|
+
".": DOT,
|
104
|
+
",": COMMA,
|
105
|
+
"-": DASH,
|
106
|
+
"_": UNDERSCORE,
|
107
|
+
"=": EQUALS,
|
108
|
+
"+": PLUS,
|
109
|
+
"*": ASTERISK,
|
110
|
+
"/": FORWARD_SLASH,
|
111
|
+
"\\": BACKWARD_SLASH,
|
112
|
+
"@": AT,
|
113
|
+
"#": HASH,
|
114
|
+
"$": DOLLAR,
|
115
|
+
}
|
116
|
+
|
117
|
+
console = Console()
|
118
|
+
|
119
|
+
|
120
|
+
def apply_block_style(block_rows: list[str], style: str = "solid") -> list[str]:
|
121
|
+
"""Replace block characters with different symbols."""
|
122
|
+
try:
|
123
|
+
new_char: FontStyle = FontStyle.get(value=style, default=FontStyle.SOLID)
|
124
|
+
return [row.replace(FontStyle.SOLID.text, new_char.text) for row in block_rows]
|
125
|
+
except (KeyError, AttributeError) as e:
|
126
|
+
available = ", ".join(FontStyle.keys())
|
127
|
+
raise ValueError(f"Invalid style: {style}. Available styles: {available}") from e
|
128
|
+
|
129
|
+
|
130
|
+
def char_to_block(char: str) -> list[str]:
|
131
|
+
"""Convert a single character to its block font representation."""
|
132
|
+
return BLOCK_LETTERS.get(char.upper(), [" "] * 5)
|
133
|
+
|
134
|
+
|
135
|
+
def _word_to_block(word: str) -> list[str]:
|
136
|
+
"""Convert a word to its block font representation."""
|
137
|
+
clean_text: str = "".join(char for char in word.upper() if char in BLOCK_LETTERS)
|
138
|
+
|
139
|
+
if not clean_text:
|
140
|
+
return ["No valid characters to block-ify! 🧱"]
|
141
|
+
|
142
|
+
rows = ["", "", "", "", ""]
|
143
|
+
for char in clean_text:
|
144
|
+
block_char = char_to_block(char)
|
145
|
+
for i in range(5):
|
146
|
+
rows[i] += block_char[i]
|
147
|
+
return rows
|
148
|
+
|
149
|
+
|
150
|
+
def word_to_block(word: str, font: str = "solid") -> str:
|
151
|
+
"""Convert a word to its block font representation as a single string.
|
152
|
+
|
153
|
+
Args:
|
154
|
+
word (str): The word to convert.
|
155
|
+
font (str): The style of the block font. Defaults to "solid".
|
156
|
+
|
157
|
+
Returns:
|
158
|
+
str: The block font representation of the word.
|
159
|
+
"""
|
160
|
+
block_rows = _word_to_block(word)
|
161
|
+
styled_rows = apply_block_style(block_rows, font)
|
162
|
+
return "\n".join(styled_rows)
|
163
|
+
|
164
|
+
|
165
|
+
def print_block_font(text: str, color: str = Theme.neon_green) -> None:
|
166
|
+
"""Print block font text with cyberpunk styling."""
|
167
|
+
block_rows = _word_to_block(text)
|
168
|
+
|
169
|
+
for row in block_rows:
|
170
|
+
console.print(Align.center(f"[{color}]{row}[/{color}]"))
|
171
|
+
|
172
|
+
|
173
|
+
def show_off_styles(word: str, style: str | None = None) -> None:
|
174
|
+
"""Display all block styles by using an example word"""
|
175
|
+
console.print("Available block styles:")
|
176
|
+
|
177
|
+
for symbol in FontStyle:
|
178
|
+
styled_word = word_to_block(word, font=symbol)
|
179
|
+
style = random_style()
|
180
|
+
|
181
|
+
console.print()
|
182
|
+
console.print(Align.center(f"[{Theme.system}]{symbol.title()} Style:[/]"))
|
183
|
+
console.print(Align.center(f"[{style}]{styled_word}[/]"))
|
184
|
+
console.print()
|
185
|
+
|
186
|
+
|
187
|
+
__all__ = ["BLOCK_LETTERS", "char_to_block", "word_to_block"]
|
188
|
+
# fmt: on
|
189
|
+
|
190
|
+
if __name__ == "__main__":
|
191
|
+
WORD = "CLAIRE"
|
192
|
+
show_off_styles(WORD)
|
@@ -0,0 +1,63 @@
|
|
1
|
+
"""Ascii art glitch font generator for cyberpunk vibes!"""
|
2
|
+
|
3
|
+
from io import StringIO
|
4
|
+
import random
|
5
|
+
|
6
|
+
GLITCH_CHARS: list[str] = ["█", "▓", "░", "▒", "■", "□", "▪", "▫"]
|
7
|
+
|
8
|
+
|
9
|
+
def dice_roll(chance: float) -> bool:
|
10
|
+
"""Roll a dice with a given chance."""
|
11
|
+
return random.random() < chance # noqa: S311, we aren't doing crypto bro
|
12
|
+
|
13
|
+
|
14
|
+
def dice_roll_choice(choices: list[str], chance: float = 0.5) -> str:
|
15
|
+
"""Roll a dice to choose from a list of choices with a given chance."""
|
16
|
+
if dice_roll(chance):
|
17
|
+
return random.choice(choices) # noqa: S311, we aren't doing crypto bro
|
18
|
+
return ""
|
19
|
+
|
20
|
+
|
21
|
+
def glitch_font_generator(text: str, glitch_intensity: float = 0.3) -> str:
|
22
|
+
"""Generate beautifully corrupted glitch text with MIXED characters."""
|
23
|
+
output = StringIO()
|
24
|
+
|
25
|
+
for char in text.upper():
|
26
|
+
if char == " ":
|
27
|
+
output.write(" ")
|
28
|
+
continue
|
29
|
+
|
30
|
+
output.write(char)
|
31
|
+
symbol: str = dice_roll_choice(GLITCH_CHARS, glitch_intensity)
|
32
|
+
output.write(symbol)
|
33
|
+
symbol2: str = dice_roll_choice(GLITCH_CHARS, glitch_intensity * 0.4)
|
34
|
+
output.write(symbol2)
|
35
|
+
|
36
|
+
result: str = output.getvalue()
|
37
|
+
output.close()
|
38
|
+
return result
|
39
|
+
|
40
|
+
|
41
|
+
def multi_line_glitch(*lines: str, base_intensity: float = 0.3) -> str:
|
42
|
+
"""Generate glitch effects for multiple lines with StringIO magic."""
|
43
|
+
output = StringIO()
|
44
|
+
|
45
|
+
for i, line in enumerate(lines):
|
46
|
+
intensity: float = base_intensity + (i * 0.1)
|
47
|
+
glitched_line: str = glitch_font_generator(line, intensity)
|
48
|
+
output.write(glitched_line)
|
49
|
+
|
50
|
+
if i < len(lines) - 1:
|
51
|
+
output.write("\n")
|
52
|
+
|
53
|
+
result = output.getvalue()
|
54
|
+
output.close()
|
55
|
+
return result
|
56
|
+
|
57
|
+
|
58
|
+
def cyberpunk_glitch_font(*text: str, style: str = "heavy") -> str:
|
59
|
+
"""Different glitch styles for maximum chaos."""
|
60
|
+
styles = {"light": 0.2, "medium": 0.4, "heavy": 0.6, "corrupted": 0.8}
|
61
|
+
|
62
|
+
intensity: float = styles.get(style, 0.4)
|
63
|
+
return multi_line_glitch(*text, base_intensity=intensity)
|
@@ -0,0 +1,45 @@
|
|
1
|
+
"""A module for image processing utilities, including encoding images to JPEG and PNG formats, and converting WebP images to JPEG."""
|
2
|
+
|
3
|
+
import base64
|
4
|
+
from io import BytesIO
|
5
|
+
from pathlib import Path
|
6
|
+
from typing import TYPE_CHECKING
|
7
|
+
|
8
|
+
from PIL import Image
|
9
|
+
|
10
|
+
if TYPE_CHECKING:
|
11
|
+
from PIL.Image import Image as PILImage
|
12
|
+
|
13
|
+
|
14
|
+
def encode_image_to_jpeg(image_path: Path, max_size: int = 1024, jpeg_quality: int = 75) -> str:
|
15
|
+
"""Resize image to optimize for token usage"""
|
16
|
+
image = Image.open(image_path)
|
17
|
+
if max(image.size) > max_size:
|
18
|
+
image.thumbnail((max_size, max_size))
|
19
|
+
buffered = BytesIO()
|
20
|
+
if image.format != "JPEG":
|
21
|
+
image: PILImage = image.convert("RGB")
|
22
|
+
image.save(buffered, format="JPEG", quality=jpeg_quality)
|
23
|
+
return base64.b64encode(buffered.getvalue()).decode("utf-8")
|
24
|
+
|
25
|
+
|
26
|
+
def encode_image_to_png(image_path: Path, max_size: int = 1024) -> str:
|
27
|
+
"""Resize image to optimize for token usage"""
|
28
|
+
image = Image.open(image_path)
|
29
|
+
if max(image.size) > max_size:
|
30
|
+
image.thumbnail((max_size, max_size))
|
31
|
+
buffered = BytesIO()
|
32
|
+
if image.format != "PNG":
|
33
|
+
image: PILImage = image.convert("RGBA")
|
34
|
+
image.save(buffered, format="PNG")
|
35
|
+
return base64.b64encode(buffered.getvalue()).decode("utf-8")
|
36
|
+
|
37
|
+
|
38
|
+
def convert_webp_to_jpeg(image_path: Path, jpeg_quality: int = 95) -> str:
|
39
|
+
"""Convert a WebP image to JPEG format."""
|
40
|
+
image = Image.open(image_path)
|
41
|
+
buffered = BytesIO()
|
42
|
+
if image.format != "JPEG":
|
43
|
+
image: PILImage = image.convert("RGB")
|
44
|
+
image.save(buffered, format="JPEG", quality=jpeg_quality)
|
45
|
+
return base64.b64encode(buffered.getvalue()).decode("utf-8")
|
@@ -0,0 +1,8 @@
|
|
1
|
+
"""A module for GUI-related utilities using PyQt6. Optional Module."""
|
2
|
+
|
3
|
+
try:
|
4
|
+
from .gui_tools import QTApplication, get_text, select_color
|
5
|
+
|
6
|
+
__all__ = ["QTApplication", "get_text", "select_color"]
|
7
|
+
except ImportError as e:
|
8
|
+
raise ImportError("PyQt6 is required for GUI functionality. Install it with: uv pip install bear-utils[gui]") from e
|
@@ -0,0 +1,10 @@
|
|
1
|
+
"""A module for managing a PyQt6 application instance and providing utility methods for dialogs and menus."""
|
2
|
+
|
3
|
+
try:
|
4
|
+
from .qt_app import QTApplication
|
5
|
+
from .qt_color_picker import select_color
|
6
|
+
from .qt_input_dialog import get_text
|
7
|
+
|
8
|
+
__all__ = ["QTApplication", "get_text", "select_color"]
|
9
|
+
except ImportError as e:
|
10
|
+
raise ImportError("PyQt6 is required for GUI functionality. Install it with: pip install bear-utils[gui]") from e
|
@@ -0,0 +1,36 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
from PyQt6.QtCore import QSettings
|
4
|
+
|
5
|
+
from .qt_app import QTApplication
|
6
|
+
|
7
|
+
|
8
|
+
class Settings(QTApplication):
|
9
|
+
"""Settings class that inherits from QTApplication for standalone use."""
|
10
|
+
|
11
|
+
def __init__(self, app_name: str = "Settings App", org_name: str = "YourOrg", org_domain: str = "org.domain"):
|
12
|
+
super().__init__(app_name, org_name, org_domain)
|
13
|
+
|
14
|
+
self._settings = QSettings(org_name, app_name)
|
15
|
+
|
16
|
+
def get(self, key: str, default: Any = None, value_type: type | None = None) -> Any:
|
17
|
+
"""Get a setting value with optional type conversion."""
|
18
|
+
if value_type:
|
19
|
+
return self._settings.value(key, default, type=value_type)
|
20
|
+
return self._settings.value(key, default)
|
21
|
+
|
22
|
+
def set(self, key: str, value: Any) -> None:
|
23
|
+
"""Set a setting value."""
|
24
|
+
self._settings.setValue(key, value)
|
25
|
+
|
26
|
+
def has(self, key: str) -> bool:
|
27
|
+
"""Check if a setting exists."""
|
28
|
+
return self._settings.contains(key)
|
29
|
+
|
30
|
+
def remove_key(self, key: str) -> None:
|
31
|
+
"""Remove a setting."""
|
32
|
+
self._settings.remove(key)
|
33
|
+
|
34
|
+
def clear_settings(self) -> None:
|
35
|
+
"""Clear all settings."""
|
36
|
+
self._settings.clear()
|
@@ -0,0 +1,150 @@
|
|
1
|
+
"""A module for managing a PyQt6 application instance and providing utility methods for dialogs and menus."""
|
2
|
+
|
3
|
+
import atexit
|
4
|
+
from collections.abc import Callable
|
5
|
+
from pathlib import Path
|
6
|
+
import sys
|
7
|
+
|
8
|
+
from PyQt6.QtCore import QCoreApplication, QObject, Qt
|
9
|
+
from PyQt6.QtGui import QAction, QIcon, QKeySequence, QShortcut
|
10
|
+
from PyQt6.QtWidgets import QApplication, QDialog, QLabel, QMenu, QMenuBar, QMessageBox, QVBoxLayout
|
11
|
+
|
12
|
+
from bear_utils.logger_manager import VERBOSE, ConsoleLogger
|
13
|
+
|
14
|
+
from ._types import ActionHolder
|
15
|
+
|
16
|
+
|
17
|
+
class QTApplication(QObject):
|
18
|
+
"""Singleton class to manage the QApplication instance.
|
19
|
+
|
20
|
+
This ensures that only one instance of QApplication is created.
|
21
|
+
"""
|
22
|
+
|
23
|
+
def __init__(
|
24
|
+
self,
|
25
|
+
app_name: str = "Qt Application",
|
26
|
+
org_name: str = "Organization",
|
27
|
+
org_domain: str = "org.domain",
|
28
|
+
) -> None:
|
29
|
+
"""Initialize the QTApplication instance."""
|
30
|
+
super().__init__()
|
31
|
+
if not QApplication.instance():
|
32
|
+
self.app: QCoreApplication | None = QApplication(sys.argv)
|
33
|
+
if self.app:
|
34
|
+
self.app.setApplicationName(app_name)
|
35
|
+
self.app.setOrganizationName(org_name)
|
36
|
+
self.app.setOrganizationDomain(org_domain)
|
37
|
+
else:
|
38
|
+
self.app = QApplication.instance()
|
39
|
+
self.console: ConsoleLogger = ConsoleLogger.get_instance(init=True, name=app_name, level=VERBOSE)
|
40
|
+
atexit.register(self.cleanup)
|
41
|
+
|
42
|
+
def _default_exit_shortcuts(self) -> None:
|
43
|
+
"""Set up default exit shortcuts for the application."""
|
44
|
+
self._add_shortcut(QKeySequence("Escape"), self.cleanup)
|
45
|
+
|
46
|
+
def _add_shortcut(self, shortcut: QKeySequence | QKeySequence.StandardKey, callback: Callable) -> None:
|
47
|
+
"""Add a shortcut to the application."""
|
48
|
+
q_shortcut = QShortcut(shortcut, self.dialog)
|
49
|
+
q_shortcut.activated.connect(callback)
|
50
|
+
|
51
|
+
def _add_to_menu(self, menu_name: str, actions: list[ActionHolder]) -> QMenu:
|
52
|
+
"""Add an action to the menu."""
|
53
|
+
menu = QMenu(menu_name, self.dialog)
|
54
|
+
for a in actions:
|
55
|
+
action: QAction = self._add_action(text=a.text, shortcut=a.shortcut, callback=a.callback)
|
56
|
+
menu.addAction(action)
|
57
|
+
return menu
|
58
|
+
|
59
|
+
def _add_action(self, text: str, shortcut: str, callback: Callable) -> QAction:
|
60
|
+
"""Create and return an action for the menu."""
|
61
|
+
action = QAction(text, self.dialog)
|
62
|
+
action.setShortcut(shortcut)
|
63
|
+
action.triggered.connect(callback)
|
64
|
+
return action
|
65
|
+
|
66
|
+
def _start_menu_bar(self) -> None:
|
67
|
+
"""Create and setup the menu bar."""
|
68
|
+
self.menu_bar = QMenuBar(self.dialog)
|
69
|
+
|
70
|
+
def _end_menu_bar(self, menus_to_add: list[QMenu]) -> None:
|
71
|
+
for menu in menus_to_add:
|
72
|
+
self.menu_bar.addMenu(menu)
|
73
|
+
self.main_layout.setMenuBar(self.menu_bar)
|
74
|
+
|
75
|
+
def _setup_initial_window(self, title: str, icon_path: Path, width: int, height: int) -> None:
|
76
|
+
"""Create and show the initial window with loading indicator."""
|
77
|
+
self.dialog = QDialog(None)
|
78
|
+
self.dialog.setWindowTitle(title)
|
79
|
+
self.dialog.setMinimumSize(width, height)
|
80
|
+
|
81
|
+
if icon_path.exists():
|
82
|
+
self.dialog.setWindowIcon(QIcon(str(icon_path)))
|
83
|
+
|
84
|
+
self.main_layout = QVBoxLayout(self.dialog)
|
85
|
+
self.loading_label = QLabel("Loading...")
|
86
|
+
self.main_layout.addWidget(self.loading_label)
|
87
|
+
|
88
|
+
def get_app(self) -> QApplication | QCoreApplication | None:
|
89
|
+
"""Get the current QApplication instance."""
|
90
|
+
if not self.app and not QApplication.instance():
|
91
|
+
self.app = QApplication(sys.argv)
|
92
|
+
elif not self.app:
|
93
|
+
self.app: QCoreApplication | None = QApplication.instance()
|
94
|
+
return self.app
|
95
|
+
|
96
|
+
def show_message(
|
97
|
+
self,
|
98
|
+
message: str,
|
99
|
+
title: str = "Message",
|
100
|
+
icon: QMessageBox.Icon = QMessageBox.Icon.Information,
|
101
|
+
on_ok_action: Callable[[], None] | None = None,
|
102
|
+
) -> None:
|
103
|
+
"""Show a message dialog with configurable icon and action.
|
104
|
+
|
105
|
+
Args:
|
106
|
+
message: The message to display
|
107
|
+
title: Dialog title
|
108
|
+
icon: Message box icon (Information, Warning, Critical, Question)
|
109
|
+
on_ok_action: Function to call when OK is clicked
|
110
|
+
"""
|
111
|
+
msg_box = QMessageBox()
|
112
|
+
msg_box.setIcon(icon)
|
113
|
+
msg_box.setWindowTitle(title)
|
114
|
+
msg_box.setText(message)
|
115
|
+
msg_box.setStandardButtons(QMessageBox.StandardButton.Ok)
|
116
|
+
msg_box.setWindowModality(Qt.WindowModality.ApplicationModal)
|
117
|
+
|
118
|
+
result = msg_box.exec()
|
119
|
+
|
120
|
+
if result == QMessageBox.StandardButton.Ok and on_ok_action:
|
121
|
+
on_ok_action()
|
122
|
+
|
123
|
+
def show_warning(
|
124
|
+
self, message: str, on_ok_action: Callable[[], None] | None = None, title: str = "Warning"
|
125
|
+
) -> None:
|
126
|
+
"""Show a warning dialog."""
|
127
|
+
self.show_message(message, title=title, icon=QMessageBox.Icon.Warning, on_ok_action=on_ok_action)
|
128
|
+
|
129
|
+
def show_error(self, message: str, on_ok_action: Callable[[], None] | None = None, title: str = "Error") -> None:
|
130
|
+
"""Show an error dialog."""
|
131
|
+
self.show_message(message, title=title, icon=QMessageBox.Icon.Critical, on_ok_action=on_ok_action)
|
132
|
+
|
133
|
+
def show_info(self, message: str, title: str = "Information") -> None:
|
134
|
+
"""Show an information dialog."""
|
135
|
+
self.show_message(message, title=title, icon=QMessageBox.Icon.Information)
|
136
|
+
|
137
|
+
def cleanup(self) -> None:
|
138
|
+
"""Clean up the QTApplication instance."""
|
139
|
+
if self.app:
|
140
|
+
self.console.verbose("Cleaning up QTApplication instance.")
|
141
|
+
self.app.quit()
|
142
|
+
self.app = None
|
143
|
+
|
144
|
+
|
145
|
+
if __name__ == "__main__":
|
146
|
+
qt_app = QTApplication()
|
147
|
+
qt_app.show_info("This is an info message.")
|
148
|
+
qt_app.show_warning("This is a warning message.")
|
149
|
+
qt_app.show_error("This is an error message.")
|
150
|
+
qt_app.cleanup()
|
@@ -0,0 +1,130 @@
|
|
1
|
+
"""A module for a color picker dialog using PyQt6."""
|
2
|
+
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
from PyQt6.QtGui import QColor
|
7
|
+
from PyQt6.QtWidgets import QColorDialog
|
8
|
+
from rich.color_triplet import ColorTriplet
|
9
|
+
|
10
|
+
from .qt_app import QTApplication
|
11
|
+
|
12
|
+
|
13
|
+
@dataclass
|
14
|
+
class ColorInfo:
|
15
|
+
"""Data class to hold color information."""
|
16
|
+
|
17
|
+
qcolor: QColor
|
18
|
+
hex: str
|
19
|
+
rgb: ColorTriplet
|
20
|
+
rgba: tuple[int, int, int, int]
|
21
|
+
hsv: tuple[int, int, int]
|
22
|
+
|
23
|
+
|
24
|
+
class QTColorPicker(QTApplication):
|
25
|
+
"""Singleton class to manage the color picker dialog."""
|
26
|
+
|
27
|
+
def select_color(
|
28
|
+
self,
|
29
|
+
initial_color: str | None = None,
|
30
|
+
title: str = "Select Color",
|
31
|
+
options: Any | None = None,
|
32
|
+
) -> ColorInfo | None:
|
33
|
+
"""Shows a color selection dialog and returns the selected color.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
initial_color: Initial color to show in the dialog. Can be:
|
37
|
+
- QColor object
|
38
|
+
- Hex string (e.g., "#FF5733")
|
39
|
+
- RGB tuple (e.g., (255, 87, 51))
|
40
|
+
title (str): The dialog window title
|
41
|
+
options: QColorDialog options (optional)
|
42
|
+
|
43
|
+
Returns:
|
44
|
+
dict: Color information with keys:
|
45
|
+
- 'qcolor': QColor object
|
46
|
+
- 'hex': Hex string (e.g., "#FF5733")
|
47
|
+
- 'rgb': RGB tuple (e.g., (255, 87, 51))
|
48
|
+
- 'rgba': RGBA tuple (e.g., (255, 87, 51, 255))
|
49
|
+
- 'hsv': HSV tuple (e.g., (16, 80, 100))
|
50
|
+
Or None if dialog was canceled
|
51
|
+
"""
|
52
|
+
try:
|
53
|
+
dialog = QColorDialog()
|
54
|
+
channels_num = 3
|
55
|
+
if title:
|
56
|
+
dialog.setWindowTitle(title)
|
57
|
+
|
58
|
+
if options:
|
59
|
+
dialog.setOptions(options)
|
60
|
+
|
61
|
+
if initial_color:
|
62
|
+
if isinstance(initial_color, QColor):
|
63
|
+
dialog.setCurrentColor(initial_color)
|
64
|
+
elif isinstance(initial_color, str) and initial_color.startswith("#"):
|
65
|
+
dialog.setCurrentColor(QColor(initial_color))
|
66
|
+
elif isinstance(initial_color, tuple) and len(initial_color) >= channels_num:
|
67
|
+
r, g, b = initial_color[:channels_num]
|
68
|
+
a = initial_color[channels_num] if len(initial_color) > channels_num else 255
|
69
|
+
dialog.setCurrentColor(QColor(int(r), int(g), int(b), int(a)))
|
70
|
+
|
71
|
+
if dialog.exec() == QColorDialog.DialogCode.Accepted:
|
72
|
+
selected_color = dialog.selectedColor()
|
73
|
+
|
74
|
+
if not selected_color.isValid():
|
75
|
+
return None
|
76
|
+
|
77
|
+
return ColorInfo(
|
78
|
+
qcolor=selected_color,
|
79
|
+
hex=selected_color.name(),
|
80
|
+
rgb=ColorTriplet(selected_color.red(), selected_color.green(), selected_color.blue()),
|
81
|
+
rgba=(
|
82
|
+
selected_color.red(),
|
83
|
+
selected_color.green(),
|
84
|
+
selected_color.blue(),
|
85
|
+
selected_color.alpha(),
|
86
|
+
),
|
87
|
+
hsv=(selected_color.hue(), selected_color.saturation(), selected_color.value()),
|
88
|
+
)
|
89
|
+
return None
|
90
|
+
except Exception as e:
|
91
|
+
self.console.error(f"Error in color selection dialog: {e}")
|
92
|
+
return None
|
93
|
+
|
94
|
+
|
95
|
+
def select_color(
|
96
|
+
initial_color: str | None = None,
|
97
|
+
title: str = "Select Color",
|
98
|
+
options: Any | None = None,
|
99
|
+
) -> ColorInfo | None:
|
100
|
+
"""Select a color using the QTColorPicker singleton instance.
|
101
|
+
|
102
|
+
Args:
|
103
|
+
initial_color: Initial color to show in the dialog. Can be:
|
104
|
+
- QColor object
|
105
|
+
- Hex string (e.g., "#FF5733")
|
106
|
+
- RGB tuple (e.g., (255, 87, 51))
|
107
|
+
title (str): The dialog window title
|
108
|
+
options: QColorDialog options (optional)
|
109
|
+
|
110
|
+
Returns:
|
111
|
+
dict: Color information with keys:
|
112
|
+
- 'qcolor': QColor object
|
113
|
+
- 'hex': Hex string (e.g., "#FF5733")
|
114
|
+
- 'rgb': RGB tuple (e.g., (255, 87, 51))
|
115
|
+
- 'rgba': RGBA tuple (e.g., (255, 87, 51, 255))
|
116
|
+
- 'hsv': HSV tuple (e.g., (16, 80, 100))
|
117
|
+
Or None if dialog was canceled
|
118
|
+
"""
|
119
|
+
qt_color_picker = QTColorPicker()
|
120
|
+
return qt_color_picker.select_color(initial_color, title, options)
|
121
|
+
|
122
|
+
|
123
|
+
if __name__ == "__main__":
|
124
|
+
# Example usage
|
125
|
+
color_picker = QTColorPicker()
|
126
|
+
selected_color: ColorInfo | None = color_picker.select_color(initial_color="#FF5733", title="Choose a Color")
|
127
|
+
if selected_color:
|
128
|
+
color_picker.console.info(f"Selected Color: {selected_color}")
|
129
|
+
else:
|
130
|
+
color_picker.console.warning("No color selected.")
|