curses-themes 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.
- curses_themes/__init__.py +110 -0
- curses_themes/colors.py +324 -0
- curses_themes/manager.py +372 -0
- curses_themes/theme.py +468 -0
- curses_themes/theme3d.py +499 -0
- curses_themes/themes/__init__.py +45 -0
- curses_themes/themes/borland3d.py +362 -0
- curses_themes/themes/dark.py +125 -0
- curses_themes/themes/dbase3.py +169 -0
- curses_themes/themes/dbase4.py +180 -0
- curses_themes/themes/dbase4_3d.py +294 -0
- curses_themes/themes/default.py +125 -0
- curses_themes/themes/dos.py +179 -0
- curses_themes/themes/light.py +124 -0
- curses_themes/themes/ti994a.py +136 -0
- curses_themes/themes/trs80.py +153 -0
- curses_themes-0.1.dist-info/METADATA +337 -0
- curses_themes-0.1.dist-info/RECORD +21 -0
- curses_themes-0.1.dist-info/WHEEL +5 -0
- curses_themes-0.1.dist-info/licenses/LICENSE +23 -0
- curses_themes-0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
curses-themes: Lightweight theme support for Python curses applications
|
|
4
|
+
|
|
5
|
+
Inspired by FlossWare curses-java, this library brings professional theme
|
|
6
|
+
support to Python's standard curses module with zero external dependencies.
|
|
7
|
+
|
|
8
|
+
Copyright (C) 2024 FlossWare
|
|
9
|
+
|
|
10
|
+
This program is free software: you can redistribute it and/or modify
|
|
11
|
+
it under the terms of the GNU General Public License as published by
|
|
12
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
13
|
+
(at your option) any later version.
|
|
14
|
+
|
|
15
|
+
This program is distributed in the hope that it will be useful,
|
|
16
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
17
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
18
|
+
GNU General Public License for more details.
|
|
19
|
+
|
|
20
|
+
You should have received a copy of the GNU General Public License
|
|
21
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
Basic usage with built-in theme::
|
|
25
|
+
|
|
26
|
+
import curses
|
|
27
|
+
from curses_themes import ThemeManager
|
|
28
|
+
|
|
29
|
+
def main(stdscr):
|
|
30
|
+
# Load and apply a theme
|
|
31
|
+
theme = ThemeManager.load('dracula')
|
|
32
|
+
theme.apply(stdscr)
|
|
33
|
+
|
|
34
|
+
# Use semantic colors
|
|
35
|
+
stdscr.addstr(0, 0, "Success!", theme.colors.success)
|
|
36
|
+
stdscr.addstr(1, 0, "Error!", theme.colors.error)
|
|
37
|
+
|
|
38
|
+
# Draw themed boxes
|
|
39
|
+
theme.draw_box(stdscr, 3, 2, 10, 40, title="My Panel")
|
|
40
|
+
|
|
41
|
+
stdscr.refresh()
|
|
42
|
+
stdscr.getch()
|
|
43
|
+
|
|
44
|
+
if __name__ == "__main__":
|
|
45
|
+
curses.wrapper(main)
|
|
46
|
+
|
|
47
|
+
Creating a custom theme::
|
|
48
|
+
|
|
49
|
+
from curses_themes import Theme, ThemeManager
|
|
50
|
+
|
|
51
|
+
class MyTheme(Theme):
|
|
52
|
+
def __init__(self):
|
|
53
|
+
super().__init__(
|
|
54
|
+
name="My Theme",
|
|
55
|
+
description="A custom theme",
|
|
56
|
+
author="Your Name"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def get_color_map(self):
|
|
60
|
+
return {
|
|
61
|
+
'background': (0, 0, 0),
|
|
62
|
+
'foreground': (255, 255, 255),
|
|
63
|
+
'primary': (0, 120, 215),
|
|
64
|
+
'success': (16, 124, 16),
|
|
65
|
+
'error': (232, 17, 35),
|
|
66
|
+
'warning': (193, 156, 0),
|
|
67
|
+
'info': (0, 120, 212),
|
|
68
|
+
'accent': (142, 68, 173),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Register and use
|
|
72
|
+
ThemeManager.register(MyTheme)
|
|
73
|
+
theme = ThemeManager.load('my-theme')
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
from .colors import ColorManager
|
|
77
|
+
from .manager import ThemeManager
|
|
78
|
+
from .theme import ColorPair, ComponentColors, SemanticColors, Theme
|
|
79
|
+
from .themes import (
|
|
80
|
+
DarkTheme,
|
|
81
|
+
DBase3Theme,
|
|
82
|
+
DBase4Theme,
|
|
83
|
+
DefaultTheme,
|
|
84
|
+
DOSTheme,
|
|
85
|
+
LightTheme,
|
|
86
|
+
TI994ATheme,
|
|
87
|
+
TRS80Theme,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
__version__ = "0.1"
|
|
91
|
+
__author__ = "FlossWare"
|
|
92
|
+
__license__ = "GPL-3.0"
|
|
93
|
+
|
|
94
|
+
__all__ = [
|
|
95
|
+
"ColorManager",
|
|
96
|
+
"ColorPair",
|
|
97
|
+
"ComponentColors",
|
|
98
|
+
"DBase3Theme",
|
|
99
|
+
"DBase4Theme",
|
|
100
|
+
"DOSTheme",
|
|
101
|
+
"DarkTheme",
|
|
102
|
+
"DefaultTheme",
|
|
103
|
+
"LightTheme",
|
|
104
|
+
"SemanticColors",
|
|
105
|
+
"TI994ATheme",
|
|
106
|
+
"TRS80Theme",
|
|
107
|
+
"Theme",
|
|
108
|
+
"ThemeManager",
|
|
109
|
+
"__version__",
|
|
110
|
+
]
|
curses_themes/colors.py
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Color management and terminal capability detection for curses themes.
|
|
4
|
+
|
|
5
|
+
This module handles the complexities of terminal color support, including
|
|
6
|
+
detection of 8/16/256 color capabilities and RGB-to-palette conversion.
|
|
7
|
+
|
|
8
|
+
Copyright (C) 2024 FlossWare
|
|
9
|
+
|
|
10
|
+
This program is free software: you can redistribute it and/or modify
|
|
11
|
+
it under the terms of the GNU General Public License as published by
|
|
12
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
13
|
+
(at your option) any later version.
|
|
14
|
+
|
|
15
|
+
This program is distributed in the hope that it will be useful,
|
|
16
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
17
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
18
|
+
GNU General Public License for more details.
|
|
19
|
+
|
|
20
|
+
You should have received a copy of the GNU General Public License
|
|
21
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import curses
|
|
25
|
+
from typing import Optional
|
|
26
|
+
|
|
27
|
+
from .theme import ComponentColors, SemanticColors
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ColorManager:
|
|
31
|
+
"""
|
|
32
|
+
Manages color initialization and terminal capability detection.
|
|
33
|
+
|
|
34
|
+
This class handles the complexities of working with different terminal
|
|
35
|
+
color capabilities (8, 16, or 256 colors) and converts RGB values to
|
|
36
|
+
appropriate curses color pairs.
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
```python
|
|
40
|
+
def main(stdscr):
|
|
41
|
+
manager = ColorManager(stdscr)
|
|
42
|
+
colors = manager.initialize_theme(my_theme)
|
|
43
|
+
stdscr.addstr(0, 0, "Hello", curses.color_pair(colors.primary))
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Attributes:
|
|
47
|
+
stdscr: The curses window object
|
|
48
|
+
color_count: Number of colors supported (8, 16, or 256)
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
# Standard 8-color palette (ANSI colors)
|
|
52
|
+
BASIC_COLORS = {
|
|
53
|
+
curses.COLOR_BLACK: (0, 0, 0),
|
|
54
|
+
curses.COLOR_RED: (205, 0, 0),
|
|
55
|
+
curses.COLOR_GREEN: (0, 205, 0),
|
|
56
|
+
curses.COLOR_YELLOW: (205, 205, 0),
|
|
57
|
+
curses.COLOR_BLUE: (0, 0, 238),
|
|
58
|
+
curses.COLOR_MAGENTA: (205, 0, 205),
|
|
59
|
+
curses.COLOR_CYAN: (0, 205, 205),
|
|
60
|
+
curses.COLOR_WHITE: (229, 229, 229),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Next color pair number to allocate
|
|
64
|
+
_next_pair = 1
|
|
65
|
+
|
|
66
|
+
def __init__(self, stdscr):
|
|
67
|
+
"""
|
|
68
|
+
Initialize color manager for a curses screen.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
stdscr: Curses window object (typically from curses.wrapper)
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
RuntimeError: If the terminal doesn't support colors
|
|
75
|
+
"""
|
|
76
|
+
self.stdscr = stdscr
|
|
77
|
+
|
|
78
|
+
if not curses.has_colors():
|
|
79
|
+
raise RuntimeError(
|
|
80
|
+
"Terminal does not support colors. "
|
|
81
|
+
"Please use a color-capable terminal emulator."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
curses.start_color()
|
|
85
|
+
curses.use_default_colors()
|
|
86
|
+
|
|
87
|
+
self.color_count = self._detect_color_capability()
|
|
88
|
+
|
|
89
|
+
def _detect_color_capability(self) -> int:
|
|
90
|
+
"""
|
|
91
|
+
Detect terminal color support level.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Number of colors supported: 8, 16, or 256
|
|
95
|
+
|
|
96
|
+
Note:
|
|
97
|
+
Some terminals report more colors than they actually support well.
|
|
98
|
+
This method provides conservative detection.
|
|
99
|
+
"""
|
|
100
|
+
max_colors = curses.COLORS
|
|
101
|
+
|
|
102
|
+
if max_colors >= 256:
|
|
103
|
+
return 256
|
|
104
|
+
elif max_colors >= 16:
|
|
105
|
+
return 16
|
|
106
|
+
else:
|
|
107
|
+
return 8
|
|
108
|
+
|
|
109
|
+
def _rgb_to_curses_color(self, r: int, g: int, b: int) -> int:
|
|
110
|
+
"""
|
|
111
|
+
Convert RGB values to a curses color number.
|
|
112
|
+
|
|
113
|
+
Adapts to terminal capabilities by mapping to 256-color palette,
|
|
114
|
+
16-color palette, or 8-color palette as appropriate.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
r: Red component (0-255)
|
|
118
|
+
g: Green component (0-255)
|
|
119
|
+
b: Blue component (0-255)
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Curses color number appropriate for terminal capability
|
|
123
|
+
"""
|
|
124
|
+
if self.color_count >= 256:
|
|
125
|
+
return self._rgb_to_256(r, g, b)
|
|
126
|
+
else:
|
|
127
|
+
return self._rgb_to_basic(r, g, b)
|
|
128
|
+
|
|
129
|
+
def _rgb_to_256(self, r: int, g: int, b: int) -> int:
|
|
130
|
+
"""
|
|
131
|
+
Map RGB to 256-color palette.
|
|
132
|
+
|
|
133
|
+
Uses the standard xterm 256-color palette:
|
|
134
|
+
- Colors 0-15: System colors
|
|
135
|
+
- Colors 16-231: 6x6x6 RGB cube
|
|
136
|
+
- Colors 232-255: Grayscale ramp
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
r: Red component (0-255)
|
|
140
|
+
g: Green component (0-255)
|
|
141
|
+
b: Blue component (0-255)
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Color number in range 0-255
|
|
145
|
+
"""
|
|
146
|
+
# Check if it's a grayscale color
|
|
147
|
+
if r == g == b:
|
|
148
|
+
# Use grayscale ramp (232-255)
|
|
149
|
+
if r < 8:
|
|
150
|
+
return 16 # Black
|
|
151
|
+
if r > 247:
|
|
152
|
+
return 231 # White
|
|
153
|
+
return 232 + (r - 8) // 10
|
|
154
|
+
|
|
155
|
+
# Map to 6x6x6 RGB cube (16-231)
|
|
156
|
+
# Each component mapped to 0-5
|
|
157
|
+
r_index = (r * 6) // 256
|
|
158
|
+
g_index = (g * 6) // 256
|
|
159
|
+
b_index = (b * 6) // 256
|
|
160
|
+
|
|
161
|
+
return 16 + (36 * r_index) + (6 * g_index) + b_index
|
|
162
|
+
|
|
163
|
+
def _rgb_to_basic(self, r: int, g: int, b: int) -> int:
|
|
164
|
+
"""
|
|
165
|
+
Map RGB to 8 or 16 basic ANSI colors.
|
|
166
|
+
|
|
167
|
+
Finds the closest match in the basic color palette using
|
|
168
|
+
simple Euclidean distance in RGB space.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
r: Red component (0-255)
|
|
172
|
+
g: Green component (0-255)
|
|
173
|
+
b: Blue component (0-255)
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Basic curses color constant (COLOR_BLACK, COLOR_RED, etc.)
|
|
177
|
+
"""
|
|
178
|
+
min_distance = float("inf")
|
|
179
|
+
closest_color = curses.COLOR_WHITE
|
|
180
|
+
|
|
181
|
+
for color, (cr, cg, cb) in self.BASIC_COLORS.items():
|
|
182
|
+
# Euclidean distance in RGB space
|
|
183
|
+
distance = ((r - cr) ** 2 + (g - cg) ** 2 + (b - cb) ** 2) ** 0.5
|
|
184
|
+
|
|
185
|
+
if distance < min_distance:
|
|
186
|
+
min_distance = distance
|
|
187
|
+
closest_color = color
|
|
188
|
+
|
|
189
|
+
return closest_color
|
|
190
|
+
|
|
191
|
+
def _init_color_pair(
|
|
192
|
+
self,
|
|
193
|
+
fg_rgb: tuple[int, int, int],
|
|
194
|
+
bg_rgb: Optional[tuple[int, int, int]] = None,
|
|
195
|
+
) -> int:
|
|
196
|
+
"""
|
|
197
|
+
Initialize a curses color pair from RGB values.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
fg_rgb: Foreground RGB tuple (R, G, B)
|
|
201
|
+
bg_rgb: Background RGB tuple, or None for default background
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Color pair number that can be used with curses.color_pair()
|
|
205
|
+
"""
|
|
206
|
+
fg_color = self._rgb_to_curses_color(*fg_rgb)
|
|
207
|
+
|
|
208
|
+
if bg_rgb is None:
|
|
209
|
+
bg_color = -1 # Use default background
|
|
210
|
+
else:
|
|
211
|
+
bg_color = self._rgb_to_curses_color(*bg_rgb)
|
|
212
|
+
|
|
213
|
+
pair_num = ColorManager._next_pair
|
|
214
|
+
ColorManager._next_pair += 1
|
|
215
|
+
|
|
216
|
+
# Ensure we don't exceed curses color pair limit
|
|
217
|
+
if pair_num >= curses.COLOR_PAIRS:
|
|
218
|
+
raise RuntimeError(
|
|
219
|
+
f"Exceeded maximum color pairs ({curses.COLOR_PAIRS}). "
|
|
220
|
+
"Too many themes or colors in use."
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
curses.init_pair(pair_num, fg_color, bg_color)
|
|
225
|
+
except curses.error as e:
|
|
226
|
+
raise RuntimeError(
|
|
227
|
+
f"Failed to initialize color pair {pair_num} "
|
|
228
|
+
f"(fg={fg_color}, bg={bg_color}): {e}"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
return pair_num
|
|
232
|
+
|
|
233
|
+
def initialize_theme(self, theme) -> tuple[SemanticColors, ComponentColors]:
|
|
234
|
+
"""
|
|
235
|
+
Initialize all color pairs for a theme.
|
|
236
|
+
|
|
237
|
+
Converts the theme's RGB color map and component colors to curses
|
|
238
|
+
color pairs appropriate for the terminal's capabilities.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
theme: Theme instance with get_color_map() and component methods
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Tuple of (SemanticColors, ComponentColors) with initialized color pair numbers
|
|
245
|
+
|
|
246
|
+
Raises:
|
|
247
|
+
ValueError: If color map is missing required keys
|
|
248
|
+
RuntimeError: If color initialization fails
|
|
249
|
+
"""
|
|
250
|
+
color_map = theme.get_color_map()
|
|
251
|
+
|
|
252
|
+
# Validate required colors
|
|
253
|
+
required_colors = {
|
|
254
|
+
"background",
|
|
255
|
+
"foreground",
|
|
256
|
+
"primary",
|
|
257
|
+
"success",
|
|
258
|
+
"error",
|
|
259
|
+
"warning",
|
|
260
|
+
"info",
|
|
261
|
+
"accent",
|
|
262
|
+
}
|
|
263
|
+
missing = required_colors - set(color_map.keys())
|
|
264
|
+
if missing:
|
|
265
|
+
raise ValueError(f"Theme '{theme.name}' missing required colors: {missing}")
|
|
266
|
+
|
|
267
|
+
# Get background RGB for all pairs
|
|
268
|
+
bg_rgb = color_map["background"]
|
|
269
|
+
|
|
270
|
+
# Initialize color pairs for each semantic color
|
|
271
|
+
# Background pair uses default background for transparency
|
|
272
|
+
background_pair = self._init_color_pair(color_map["foreground"], bg_rgb)
|
|
273
|
+
|
|
274
|
+
# All other colors use the theme's background
|
|
275
|
+
semantic_colors = SemanticColors(
|
|
276
|
+
primary=self._init_color_pair(color_map["primary"], bg_rgb),
|
|
277
|
+
success=self._init_color_pair(color_map["success"], bg_rgb),
|
|
278
|
+
error=self._init_color_pair(color_map["error"], bg_rgb),
|
|
279
|
+
warning=self._init_color_pair(color_map["warning"], bg_rgb),
|
|
280
|
+
info=self._init_color_pair(color_map["info"], bg_rgb),
|
|
281
|
+
background=background_pair,
|
|
282
|
+
foreground=self._init_color_pair(color_map["foreground"], bg_rgb),
|
|
283
|
+
accent=self._init_color_pair(color_map["accent"], bg_rgb),
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Initialize component-based color pairs from theme methods
|
|
287
|
+
component_colors = ComponentColors(
|
|
288
|
+
background=self._init_color_pair_from_colorpair(theme.get_background()),
|
|
289
|
+
button=self._init_color_pair_from_colorpair(theme.get_button()),
|
|
290
|
+
button_focused=self._init_color_pair_from_colorpair(
|
|
291
|
+
theme.get_button_focused()
|
|
292
|
+
),
|
|
293
|
+
text_input=self._init_color_pair_from_colorpair(theme.get_text_input()),
|
|
294
|
+
border=self._init_color_pair_from_colorpair(theme.get_border()),
|
|
295
|
+
selection=self._init_color_pair_from_colorpair(theme.get_selection()),
|
|
296
|
+
disabled=self._init_color_pair_from_colorpair(theme.get_disabled()),
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
return semantic_colors, component_colors
|
|
300
|
+
|
|
301
|
+
def _init_color_pair_from_colorpair(self, color_pair) -> int:
|
|
302
|
+
"""
|
|
303
|
+
Initialize a curses color pair from a ColorPair object.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
color_pair: ColorPair with foreground and background RGB tuples
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
Color pair number that can be used with curses.color_pair()
|
|
310
|
+
"""
|
|
311
|
+
return self._init_color_pair(color_pair.foreground, color_pair.background)
|
|
312
|
+
|
|
313
|
+
def reset(self) -> None:
|
|
314
|
+
"""
|
|
315
|
+
Reset color pair counter.
|
|
316
|
+
|
|
317
|
+
This is primarily for testing. In normal use, color pairs persist
|
|
318
|
+
for the lifetime of the curses session.
|
|
319
|
+
"""
|
|
320
|
+
ColorManager._next_pair = 1
|
|
321
|
+
|
|
322
|
+
def __repr__(self) -> str:
|
|
323
|
+
"""String representation for debugging."""
|
|
324
|
+
return f"ColorManager(colors={self.color_count})"
|