0
# User Interface
1
2
Command-line interface utilities, user interaction functions, output formatting, and command infrastructure for building interactive music management tools. The UI system provides the foundation for beets' CLI and enables plugins to create rich interactive experiences.
3
4
## Capabilities
5
6
### Input Functions
7
8
Functions for getting user input with proper Unicode handling and validation.
9
10
```python { .api }
11
def input_(prompt: str = None) -> str:
12
"""
13
Get user input with Unicode handling and error checking.
14
15
Parameters:
16
- prompt: Optional prompt string to display
17
18
Returns:
19
User input as Unicode string
20
21
Raises:
22
UserError: If stdin is not available (e.g., in non-interactive mode)
23
"""
24
25
def input_options(options: List[str], require: bool = False, prompt: str = None,
26
fallback_prompt: str = None, numrange: Tuple[int, int] = None,
27
default: str = None, max_width: int = 72) -> str:
28
"""
29
Prompt user to choose from a list of options.
30
31
Parameters:
32
- options: List of option strings (capitalize letters for shortcuts)
33
- require: If True, user must make a choice (no default)
34
- prompt: Custom prompt string
35
- fallback_prompt: Prompt shown for invalid input
36
- numrange: Optional (low, high) tuple for numeric input
37
- default: Default option if user presses enter
38
- max_width: Maximum prompt width for wrapping
39
40
Returns:
41
Single character representing chosen option
42
"""
43
44
def input_yn(prompt: str, require: bool = False) -> bool:
45
"""
46
Prompt user for yes/no response.
47
48
Parameters:
49
- prompt: Question to ask user
50
- require: If True, user must explicitly choose (no default)
51
52
Returns:
53
True for yes, False for no
54
"""
55
56
def input_select_objects(prompt: str, objs: List[Any], rep: Callable,
57
prompt_all: str = None) -> List[Any]:
58
"""
59
Let user select from a list of objects.
60
61
Parameters:
62
- prompt: Action prompt (e.g., "Delete item")
63
- objs: List of objects to choose from
64
- rep: Function to display each object
65
- prompt_all: Optional prompt for the initial all/none/select choice
66
67
Returns:
68
List of selected objects
69
"""
70
```
71
72
### Output Functions
73
74
Functions for safe, formatted output with encoding and colorization support.
75
76
```python { .api }
77
def print_(*strings: List[str], **kwargs) -> None:
78
"""
79
Print strings with safe Unicode encoding handling.
80
81
Parameters:
82
- *strings: String arguments to print (must be Unicode)
83
- end: String to append (defaults to newline)
84
85
Notes:
86
Handles encoding errors gracefully and respects terminal capabilities
87
"""
88
89
def colorize(color_name: str, text: str) -> str:
90
"""
91
Apply color formatting to text for terminal display.
92
93
Parameters:
94
- color_name: Color name from configuration (e.g., 'text_error', 'text_success')
95
- text: Text to colorize
96
97
Returns:
98
Text with ANSI color codes (if color is enabled)
99
"""
100
101
def uncolorize(colored_text: str) -> str:
102
"""
103
Remove ANSI color codes from text.
104
105
Parameters:
106
- colored_text: Text potentially containing ANSI codes
107
108
Returns:
109
Plain text with color codes stripped
110
"""
111
112
def colordiff(a: Any, b: Any) -> Tuple[str, str]:
113
"""
114
Highlight differences between two values.
115
116
Parameters:
117
- a: First value for comparison
118
- b: Second value for comparison
119
120
Returns:
121
Tuple of (colored_a, colored_b) with differences highlighted
122
"""
123
```
124
125
### Formatting Functions
126
127
Functions for human-readable formatting of various data types.
128
129
```python { .api }
130
def human_bytes(size: int) -> str:
131
"""
132
Format byte count in human-readable format.
133
134
Parameters:
135
- size: Size in bytes
136
137
Returns:
138
Formatted string (e.g., "1.5 MB", "832 KB")
139
"""
140
141
def human_seconds(interval: float) -> str:
142
"""
143
Format time interval in human-readable format.
144
145
Parameters:
146
- interval: Time interval in seconds
147
148
Returns:
149
Formatted string (e.g., "3.2 minutes", "1.5 hours")
150
"""
151
152
def human_seconds_short(interval: float) -> str:
153
"""
154
Format time interval in short M:SS format.
155
156
Parameters:
157
- interval: Time interval in seconds
158
159
Returns:
160
Formatted string (e.g., "3:45", "12:03")
161
"""
162
```
163
164
### Display Functions
165
166
Functions for displaying model changes and file operations.
167
168
```python { .api }
169
def show_model_changes(new: Model, old: Model = None, fields: List[str] = None,
170
always: bool = False) -> bool:
171
"""
172
Display changes between model objects.
173
174
Parameters:
175
- new: Updated model object
176
- old: Original model object (uses pristine version if None)
177
- fields: Specific fields to show (all fields if None)
178
- always: Show object even if no changes found
179
180
Returns:
181
True if changes were found and displayed
182
"""
183
184
def show_path_changes(path_changes: List[Tuple[str, str]]) -> None:
185
"""
186
Display file path changes in formatted layout.
187
188
Parameters:
189
- path_changes: List of (source_path, dest_path) tuples
190
191
Notes:
192
Automatically chooses single-line or multi-line layout based on path lengths
193
"""
194
```
195
196
### Command Infrastructure
197
198
Classes for building CLI commands and option parsing.
199
200
```python { .api }
201
class Subcommand:
202
"""Represents a CLI subcommand that can be invoked by beets."""
203
204
def __init__(self, name: str, parser: OptionParser = None, help: str = "",
205
aliases: List[str] = (), hide: bool = False):
206
"""
207
Create a new subcommand.
208
209
Parameters:
210
- name: Primary command name
211
- parser: OptionParser for command options (creates default if None)
212
- help: Help text description
213
- aliases: Alternative names for the command
214
- hide: Whether to hide from help output
215
"""
216
217
func: Callable[[Library, optparse.Values, List[str]], Any]
218
"""Function to execute when command is invoked."""
219
220
def parse_args(self, args: List[str]) -> Tuple[optparse.Values, List[str]]:
221
"""
222
Parse command-line arguments for this command.
223
224
Parameters:
225
- args: List of argument strings
226
227
Returns:
228
Tuple of (options, remaining_args)
229
"""
230
231
class CommonOptionsParser(optparse.OptionParser):
232
"""Enhanced OptionParser with common beets options."""
233
234
def add_album_option(self, flags: Tuple[str, str] = ("-a", "--album")) -> None:
235
"""Add -a/--album option to match albums instead of tracks."""
236
237
def add_path_option(self, flags: Tuple[str, str] = ("-p", "--path")) -> None:
238
"""Add -p/--path option to display paths instead of formatted output."""
239
240
def add_format_option(self, flags: Tuple[str, str] = ("-f", "--format"),
241
target: str = None) -> None:
242
"""Add -f/--format option for custom output formatting."""
243
244
def add_all_common_options(self) -> None:
245
"""Add album, path, and format options together."""
246
247
class SubcommandsOptionParser(CommonOptionsParser):
248
"""OptionParser that handles multiple subcommands."""
249
250
def add_subcommand(self, *cmds: Subcommand) -> None:
251
"""Add one or more subcommands to the parser."""
252
253
def parse_subcommand(self, args: List[str]) -> Tuple[Subcommand, optparse.Values, List[str]]:
254
"""
255
Parse arguments and return the invoked subcommand.
256
257
Parameters:
258
- args: Command-line arguments
259
260
Returns:
261
Tuple of (subcommand, options, subcommand_args)
262
"""
263
```
264
265
### Configuration Helpers
266
267
Functions for handling UI-related configuration and decisions.
268
269
```python { .api }
270
def should_write(write_opt: bool = None) -> bool:
271
"""
272
Determine if metadata should be written to files.
273
274
Parameters:
275
- write_opt: Explicit write option (uses config if None)
276
277
Returns:
278
True if files should be written
279
"""
280
281
def should_move(move_opt: bool = None) -> bool:
282
"""
283
Determine if files should be moved after metadata updates.
284
285
Parameters:
286
- move_opt: Explicit move option (uses config if None)
287
288
Returns:
289
True if files should be moved
290
"""
291
292
def get_path_formats(subview: dict = None) -> List[Tuple[str, Template]]:
293
"""
294
Get path format templates from configuration.
295
296
Parameters:
297
- subview: Optional config subview (uses paths config if None)
298
299
Returns:
300
List of (query, template) tuples
301
"""
302
303
def get_replacements() -> List[Tuple[re.Pattern, str]]:
304
"""
305
Get character replacement rules from configuration.
306
307
Returns:
308
List of (regex_pattern, replacement) tuples
309
"""
310
```
311
312
### Terminal Utilities
313
314
Functions for terminal interaction and layout.
315
316
```python { .api }
317
def term_width() -> int:
318
"""
319
Get terminal width in columns.
320
321
Returns:
322
Terminal width or configured fallback value
323
"""
324
325
def indent(count: int) -> str:
326
"""
327
Create indentation string.
328
329
Parameters:
330
- count: Number of spaces for indentation
331
332
Returns:
333
String with specified number of spaces
334
"""
335
```
336
337
## Command Development Examples
338
339
### Basic Command Plugin
340
341
```python
342
from beets.ui import Subcommand, print_
343
from beets.plugins import BeetsPlugin
344
345
class StatsCommand(Subcommand):
346
"""Command to show library statistics."""
347
348
def __init__(self):
349
super().__init__('stats', help='show library statistics')
350
self.func = self.run
351
352
def run(self, lib, opts, args):
353
"""Execute the stats command."""
354
total_items = len(lib.items())
355
total_albums = len(lib.albums())
356
357
print_(f"Library contains:")
358
print_(f" {total_items} tracks")
359
print_(f" {total_albums} albums")
360
361
class StatsPlugin(BeetsPlugin):
362
def commands(self):
363
return [StatsCommand()]
364
```
365
366
### Interactive Command with Options
367
368
```python
369
from beets.ui import Subcommand, input_yn, input_options, show_model_changes
370
from beets.ui import CommonOptionsParser
371
372
class CleanupCommand(Subcommand):
373
"""Command with interactive cleanup options."""
374
375
def __init__(self):
376
parser = CommonOptionsParser()
377
parser.add_option('-n', '--dry-run', action='store_true',
378
help='show what would be done without making changes')
379
parser.add_album_option()
380
381
super().__init__('cleanup', parser=parser,
382
help='interactive library cleanup')
383
self.func = self.run
384
385
def run(self, lib, opts, args):
386
"""Execute cleanup with user interaction."""
387
# Find items with potential issues
388
problematic = lib.items('genre:') # Items with no genre
389
390
if not problematic:
391
print_("No cleanup needed!")
392
return
393
394
print_(f"Found {len(problematic)} items without genre")
395
396
if opts.dry_run:
397
for item in problematic:
398
print_(f"Would process: {item}")
399
return
400
401
# Interactive processing
402
action = input_options(['fix', 'skip', 'quit'],
403
prompt="Fix genres automatically?")
404
405
if action == 'f': # fix
406
for item in problematic:
407
item.genre = 'Unknown'
408
if show_model_changes(item):
409
if input_yn(f"Save changes to {item}?"):
410
item.store()
411
elif action == 'q': # quit
412
return
413
```
414
415
### Command with Custom Formatting
416
417
```python
418
from beets.ui import Subcommand, print_, colorize
419
from beets.ui import CommonOptionsParser
420
421
class ListCommand(Subcommand):
422
"""Enhanced list command with custom formatting."""
423
424
def __init__(self):
425
parser = CommonOptionsParser()
426
parser.add_all_common_options()
427
parser.add_option('--count', action='store_true',
428
help='show count instead of items')
429
430
super().__init__('mylist', parser=parser,
431
help='list items with custom formatting')
432
self.func = self.run
433
434
def run(self, lib, opts, args):
435
"""Execute custom list command."""
436
query = ' '.join(args) if args else None
437
438
if hasattr(opts, 'album') and opts.album:
439
objs = lib.albums(query)
440
obj_type = "albums"
441
else:
442
objs = lib.items(query)
443
obj_type = "items"
444
445
if opts.count:
446
print_(f"{len(objs)} {obj_type}")
447
return
448
449
# Custom formatting
450
for obj in objs:
451
if hasattr(obj, 'album'): # Item
452
artist = colorize('text_highlight', obj.artist)
453
title = colorize('text_success', obj.title)
454
print_(f"{artist} - {title}")
455
else: # Album
456
artist = colorize('text_highlight', obj.albumartist)
457
album = colorize('text_success', obj.album)
458
year = f" ({obj.year})" if obj.year else ""
459
print_(f"{artist} - {album}{year}")
460
```
461
462
### Form-based Input
463
464
```python
465
from beets.ui import input_, input_options, input_select_objects
466
467
def interactive_import_session():
468
"""Example of complex user interaction."""
469
470
# Get import directory
471
directory = input_("Enter directory to import: ")
472
473
# Get import options
474
write_tags = input_yn("Write tags to files?", require=False)
475
copy_files = input_yn("Copy files (vs. move)?", require=False)
476
477
# Choose import strategy
478
strategy = input_options(
479
['automatic', 'manual', 'skip-existing'],
480
prompt="Choose import strategy",
481
default='automatic'
482
)
483
484
print_(f"Importing from: {directory}")
485
print_(f"Write tags: {write_tags}")
486
print_(f"Copy files: {copy_files}")
487
print_(f"Strategy: {strategy}")
488
489
def select_items_for_action(lib):
490
"""Example of object selection UI."""
491
492
# Get all untagged items
493
untagged = list(lib.items('artist:'))
494
495
if not untagged:
496
print_("No untagged items found")
497
return
498
499
# Let user select items to process
500
def show_item(item):
501
print_(f" {item.path}")
502
503
selected = input_select_objects(
504
"Tag item",
505
untagged,
506
show_item,
507
"Tag all untagged items"
508
)
509
510
print_(f"Selected {len(selected)} items for tagging")
511
return selected
512
```
513
514
## Color Configuration
515
516
### Color Names
517
518
```python { .api }
519
# Standard color names used by beets
520
COLOR_NAMES = [
521
'text_success', # Success messages
522
'text_warning', # Warning messages
523
'text_error', # Error messages
524
'text_highlight', # Important text
525
'text_highlight_minor', # Secondary highlights
526
'action_default', # Default action in prompts
527
'action', # Action text in prompts
528
'text', # Normal text
529
'text_faint', # Subdued text
530
'import_path', # Import path displays
531
'import_path_items', # Import item paths
532
'action_description', # Action descriptions
533
'added', # Added content
534
'removed', # Removed content
535
'changed', # Changed content
536
'added_highlight', # Highlighted additions
537
'removed_highlight', # Highlighted removals
538
'changed_highlight', # Highlighted changes
539
'text_diff_added', # Diff additions
540
'text_diff_removed', # Diff removals
541
'text_diff_changed', # Diff changes
542
]
543
```
544
545
### Color Usage Examples
546
547
```python
548
from beets.ui import colorize, print_
549
550
# Status messages
551
print_(colorize('text_success', "Import completed successfully"))
552
print_(colorize('text_warning', "Some files could not be processed"))
553
print_(colorize('text_error', "Database connection failed"))
554
555
# Highlighted content
556
artist = colorize('text_highlight', item.artist)
557
title = colorize('text_highlight_minor', item.title)
558
print_(f"{artist} - {title}")
559
560
# Action prompts
561
action = colorize('action_default', "[A]pply")
562
description = colorize('action_description', "apply changes")
563
print_(f"{action} {description}")
564
```
565
566
## Exception Handling
567
568
```python { .api }
569
class UserError(Exception):
570
"""Exception for user-facing error messages."""
571
572
def __init__(self, message: str):
573
"""
574
Initialize user error.
575
576
Parameters:
577
- message: Human-readable error message
578
"""
579
super().__init__(message)
580
```
581
582
### Error Handling Examples
583
584
```python
585
from beets.ui import UserError, print_, colorize
586
587
def safe_command_execution(lib, opts, args):
588
"""Example of proper error handling in commands."""
589
590
try:
591
# Command logic here
592
result = perform_operation(lib, args)
593
print_(colorize('text_success', f"Operation completed: {result}"))
594
595
except UserError as e:
596
print_(colorize('text_error', f"Error: {e}"))
597
return 1
598
599
except KeyboardInterrupt:
600
print_(colorize('text_warning', "\nOperation cancelled by user"))
601
return 1
602
603
except Exception as e:
604
print_(colorize('text_error', f"Unexpected error: {e}"))
605
return 1
606
607
return 0
608
```
609
610
This comprehensive UI system enables the creation of rich, interactive command-line tools that integrate seamlessly with beets' existing interface patterns and user experience conventions.