or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

configuration.mdimport-autotag.mdindex.mdlibrary-management.mdplugin-system.mdquery-system.mduser-interface.mdutilities-templates.md

user-interface.mddocs/

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.