phue2 0.0.1.dev279__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.
- phue2/__init__.py +37 -0
- phue2/__main__.py +408 -0
- phue2/_internal/__init__.py +1 -0
- phue2/_internal/console.py +175 -0
- phue2/bridge.py +1246 -0
- phue2/exceptions.py +22 -0
- phue2/group.py +118 -0
- phue2/light.py +301 -0
- phue2/scene.py +58 -0
- phue2/sensor.py +307 -0
- phue2-0.0.1.dev279.dist-info/METADATA +188 -0
- phue2-0.0.1.dev279.dist-info/RECORD +15 -0
- phue2-0.0.1.dev279.dist-info/WHEEL +4 -0
- phue2-0.0.1.dev279.dist-info/entry_points.txt +2 -0
- phue2-0.0.1.dev279.dist-info/licenses/LICENSE +21 -0
phue2/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
phue2 - A modernized Philips Hue Python library
|
|
3
|
+
Based on the original phue by Nathanaël Lécaudé
|
|
4
|
+
Original protocol hacking by rsmck: http://rsmck.co.uk/hue
|
|
5
|
+
|
|
6
|
+
Published under the MIT license
|
|
7
|
+
|
|
8
|
+
"Hue Personal Wireless Lighting" is a trademark owned by Koninklijke Philips Electronics N.V.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .exceptions import PhueException, PhueRegistrationException, PhueRequestTimeout
|
|
12
|
+
from .light import Light
|
|
13
|
+
from .sensor import Sensor, SensorState, SensorConfig
|
|
14
|
+
from .group import Group, AllLights
|
|
15
|
+
from .scene import Scene
|
|
16
|
+
from .bridge import Bridge
|
|
17
|
+
from ._internal.console import console
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger("phue2")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"Bridge",
|
|
26
|
+
"PhueException",
|
|
27
|
+
"PhueRegistrationException",
|
|
28
|
+
"PhueRequestTimeout",
|
|
29
|
+
"Light",
|
|
30
|
+
"Group",
|
|
31
|
+
"AllLights",
|
|
32
|
+
"Scene",
|
|
33
|
+
"Sensor",
|
|
34
|
+
"SensorState",
|
|
35
|
+
"SensorConfig",
|
|
36
|
+
"console",
|
|
37
|
+
]
|
phue2/__main__.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
"""Command-line interface for the phue library."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from phue2 import Bridge, PhueRegistrationException
|
|
10
|
+
from phue2._internal.console import (
|
|
11
|
+
BLUE,
|
|
12
|
+
BOLD,
|
|
13
|
+
CYAN,
|
|
14
|
+
GREEN,
|
|
15
|
+
MAGENTA,
|
|
16
|
+
RED,
|
|
17
|
+
YELLOW,
|
|
18
|
+
console,
|
|
19
|
+
styled_text,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
DISABLE_STYLING = False
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def styled_for_cli(text: str, style: str) -> str:
|
|
26
|
+
"""Apply styling if enabled, otherwise return plain text.
|
|
27
|
+
|
|
28
|
+
This helper makes tests less brittle by allowing them to match
|
|
29
|
+
on the plain text content.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
text: Text to style
|
|
33
|
+
style: Style to apply
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Styled text if styling is enabled, otherwise plain text
|
|
37
|
+
"""
|
|
38
|
+
if DISABLE_STYLING:
|
|
39
|
+
return text
|
|
40
|
+
return styled_text(text, style)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def parse_args(
|
|
44
|
+
argv: list[str] | None = None,
|
|
45
|
+
) -> tuple[argparse.ArgumentParser, argparse.Namespace]:
|
|
46
|
+
"""Parse command line arguments.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
argv: Command line arguments (defaults to sys.argv[1:])
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Tuple of (parser, parsed_args)
|
|
53
|
+
"""
|
|
54
|
+
# Set up argument parser
|
|
55
|
+
parser = argparse.ArgumentParser(
|
|
56
|
+
description="Control Philips Hue lights from the command line"
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"--host", help="IP address of the Hue bridge (auto-detected if not provided)"
|
|
60
|
+
)
|
|
61
|
+
parser.add_argument("--config-file-path", help="Path to the config file")
|
|
62
|
+
parser.add_argument("--debug", action="store_true", help="Enable debug logging")
|
|
63
|
+
|
|
64
|
+
# Command subparsers
|
|
65
|
+
subparsers = parser.add_subparsers(dest="command", help="Command to execute")
|
|
66
|
+
|
|
67
|
+
# List command (with ls alias)
|
|
68
|
+
list_parser = subparsers.add_parser(
|
|
69
|
+
"list", aliases=["ls"], help="List available resources"
|
|
70
|
+
)
|
|
71
|
+
list_parser.add_argument(
|
|
72
|
+
"resource",
|
|
73
|
+
nargs="?",
|
|
74
|
+
choices=["lights", "groups", "scenes"],
|
|
75
|
+
default="lights",
|
|
76
|
+
help="Resource type to list",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Get command
|
|
80
|
+
get_parser = subparsers.add_parser("get", help="Get resource details")
|
|
81
|
+
get_parser.add_argument(
|
|
82
|
+
"resource", choices=["light", "group", "scene"], help="Resource type"
|
|
83
|
+
)
|
|
84
|
+
get_parser.add_argument("name", help="Resource name or ID")
|
|
85
|
+
|
|
86
|
+
# Set command
|
|
87
|
+
set_parser = subparsers.add_parser("set", help="Set resource state")
|
|
88
|
+
set_parser.add_argument(
|
|
89
|
+
"resource", choices=["light", "group"], help="Resource type"
|
|
90
|
+
)
|
|
91
|
+
set_parser.add_argument("name", help="Resource name or ID")
|
|
92
|
+
set_parser.add_argument("--on", action="store_true", help="Turn on")
|
|
93
|
+
set_parser.add_argument("--off", action="store_true", help="Turn off")
|
|
94
|
+
set_parser.add_argument("--bri", type=int, help="Set brightness (0-254)")
|
|
95
|
+
set_parser.add_argument("--hue", type=int, help="Set hue (0-65535)")
|
|
96
|
+
set_parser.add_argument("--sat", type=int, help="Set saturation (0-254)")
|
|
97
|
+
|
|
98
|
+
# Parse arguments
|
|
99
|
+
return parser, parser.parse_args(argv)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_bridge_from_config(config_path: str | None = None) -> Bridge | None:
|
|
103
|
+
"""Attempt to create a Bridge using existing config.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
config_path: Path to config file (uses default if None)
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Bridge instance if successful, None otherwise
|
|
110
|
+
"""
|
|
111
|
+
if not config_path:
|
|
112
|
+
config_path = os.path.expanduser("~/.python_hue")
|
|
113
|
+
|
|
114
|
+
if not os.path.exists(config_path):
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
with open(config_path) as f:
|
|
119
|
+
config = json.loads(f.read())
|
|
120
|
+
|
|
121
|
+
# Try each bridge in the config
|
|
122
|
+
for ip in config:
|
|
123
|
+
if "username" in config[ip]:
|
|
124
|
+
try:
|
|
125
|
+
bridge = Bridge(ip=ip, config_file_path=config_path)
|
|
126
|
+
console.info(
|
|
127
|
+
f"{styled_for_cli('Using saved connection to bridge at', MAGENTA)} {styled_for_cli(ip, YELLOW)}"
|
|
128
|
+
)
|
|
129
|
+
return bridge
|
|
130
|
+
except Exception:
|
|
131
|
+
continue
|
|
132
|
+
except Exception:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def main(argv: list[str] | None = None) -> int:
|
|
139
|
+
"""Run the phue command-line interface.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
argv: Command line arguments (defaults to sys.argv[1:])
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Exit code
|
|
146
|
+
"""
|
|
147
|
+
# Parse arguments
|
|
148
|
+
parser, args = parse_args(argv)
|
|
149
|
+
|
|
150
|
+
# Configure logging
|
|
151
|
+
log_level = logging.DEBUG if args.debug else logging.WARNING
|
|
152
|
+
logging.basicConfig(level=log_level)
|
|
153
|
+
|
|
154
|
+
# Get bridge - first try config if no host specified
|
|
155
|
+
bridge = None
|
|
156
|
+
if not args.host:
|
|
157
|
+
bridge = get_bridge_from_config(args.config_file_path)
|
|
158
|
+
|
|
159
|
+
# If not connected and host provided, connect to specified host
|
|
160
|
+
if not bridge and args.host:
|
|
161
|
+
console.info(f"Connecting to bridge at {args.host}...")
|
|
162
|
+
while True:
|
|
163
|
+
try:
|
|
164
|
+
bridge = Bridge(args.host, config_file_path=args.config_file_path)
|
|
165
|
+
console.success("Successfully connected to the bridge!")
|
|
166
|
+
break
|
|
167
|
+
except PhueRegistrationException:
|
|
168
|
+
console.warning(
|
|
169
|
+
"Link button not pressed. Press the link button on your bridge."
|
|
170
|
+
)
|
|
171
|
+
input("Press Enter to try again...")
|
|
172
|
+
except Exception as e:
|
|
173
|
+
console.error(f"Failed to connect to the bridge: {e}")
|
|
174
|
+
return 1
|
|
175
|
+
|
|
176
|
+
# If we still don't have a bridge but command specified, error out
|
|
177
|
+
if not bridge and args.command:
|
|
178
|
+
console.error(
|
|
179
|
+
"No bridge connection available. Please specify --host or ensure config file exists."
|
|
180
|
+
)
|
|
181
|
+
return 1
|
|
182
|
+
|
|
183
|
+
# If we still don't have a bridge and no command specified, show general help
|
|
184
|
+
if not bridge and not args.command:
|
|
185
|
+
console.error(
|
|
186
|
+
"No bridge connection available. Please specify --host or ensure config file exists."
|
|
187
|
+
)
|
|
188
|
+
args_parser = argparse.ArgumentParser(
|
|
189
|
+
description="Control Philips Hue lights from the command line"
|
|
190
|
+
)
|
|
191
|
+
args_parser.print_help()
|
|
192
|
+
return 1
|
|
193
|
+
|
|
194
|
+
# If no command specified but we have a bridge, show command help
|
|
195
|
+
if bridge and not args.command:
|
|
196
|
+
console.info(
|
|
197
|
+
f"{styled_for_cli('Connected to bridge.', GREEN)} {styled_for_cli('Use a command to continue.', CYAN)}"
|
|
198
|
+
)
|
|
199
|
+
parser.print_help()
|
|
200
|
+
return 0
|
|
201
|
+
|
|
202
|
+
# Process commands
|
|
203
|
+
|
|
204
|
+
# Handle list/ls command
|
|
205
|
+
if args.command in ["list", "ls"]:
|
|
206
|
+
if not bridge:
|
|
207
|
+
console.error("No bridge connection available")
|
|
208
|
+
return 1
|
|
209
|
+
|
|
210
|
+
if args.resource == "lights":
|
|
211
|
+
lights = bridge.lights
|
|
212
|
+
console.info(styled_for_cli(f"LIGHTS ({len(lights)}):", YELLOW + BOLD))
|
|
213
|
+
for light in lights:
|
|
214
|
+
status = "ON" if light.on else "OFF"
|
|
215
|
+
status_styled = styled_for_cli(status, GREEN if light.on else RED)
|
|
216
|
+
name = str(light.name)
|
|
217
|
+
name_styled = styled_for_cli(name, CYAN)
|
|
218
|
+
console.info(f" {name_styled:<25} {status_styled}")
|
|
219
|
+
|
|
220
|
+
elif args.resource == "groups":
|
|
221
|
+
groups = bridge.groups
|
|
222
|
+
console.info(styled_for_cli(f"GROUPS ({len(groups)}):", YELLOW + BOLD))
|
|
223
|
+
for group in groups:
|
|
224
|
+
name = str(group.name)
|
|
225
|
+
name_styled = styled_for_cli(name, CYAN)
|
|
226
|
+
console.info(f" {name_styled}")
|
|
227
|
+
|
|
228
|
+
elif args.resource == "scenes":
|
|
229
|
+
scenes = bridge.scenes
|
|
230
|
+
console.info(styled_for_cli(f"SCENES ({len(scenes)}):", YELLOW + BOLD))
|
|
231
|
+
for scene in scenes:
|
|
232
|
+
name = str(scene.name)
|
|
233
|
+
name_styled = styled_for_cli(name, CYAN)
|
|
234
|
+
console.info(f" {name_styled}")
|
|
235
|
+
|
|
236
|
+
# Handle get command
|
|
237
|
+
elif args.command == "get":
|
|
238
|
+
if not bridge:
|
|
239
|
+
console.error("No bridge connection available")
|
|
240
|
+
return 1
|
|
241
|
+
|
|
242
|
+
resource = args.resource
|
|
243
|
+
name = args.name
|
|
244
|
+
|
|
245
|
+
if resource == "light":
|
|
246
|
+
# Try by ID first, then by name
|
|
247
|
+
light = None
|
|
248
|
+
try:
|
|
249
|
+
light_id = int(name)
|
|
250
|
+
light = bridge.lights_by_id.get(light_id)
|
|
251
|
+
except ValueError:
|
|
252
|
+
pass
|
|
253
|
+
|
|
254
|
+
if not light:
|
|
255
|
+
light = bridge.lights_by_name.get(name)
|
|
256
|
+
|
|
257
|
+
if not light:
|
|
258
|
+
console.error(f"Light '{name}' not found")
|
|
259
|
+
return 1
|
|
260
|
+
|
|
261
|
+
console.info(styled_for_cli(f"LIGHT: {light.name}", YELLOW + BOLD))
|
|
262
|
+
console.info(
|
|
263
|
+
f" {styled_for_cli('Status:', BLUE)} {styled_for_cli('ON' if light.on else 'OFF', GREEN if light.on else RED)}"
|
|
264
|
+
)
|
|
265
|
+
console.info(f" {styled_for_cli('Type:', BLUE)} {light.type}")
|
|
266
|
+
console.info(
|
|
267
|
+
f" {styled_for_cli('Brightness:', BLUE)} {light.brightness}/254"
|
|
268
|
+
)
|
|
269
|
+
console.info(f" {styled_for_cli('Hue:', BLUE)} {light.hue}/65535")
|
|
270
|
+
console.info(
|
|
271
|
+
f" {styled_for_cli('Saturation:', BLUE)} {light.saturation}/254"
|
|
272
|
+
)
|
|
273
|
+
console.info(f" {styled_for_cli('Reachable:', BLUE)} {light.reachable}")
|
|
274
|
+
|
|
275
|
+
elif resource == "group":
|
|
276
|
+
# Try to find group by name
|
|
277
|
+
group_id = None
|
|
278
|
+
try:
|
|
279
|
+
group_id = bridge.get_group_id_by_name(name)
|
|
280
|
+
except Exception:
|
|
281
|
+
pass
|
|
282
|
+
|
|
283
|
+
if group_id is None:
|
|
284
|
+
try:
|
|
285
|
+
group_id = int(name)
|
|
286
|
+
except ValueError:
|
|
287
|
+
group_id = None
|
|
288
|
+
|
|
289
|
+
if group_id is None:
|
|
290
|
+
console.error(f"Group '{name}' not found")
|
|
291
|
+
return 1
|
|
292
|
+
|
|
293
|
+
group = bridge.get_group(group_id)
|
|
294
|
+
|
|
295
|
+
console.info(styled_for_cli(f"GROUP: {group['name']}", YELLOW + BOLD))
|
|
296
|
+
console.info(f" {styled_for_cli('Type:', BLUE)} {group['type']}")
|
|
297
|
+
console.info(
|
|
298
|
+
f" {styled_for_cli('Lights:', BLUE)} {', '.join(group['lights'])}"
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
elif resource == "scene":
|
|
302
|
+
# This needs bridge.get_scene() implementation which isn't shown
|
|
303
|
+
console.error("Scene details not yet implemented")
|
|
304
|
+
return 1
|
|
305
|
+
|
|
306
|
+
# Handle set command
|
|
307
|
+
elif args.command == "set":
|
|
308
|
+
if not bridge:
|
|
309
|
+
console.error("No bridge connection available")
|
|
310
|
+
return 1
|
|
311
|
+
|
|
312
|
+
resource = args.resource
|
|
313
|
+
name = args.name
|
|
314
|
+
changed = False
|
|
315
|
+
|
|
316
|
+
if resource == "light":
|
|
317
|
+
# Try by ID first, then by name
|
|
318
|
+
light = None
|
|
319
|
+
try:
|
|
320
|
+
light_id = int(name)
|
|
321
|
+
light = bridge.lights_by_id.get(light_id)
|
|
322
|
+
except ValueError:
|
|
323
|
+
pass
|
|
324
|
+
|
|
325
|
+
if not light:
|
|
326
|
+
light = bridge.lights_by_name.get(name)
|
|
327
|
+
|
|
328
|
+
if not light:
|
|
329
|
+
console.error(f"Light '{name}' not found")
|
|
330
|
+
return 1
|
|
331
|
+
|
|
332
|
+
# Apply state changes
|
|
333
|
+
if args.on:
|
|
334
|
+
light.on = True
|
|
335
|
+
changed = True
|
|
336
|
+
elif args.off:
|
|
337
|
+
light.on = False
|
|
338
|
+
changed = True
|
|
339
|
+
|
|
340
|
+
if args.bri is not None:
|
|
341
|
+
light.brightness = args.bri
|
|
342
|
+
changed = True
|
|
343
|
+
|
|
344
|
+
if args.hue is not None:
|
|
345
|
+
light.hue = args.hue
|
|
346
|
+
changed = True
|
|
347
|
+
|
|
348
|
+
if args.sat is not None:
|
|
349
|
+
light.saturation = args.sat
|
|
350
|
+
changed = True
|
|
351
|
+
|
|
352
|
+
if changed:
|
|
353
|
+
console.success(f"Updated light '{light.name}'")
|
|
354
|
+
else:
|
|
355
|
+
console.warning("No changes specified")
|
|
356
|
+
|
|
357
|
+
elif resource == "group":
|
|
358
|
+
# Try to find group by name
|
|
359
|
+
group_id = None
|
|
360
|
+
try:
|
|
361
|
+
group_id = bridge.get_group_id_by_name(name)
|
|
362
|
+
except Exception:
|
|
363
|
+
pass
|
|
364
|
+
|
|
365
|
+
if group_id is None:
|
|
366
|
+
try:
|
|
367
|
+
group_id = int(name)
|
|
368
|
+
except ValueError:
|
|
369
|
+
group_id = None
|
|
370
|
+
|
|
371
|
+
if group_id is None:
|
|
372
|
+
console.error(f"Group '{name}' not found")
|
|
373
|
+
return 1
|
|
374
|
+
|
|
375
|
+
# Apply state changes
|
|
376
|
+
state_changes: dict[str, object] = {}
|
|
377
|
+
changed = False
|
|
378
|
+
|
|
379
|
+
if args.on:
|
|
380
|
+
state_changes["on"] = True
|
|
381
|
+
changed = True
|
|
382
|
+
elif args.off:
|
|
383
|
+
state_changes["on"] = False
|
|
384
|
+
changed = True
|
|
385
|
+
|
|
386
|
+
if args.bri is not None:
|
|
387
|
+
state_changes["bri"] = args.bri
|
|
388
|
+
changed = True
|
|
389
|
+
|
|
390
|
+
if args.hue is not None:
|
|
391
|
+
state_changes["hue"] = args.hue
|
|
392
|
+
changed = True
|
|
393
|
+
|
|
394
|
+
if args.sat is not None:
|
|
395
|
+
state_changes["sat"] = args.sat
|
|
396
|
+
changed = True
|
|
397
|
+
|
|
398
|
+
if changed:
|
|
399
|
+
bridge.set_group(group_id, state_changes)
|
|
400
|
+
console.success(f"Updated group {name}")
|
|
401
|
+
else:
|
|
402
|
+
console.warning("No changes specified")
|
|
403
|
+
|
|
404
|
+
return 0
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
if __name__ == "__main__":
|
|
408
|
+
sys.exit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Utilities for the phue package."""
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Terminal utilities for displaying colorful output in the console."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import platform
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import TypeVar
|
|
8
|
+
|
|
9
|
+
# Check if we're running on Windows
|
|
10
|
+
COLORS_ENABLED = platform.system() != "Windows"
|
|
11
|
+
|
|
12
|
+
# ANSI color/style codes - used only if COLORS_ENABLED is True
|
|
13
|
+
RESET = "\033[0m"
|
|
14
|
+
BOLD = "\033[1m"
|
|
15
|
+
GREEN = "\033[32m"
|
|
16
|
+
YELLOW = "\033[33m"
|
|
17
|
+
RED = "\033[31m"
|
|
18
|
+
BLUE = "\033[34m"
|
|
19
|
+
CYAN = "\033[36m"
|
|
20
|
+
MAGENTA = "\033[35m"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def styled_text(text: str, *styles: str) -> str:
|
|
24
|
+
"""Apply ANSI styles to text if colors are enabled.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
text: The text to style
|
|
28
|
+
*styles: ANSI style codes to apply
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The styled text, or the original text if colors are disabled
|
|
32
|
+
"""
|
|
33
|
+
if not COLORS_ENABLED or not styles:
|
|
34
|
+
return text
|
|
35
|
+
|
|
36
|
+
style = "".join(styles)
|
|
37
|
+
return f"{style}{text}{RESET}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def create_printer(style: str) -> Callable[[str], None]:
|
|
41
|
+
"""Create a function that prints text with the given style.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
style: ANSI style code (will be ignored on Windows)
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
A function that prints text with the given style if supported
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def printer(text: str) -> None:
|
|
51
|
+
print(styled_text(text, style))
|
|
52
|
+
|
|
53
|
+
return printer
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
print_success = create_printer(f"{GREEN}{BOLD}")
|
|
57
|
+
print_info = create_printer(CYAN)
|
|
58
|
+
print_error = create_printer(f"{RED}{BOLD}")
|
|
59
|
+
print_warning = create_printer(f"{YELLOW}{BOLD}")
|
|
60
|
+
print_header = create_printer(f"{BLUE}{BOLD}")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
T = TypeVar("T")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TerminalUI:
|
|
67
|
+
"""A simple UI class for terminal-based interfaces."""
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def header(title: str) -> None:
|
|
71
|
+
"""Print a header with a title.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
title: The title to display
|
|
75
|
+
"""
|
|
76
|
+
print(f"\n{styled_text('╔' + '═' * 50 + '╗', BLUE, BOLD)}")
|
|
77
|
+
print(
|
|
78
|
+
f"{styled_text('║', BLUE, BOLD)} {styled_text(title.center(48), CYAN, BOLD)} {styled_text('║', BLUE, BOLD)}"
|
|
79
|
+
)
|
|
80
|
+
print(f"{styled_text('╚' + '═' * 50 + '╝', BLUE, BOLD)}")
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
def section(title: str) -> None:
|
|
84
|
+
"""Print a section divider with a title.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
title: The title to display
|
|
88
|
+
"""
|
|
89
|
+
print(f"\n{styled_text(f'▓▒░ {title} ░▒▓', YELLOW, BOLD)}")
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def success(message: str) -> None:
|
|
93
|
+
"""Print a success message.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
message: The message to display
|
|
97
|
+
"""
|
|
98
|
+
print(f"{styled_text(f'✓ {message}', GREEN, BOLD)}")
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def info(message: str) -> None:
|
|
102
|
+
"""Print an info message.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
message: The message to display
|
|
106
|
+
"""
|
|
107
|
+
print(f"{styled_text(message, CYAN)}")
|
|
108
|
+
|
|
109
|
+
@staticmethod
|
|
110
|
+
def error(message: str) -> None:
|
|
111
|
+
"""Print an error message.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
message: The message to display
|
|
115
|
+
"""
|
|
116
|
+
print(f"{styled_text(f'✗ {message}', RED, BOLD)}")
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def warning(message: str) -> None:
|
|
120
|
+
"""Print a warning message.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
message: The message to display
|
|
124
|
+
"""
|
|
125
|
+
print(f"{styled_text(f'⚠ {message}', YELLOW, BOLD)}")
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def box(title: str, messages: list[str], style: str = MAGENTA) -> None:
|
|
129
|
+
"""Print a box with a title and messages.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
title: The box title
|
|
133
|
+
messages: The messages to display inside the box
|
|
134
|
+
style: ANSI style code for the box
|
|
135
|
+
"""
|
|
136
|
+
# Find the longest message to size the box
|
|
137
|
+
width = max(len(title), max(len(m) for m in messages)) + 4
|
|
138
|
+
|
|
139
|
+
# Print the box
|
|
140
|
+
print(
|
|
141
|
+
f"{styled_text(f'┌─ {title} ' + '─' * (width - len(title) - 4) + '┐', style, BOLD)}"
|
|
142
|
+
)
|
|
143
|
+
for msg in messages:
|
|
144
|
+
print(
|
|
145
|
+
f"{styled_text(f'│ {msg}' + ' ' * (width - len(msg) - 2) + '│', style, BOLD)}"
|
|
146
|
+
)
|
|
147
|
+
print(f"{styled_text('└' + '─' * (width - 2) + '┘', style, BOLD)}")
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def table(
|
|
151
|
+
items: list[T], formatter: Callable[[T], str], border_style: str = CYAN
|
|
152
|
+
) -> None:
|
|
153
|
+
"""Print a simple table of items.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
items: List of items to display
|
|
157
|
+
formatter: Function to format each item
|
|
158
|
+
border_style: ANSI style code for the table border
|
|
159
|
+
"""
|
|
160
|
+
if not items:
|
|
161
|
+
print(styled_text("No items to display", CYAN))
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
# Create header and footer
|
|
165
|
+
print(f"{styled_text('╭─ Items ' + '─' * 40 + '╮', border_style)}")
|
|
166
|
+
|
|
167
|
+
# Print each item
|
|
168
|
+
for item in items:
|
|
169
|
+
print(f"{styled_text('│', border_style)} {formatter(item)}")
|
|
170
|
+
|
|
171
|
+
# Footer
|
|
172
|
+
print(f"{styled_text('╰' + '─' * 48 + '╯', border_style)}")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
console = TerminalUI()
|