backpack-backtrack 0.3.0__tar.gz → 0.3.2__tar.gz

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.
Files changed (140) hide show
  1. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/PKG-INFO +17 -9
  2. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/README.md +15 -7
  3. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backpack_backtrack.egg-info/PKG-INFO +17 -9
  4. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backpack_backtrack.egg-info/SOURCES.txt +1 -0
  5. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backpack_backtrack.egg-info/requires.txt +1 -1
  6. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/bulk_pattern.py +1 -1
  7. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/bulk_art.py +9 -9
  8. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/bulk_assign.py +4 -4
  9. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/bulk_menu.py +10 -10
  10. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/bulk_names.py +6 -6
  11. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/bulk_sort.py +1 -1
  12. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/tag_handler.py +5 -5
  13. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/lyrics/editor_keys.py +2 -2
  14. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/lyrics/editor_view.py +12 -10
  15. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/main.py +4 -0
  16. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/menus/__init__.py +3 -2
  17. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/menus/activity.py +1 -1
  18. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/menus/browse.py +4 -4
  19. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/menus/command_line.py +15 -8
  20. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/menus/common.py +1 -38
  21. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/menus/settings.py +33 -91
  22. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/menus/sorting.py +4 -4
  23. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/music_library.py +1 -1
  24. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/playback/now_playing_box.py +1 -1
  25. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/playback/player.py +119 -9
  26. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/playback/player_art.py +3 -0
  27. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/playback/player_geom.py +1 -1
  28. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/playback/player_ui.py +102 -49
  29. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/playback/queue_pane.py +13 -3
  30. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/trim/bulk.py +6 -10
  31. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/trim/editor.py +17 -16
  32. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/pyproject.toml +2 -2
  33. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_appearance.py +1 -1
  34. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_panels.py +25 -0
  35. backpack_backtrack-0.3.2/tests/test_player_details.py +28 -0
  36. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_player_layout.py +2 -2
  37. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_player_panes.py +7 -0
  38. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_player_tiny.py +13 -1
  39. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_queue_edit.py +20 -0
  40. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/LICENSE +0 -0
  41. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backpack_backtrack.egg-info/dependency_links.txt +0 -0
  42. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backpack_backtrack.egg-info/entry_points.txt +0 -0
  43. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backpack_backtrack.egg-info/top_level.txt +0 -0
  44. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/__init__.py +0 -0
  45. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/__main__.py +0 -0
  46. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/accents.py +0 -0
  47. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/album_art.py +0 -0
  48. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/books.py +0 -0
  49. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/chapter_edit.py +0 -0
  50. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/cli.py +0 -0
  51. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/cli_commands.py +0 -0
  52. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/config.py +0 -0
  53. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/deps.py +0 -0
  54. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/feed.py +0 -0
  55. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/history.py +0 -0
  56. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/__init__.py +0 -0
  57. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/browser.py +0 -0
  58. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/bulk_common.py +0 -0
  59. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/bulk_ops.py +0 -0
  60. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/cover_matcher.py +0 -0
  61. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/file_namer.py +0 -0
  62. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/filename_parser.py +0 -0
  63. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/languages.py +0 -0
  64. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/tag_formats.py +0 -0
  65. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/tag_registry.py +0 -0
  66. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/id3/tag_writer.py +0 -0
  67. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/lyrics/__init__.py +0 -0
  68. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/lyrics/editor.py +0 -0
  69. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/lyrics/formats.py +0 -0
  70. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/lyrics/lyric_pane.py +0 -0
  71. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/lyrics/md_overlay.py +0 -0
  72. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/lyrics/sync_doc.py +0 -0
  73. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/lyrics/text.py +0 -0
  74. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/lyrics/time_fields.py +0 -0
  75. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/lyrics/verify.py +0 -0
  76. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/menus/browse_columns.py +0 -0
  77. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/menus/chapters.py +0 -0
  78. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/menus/directory.py +0 -0
  79. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/menus/history.py +0 -0
  80. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/menus/play.py +0 -0
  81. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/menus/search.py +0 -0
  82. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/playback/__init__.py +0 -0
  83. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/playback/ipc.py +0 -0
  84. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/playback/libvlc.py +0 -0
  85. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/playback/session.py +0 -0
  86. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/search.py +0 -0
  87. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/trim/__init__.py +0 -0
  88. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/trim/engine.py +0 -0
  89. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/backtrack/tuning.py +0 -0
  90. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/setup.cfg +0 -0
  91. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_accents.py +0 -0
  92. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_album_credit.py +0 -0
  93. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_anchor_times.py +0 -0
  94. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_art_fit.py +0 -0
  95. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_audiobooks.py +0 -0
  96. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_browse_categories.py +0 -0
  97. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_browse_columns.py +0 -0
  98. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_bulk_ops.py +0 -0
  99. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_bulk_preview.py +0 -0
  100. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_chapter_edit.py +0 -0
  101. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_cli.py +0 -0
  102. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_command_line.py +0 -0
  103. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_config_setting.py +0 -0
  104. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_data_safety.py +0 -0
  105. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_deps.py +0 -0
  106. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_directory.py +0 -0
  107. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_feed.py +0 -0
  108. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_idle_player.py +0 -0
  109. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_inline_art.py +0 -0
  110. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_kv_editor.py +0 -0
  111. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_list_result.py +0 -0
  112. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_lyric_pane_layout.py +0 -0
  113. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_lyric_timing.py +0 -0
  114. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_lyrics_editor_keys.py +0 -0
  115. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_md_overlay.py +0 -0
  116. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_moved_files.py +0 -0
  117. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_player_seek_keys.py +0 -0
  118. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_player_sizes.py +0 -0
  119. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_progress_flicker.py +0 -0
  120. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_random_album.py +0 -0
  121. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_rebound_keys.py +0 -0
  122. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_regressions.py +0 -0
  123. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_search.py +0 -0
  124. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_search_disc.py +0 -0
  125. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_session_equalizer.py +0 -0
  126. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_sorting.py +0 -0
  127. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_summarize_tag_value.py +0 -0
  128. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_tag_formats.py +0 -0
  129. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_tag_id_input.py +0 -0
  130. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_tag_writer.py +0 -0
  131. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_track_title.py +0 -0
  132. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_transport_icons.py +0 -0
  133. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_trim.py +0 -0
  134. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_trim_backup.py +0 -0
  135. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_trim_bulk.py +0 -0
  136. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_trim_chapters.py +0 -0
  137. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_trim_editor.py +0 -0
  138. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_trim_loudness.py +0 -0
  139. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_trim_silence.py +0 -0
  140. {backpack_backtrack-0.3.0 → backpack_backtrack-0.3.2}/tests/test_trim_sting.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: backpack-backtrack
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Terminal music player with VLC playback, lyrics sync, and metadata editing.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/RedEraRrow/backtrack
@@ -21,7 +21,7 @@ Requires-Dist: numpy>=1.24
21
21
  Requires-Dist: pyperclip>=1.11.0
22
22
  Requires-Dist: python-vlc>=3.0.21203
23
23
  Requires-Dist: colorama>=0.4.6
24
- Requires-Dist: backpack-backbone<0.4,>=0.3
24
+ Requires-Dist: backpack-backbone<0.4,>=0.3.2
25
25
  Dynamic: license-file
26
26
 
27
27
  # Backtrack
@@ -87,9 +87,11 @@ and has a tag editor (ID3, MP4 and Vorbis comments) with bulk operations for who
87
87
 
88
88
  macOS, starting from nothing:
89
89
  ```bash
90
- # Homebrew, if you don't have it yet (it asks for your password, and on Apple
91
- # Silicon prints two lines to run afterwards: run them)
90
+ # Homebrew, if you don't have it yet (it asks for your password)
92
91
  /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
92
+ # put brew on your PATH, now and in new windows (Apple Silicon; does nothing on an Intel Mac)
93
+ echo 'eval "$(/opt/homebrew/bin/brew shellenv 2>/dev/null)"' >> ~/.zprofile
94
+ eval "$(/opt/homebrew/bin/brew shellenv 2>/dev/null)"
93
95
 
94
96
  brew install pipx
95
97
  brew install --cask vlc
@@ -155,13 +157,19 @@ last run's queue. In the player, `f` hides or shows the tab bar (also Settings
155
157
  The player sits in boxes while the tab bar or a panel shows, at any window size, and edge to edge
156
158
  only with neither (`f` hides the bar). The
157
159
  boxes fill the window with no gaps between them: the art and the details in a box as wide as the art
158
- with the panels (`w`) in columns beside it when they get room enough there, else under it; and along the
160
+ with the panels (`w`) in columns beside it wherever they fit without leaving the art much smaller
161
+ (the player's box gives up width for them; a column on each side can differ in width, as can the
162
+ panels stacked in one), else under it, as in a tall, narrow window; and along the
159
163
  bottom the controls, the progress bar, the times and the speed or sleep timer. In a short, wide
160
164
  window the art sits beside the details instead of over them, so it stays as big as it can be.
161
165
  In a tiny window it's just the transport: boxed in 3 to 5 rows, bare in 1 or 2 (and the tab bar and
162
166
  status line step aside under 6 rows). As it narrows the row gives things up in turn, never cutting
163
167
  any short: the bar, then the length (the position alone), then the time, then previous, then next,
164
- leaving play/pause, and last the box. The art is a square
168
+ leaving play/pause, and last the box. Screens never show without their boxes: one in a window
169
+ too small for them (under 10 rows, or 9 columns) gives way to that same transport, alone, until the
170
+ window grows and it comes back as you left it. Meanwhile the transport's own keys work (play/pause,
171
+ next, previous, seeking, volume) and `:` opens the command line; nothing else reaches the screen.
172
+ The art is a square
165
173
  that fills its space (a cover that isn't square is cropped about its centre), in the player,
166
174
  Browse's preview and the tag editor's picture screens alike; an audiobook's booklet picture too.
167
175
  The people panel
@@ -260,11 +268,11 @@ by screen, with more than one key per action if you like), and every hint bar sh
260
268
  | `←` / `→` | Seek ∓5 s |
261
269
  | `j` / `l` | Seek ∓1 s |
262
270
  | `,` / `.` | Seek ∓30 s |
263
- | `+` / `-` | Volume up / down |
271
+ | `+` / `-` | Volume up / down, here and on any list (the level shows for a moment over the middle of the screen); in a text field they're typed |
264
272
  | `m` | Show or hide the track details line (year · genre · disc/track …); remembered. Also Settings → Track details in player |
265
273
  | `w` | Choose the panels: a box over the player to tick Lyrics, People, Queue and Chapters (one this track has nothing for says so). Its **Arrange panels…** moves them: `Tab` from panel to panel (the one moving is outlined in the accent), `↑` / `↓` up or down its column, `←` / `→` to the other side of the player, `↵` done. A wide window and a tall one each keep their own arrangement: wide, the panels are columns beside the player (either side, or both); tall, they're under it, side by side when there's room. Remembered |
266
- | `↑` / `↓` | With the queue panel showing: a cursor through the queue. `↵` plays the track at it, `J` / `K` move it up / down, `d` removes it, `x` shuffles what's coming, `c` clears what's coming, `u` undoes the last queue change |
267
- | `?` | Show or hide the key hints, on every screen: they start hidden. Each screen's top line ends in `[?] help`; in a text field, where `?` is typed, it says `[^/] help` and the key is Ctrl+/ (which works everywhere). Clicking the key in the corner works too. In the player it stays in the hint bar. Settings → Help toggle hides `[?] help` everywhere; `?` still works |
274
+ | `↑` / `↓` | With the queue panel showing: a cursor through the queue (the mouse wheel or a trackpad over the panel moves it too). `↵` plays the track at it, `J` / `K` move it up / down, `d` removes it, `x` shuffles what's coming, `c` clears what's coming, `u` undoes the last queue change |
275
+ | `?` | Show or hide the key hints, on every screen: they start hidden. Each screen's top line ends in `[?] help`; in a text field, where `?` is typed, it says `[^/] help` and the key is Ctrl+/ (macOS's Terminal sends nothing for that, so there it says `[^g] help` and the key is Ctrl+G; either works anywhere it arrives). Clicking the key in the corner works too. In the player it stays in the hint bar. Settings → [?] help toggle hides `[?] help` everywhere; `?` still works |
268
276
  | `[` / `]` | Previous / next track |
269
277
  | `e` | Edit the playing track's tags |
270
278
  | `a` / `A` | Go to the playing track's album / artist in Browse (`Esc` from there goes up through Browse) |
@@ -61,9 +61,11 @@ and has a tag editor (ID3, MP4 and Vorbis comments) with bulk operations for who
61
61
 
62
62
  macOS, starting from nothing:
63
63
  ```bash
64
- # Homebrew, if you don't have it yet (it asks for your password, and on Apple
65
- # Silicon prints two lines to run afterwards: run them)
64
+ # Homebrew, if you don't have it yet (it asks for your password)
66
65
  /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
66
+ # put brew on your PATH, now and in new windows (Apple Silicon; does nothing on an Intel Mac)
67
+ echo 'eval "$(/opt/homebrew/bin/brew shellenv 2>/dev/null)"' >> ~/.zprofile
68
+ eval "$(/opt/homebrew/bin/brew shellenv 2>/dev/null)"
67
69
 
68
70
  brew install pipx
69
71
  brew install --cask vlc
@@ -129,13 +131,19 @@ last run's queue. In the player, `f` hides or shows the tab bar (also Settings
129
131
  The player sits in boxes while the tab bar or a panel shows, at any window size, and edge to edge
130
132
  only with neither (`f` hides the bar). The
131
133
  boxes fill the window with no gaps between them: the art and the details in a box as wide as the art
132
- with the panels (`w`) in columns beside it when they get room enough there, else under it; and along the
134
+ with the panels (`w`) in columns beside it wherever they fit without leaving the art much smaller
135
+ (the player's box gives up width for them; a column on each side can differ in width, as can the
136
+ panels stacked in one), else under it, as in a tall, narrow window; and along the
133
137
  bottom the controls, the progress bar, the times and the speed or sleep timer. In a short, wide
134
138
  window the art sits beside the details instead of over them, so it stays as big as it can be.
135
139
  In a tiny window it's just the transport: boxed in 3 to 5 rows, bare in 1 or 2 (and the tab bar and
136
140
  status line step aside under 6 rows). As it narrows the row gives things up in turn, never cutting
137
141
  any short: the bar, then the length (the position alone), then the time, then previous, then next,
138
- leaving play/pause, and last the box. The art is a square
142
+ leaving play/pause, and last the box. Screens never show without their boxes: one in a window
143
+ too small for them (under 10 rows, or 9 columns) gives way to that same transport, alone, until the
144
+ window grows and it comes back as you left it. Meanwhile the transport's own keys work (play/pause,
145
+ next, previous, seeking, volume) and `:` opens the command line; nothing else reaches the screen.
146
+ The art is a square
139
147
  that fills its space (a cover that isn't square is cropped about its centre), in the player,
140
148
  Browse's preview and the tag editor's picture screens alike; an audiobook's booklet picture too.
141
149
  The people panel
@@ -234,11 +242,11 @@ by screen, with more than one key per action if you like), and every hint bar sh
234
242
  | `←` / `→` | Seek ∓5 s |
235
243
  | `j` / `l` | Seek ∓1 s |
236
244
  | `,` / `.` | Seek ∓30 s |
237
- | `+` / `-` | Volume up / down |
245
+ | `+` / `-` | Volume up / down, here and on any list (the level shows for a moment over the middle of the screen); in a text field they're typed |
238
246
  | `m` | Show or hide the track details line (year · genre · disc/track …); remembered. Also Settings → Track details in player |
239
247
  | `w` | Choose the panels: a box over the player to tick Lyrics, People, Queue and Chapters (one this track has nothing for says so). Its **Arrange panels…** moves them: `Tab` from panel to panel (the one moving is outlined in the accent), `↑` / `↓` up or down its column, `←` / `→` to the other side of the player, `↵` done. A wide window and a tall one each keep their own arrangement: wide, the panels are columns beside the player (either side, or both); tall, they're under it, side by side when there's room. Remembered |
240
- | `↑` / `↓` | With the queue panel showing: a cursor through the queue. `↵` plays the track at it, `J` / `K` move it up / down, `d` removes it, `x` shuffles what's coming, `c` clears what's coming, `u` undoes the last queue change |
241
- | `?` | Show or hide the key hints, on every screen: they start hidden. Each screen's top line ends in `[?] help`; in a text field, where `?` is typed, it says `[^/] help` and the key is Ctrl+/ (which works everywhere). Clicking the key in the corner works too. In the player it stays in the hint bar. Settings → Help toggle hides `[?] help` everywhere; `?` still works |
248
+ | `↑` / `↓` | With the queue panel showing: a cursor through the queue (the mouse wheel or a trackpad over the panel moves it too). `↵` plays the track at it, `J` / `K` move it up / down, `d` removes it, `x` shuffles what's coming, `c` clears what's coming, `u` undoes the last queue change |
249
+ | `?` | Show or hide the key hints, on every screen: they start hidden. Each screen's top line ends in `[?] help`; in a text field, where `?` is typed, it says `[^/] help` and the key is Ctrl+/ (macOS's Terminal sends nothing for that, so there it says `[^g] help` and the key is Ctrl+G; either works anywhere it arrives). Clicking the key in the corner works too. In the player it stays in the hint bar. Settings → [?] help toggle hides `[?] help` everywhere; `?` still works |
242
250
  | `[` / `]` | Previous / next track |
243
251
  | `e` | Edit the playing track's tags |
244
252
  | `a` / `A` | Go to the playing track's album / artist in Browse (`Esc` from there goes up through Browse) |
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: backpack-backtrack
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Terminal music player with VLC playback, lyrics sync, and metadata editing.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/RedEraRrow/backtrack
@@ -21,7 +21,7 @@ Requires-Dist: numpy>=1.24
21
21
  Requires-Dist: pyperclip>=1.11.0
22
22
  Requires-Dist: python-vlc>=3.0.21203
23
23
  Requires-Dist: colorama>=0.4.6
24
- Requires-Dist: backpack-backbone<0.4,>=0.3
24
+ Requires-Dist: backpack-backbone<0.4,>=0.3.2
25
25
  Dynamic: license-file
26
26
 
27
27
  # Backtrack
@@ -87,9 +87,11 @@ and has a tag editor (ID3, MP4 and Vorbis comments) with bulk operations for who
87
87
 
88
88
  macOS, starting from nothing:
89
89
  ```bash
90
- # Homebrew, if you don't have it yet (it asks for your password, and on Apple
91
- # Silicon prints two lines to run afterwards: run them)
90
+ # Homebrew, if you don't have it yet (it asks for your password)
92
91
  /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
92
+ # put brew on your PATH, now and in new windows (Apple Silicon; does nothing on an Intel Mac)
93
+ echo 'eval "$(/opt/homebrew/bin/brew shellenv 2>/dev/null)"' >> ~/.zprofile
94
+ eval "$(/opt/homebrew/bin/brew shellenv 2>/dev/null)"
93
95
 
94
96
  brew install pipx
95
97
  brew install --cask vlc
@@ -155,13 +157,19 @@ last run's queue. In the player, `f` hides or shows the tab bar (also Settings
155
157
  The player sits in boxes while the tab bar or a panel shows, at any window size, and edge to edge
156
158
  only with neither (`f` hides the bar). The
157
159
  boxes fill the window with no gaps between them: the art and the details in a box as wide as the art
158
- with the panels (`w`) in columns beside it when they get room enough there, else under it; and along the
160
+ with the panels (`w`) in columns beside it wherever they fit without leaving the art much smaller
161
+ (the player's box gives up width for them; a column on each side can differ in width, as can the
162
+ panels stacked in one), else under it, as in a tall, narrow window; and along the
159
163
  bottom the controls, the progress bar, the times and the speed or sleep timer. In a short, wide
160
164
  window the art sits beside the details instead of over them, so it stays as big as it can be.
161
165
  In a tiny window it's just the transport: boxed in 3 to 5 rows, bare in 1 or 2 (and the tab bar and
162
166
  status line step aside under 6 rows). As it narrows the row gives things up in turn, never cutting
163
167
  any short: the bar, then the length (the position alone), then the time, then previous, then next,
164
- leaving play/pause, and last the box. The art is a square
168
+ leaving play/pause, and last the box. Screens never show without their boxes: one in a window
169
+ too small for them (under 10 rows, or 9 columns) gives way to that same transport, alone, until the
170
+ window grows and it comes back as you left it. Meanwhile the transport's own keys work (play/pause,
171
+ next, previous, seeking, volume) and `:` opens the command line; nothing else reaches the screen.
172
+ The art is a square
165
173
  that fills its space (a cover that isn't square is cropped about its centre), in the player,
166
174
  Browse's preview and the tag editor's picture screens alike; an audiobook's booklet picture too.
167
175
  The people panel
@@ -260,11 +268,11 @@ by screen, with more than one key per action if you like), and every hint bar sh
260
268
  | `←` / `→` | Seek ∓5 s |
261
269
  | `j` / `l` | Seek ∓1 s |
262
270
  | `,` / `.` | Seek ∓30 s |
263
- | `+` / `-` | Volume up / down |
271
+ | `+` / `-` | Volume up / down, here and on any list (the level shows for a moment over the middle of the screen); in a text field they're typed |
264
272
  | `m` | Show or hide the track details line (year · genre · disc/track …); remembered. Also Settings → Track details in player |
265
273
  | `w` | Choose the panels: a box over the player to tick Lyrics, People, Queue and Chapters (one this track has nothing for says so). Its **Arrange panels…** moves them: `Tab` from panel to panel (the one moving is outlined in the accent), `↑` / `↓` up or down its column, `←` / `→` to the other side of the player, `↵` done. A wide window and a tall one each keep their own arrangement: wide, the panels are columns beside the player (either side, or both); tall, they're under it, side by side when there's room. Remembered |
266
- | `↑` / `↓` | With the queue panel showing: a cursor through the queue. `↵` plays the track at it, `J` / `K` move it up / down, `d` removes it, `x` shuffles what's coming, `c` clears what's coming, `u` undoes the last queue change |
267
- | `?` | Show or hide the key hints, on every screen: they start hidden. Each screen's top line ends in `[?] help`; in a text field, where `?` is typed, it says `[^/] help` and the key is Ctrl+/ (which works everywhere). Clicking the key in the corner works too. In the player it stays in the hint bar. Settings → Help toggle hides `[?] help` everywhere; `?` still works |
274
+ | `↑` / `↓` | With the queue panel showing: a cursor through the queue (the mouse wheel or a trackpad over the panel moves it too). `↵` plays the track at it, `J` / `K` move it up / down, `d` removes it, `x` shuffles what's coming, `c` clears what's coming, `u` undoes the last queue change |
275
+ | `?` | Show or hide the key hints, on every screen: they start hidden. Each screen's top line ends in `[?] help`; in a text field, where `?` is typed, it says `[^/] help` and the key is Ctrl+/ (macOS's Terminal sends nothing for that, so there it says `[^g] help` and the key is Ctrl+G; either works anywhere it arrives). Clicking the key in the corner works too. In the player it stays in the hint bar. Settings → [?] help toggle hides `[?] help` everywhere; `?` still works |
268
276
  | `[` / `]` | Previous / next track |
269
277
  | `e` | Edit the playing track's tags |
270
278
  | `a` / `A` | Go to the playing track's album / artist in Browse (`Esc` from there goes up through Browse) |
@@ -107,6 +107,7 @@ tests/test_lyrics_editor_keys.py
107
107
  tests/test_md_overlay.py
108
108
  tests/test_moved_files.py
109
109
  tests/test_panels.py
110
+ tests/test_player_details.py
110
111
  tests/test_player_layout.py
111
112
  tests/test_player_panes.py
112
113
  tests/test_player_seek_keys.py
@@ -4,4 +4,4 @@ numpy>=1.24
4
4
  pyperclip>=1.11.0
5
5
  python-vlc>=3.0.21203
6
6
  colorama>=0.4.6
7
- backpack-backbone<0.4,>=0.3
7
+ backpack-backbone<0.4,>=0.3.2
@@ -235,7 +235,7 @@ def validate_schedule_rows(rows: list, n_tracks: int) -> tuple:
235
235
  errors.append(f"row {i}: FROM/TO must both be track positions")
236
236
  continue
237
237
  if lo > hi:
238
- errors.append(f"row {i}: FROM ({lo}) is after TO ({hi})")
238
+ errors.append(f"row {i}: FROM {lo} is after TO {hi}")
239
239
  continue
240
240
  if hi < 1 or lo > n_tracks:
241
241
  errors.append(f"row {i}: positions {lo}-{hi} are outside 1-{n_tracks}")
@@ -124,14 +124,14 @@ def set_album_art_op(paths: list, library: list, header) -> None:
124
124
  # 1) Matching strategy. Each answer is kept in `state`, so a screen reopened
125
125
  # by walking back holds what it was left holding, including covers chosen by
126
126
  # hand in the preview, which survive the plan being rebuilt.
127
- _MODES = ["Auto-detect (recommended)",
127
+ _MODES = ["Auto-detect",
128
128
  "One cover per disc / series / work",
129
129
  "Matching file name",
130
130
  "Track number / order",
131
- "Name pattern (%token%)"]
132
- _GROUPS = {"Auto (disc, else series, else work)": 'auto',
131
+ "Name pattern"]
132
+ _GROUPS = {"Auto: disc, else series, else work": 'auto',
133
133
  "Disc number": 'disc',
134
- "Series / season (SxxExx)": 'season',
134
+ "Series or season, from SxxExx": 'season',
135
135
  "Work / grouping": 'work'}
136
136
  _POLICIES = ["Fill blanks only", "Overwrite existing"]
137
137
  state: dict = {'mode': _MODES[0], 'group': list(_GROUPS)[0], 'pattern': None,
@@ -160,7 +160,7 @@ def set_album_art_op(paths: list, library: list, header) -> None:
160
160
 
161
161
  def _ask_pattern():
162
162
  """The image-name pattern; only asked for pattern matching."""
163
- if state['mode'] != "Name pattern (%token%)":
163
+ if state['mode'] != "Name pattern":
164
164
  return _SKIP
165
165
  picked = _cover_pattern_prompt(header)
166
166
  if picked is None:
@@ -232,7 +232,7 @@ def set_album_art_op(paths: list, library: list, header) -> None:
232
232
  """Cell text for a track's planned cover: label, plus a "has art" badge if replacing."""
233
233
  img = plan.get(p)
234
234
  if not img:
235
- return "none (d to choose)"
235
+ return "none, d chooses"
236
236
  label = cover_label(img, os.path.dirname(os.path.abspath(p)))
237
237
  if existing_art[p]:
238
238
  return [(label, 'normal'), (' · has art', 'static-dim')]
@@ -256,7 +256,7 @@ def set_album_art_op(paths: list, library: list, header) -> None:
256
256
  state['manual'][p] = img # remembered if this screen is revisited
257
257
  if img is None:
258
258
  plan[p] = None
259
- ch.cells = [os.path.basename(p), "none (d to choose)", '']
259
+ ch.cells = [os.path.basename(p), "none, d chooses", '']
260
260
  ch.checked = False
261
261
  else:
262
262
  plan[p] = img
@@ -338,11 +338,11 @@ def set_album_art_op(paths: list, library: list, header) -> None:
338
338
  if nk:
339
339
  bits.append(f"{nk} ticked")
340
340
  elif n_match:
341
- bits.append("0 ticked (existing art); Overwrite or Space/a to tick")
341
+ bits.append("0 ticked: they have art already. Overwrite, or Space or a, ticks them")
342
342
  return header(" · ".join(bits))()
343
343
 
344
344
  sel = prompt.select(
345
- "Preview (↵ applies):",
345
+ "Preview, ↵ applies:",
346
346
  choices=preview_choices, columns=_COVER_PREVIEW_COLUMNS,
347
347
  header=_preview_header, multi=True,
348
348
  extra_hints={'tags.details': 'choose cover'}, on_inspect=_reassign,
@@ -264,7 +264,7 @@ def bulk_fraction_editor(paths: list, tag_id: str, library: list, header) -> Non
264
264
  bits.append(f"{skipped_fmt} non-ID3 skipped")
265
265
  return header(' · '.join(bits))()
266
266
 
267
- sel = prompt.select("Preview (↵ applies):", choices=choices,
267
+ sel = prompt.select("Preview, ↵ applies:", choices=choices,
268
268
  columns=_RENUMBER_COLUMNS, header=_frac_header, multi=True)
269
269
  if sel is None:
270
270
  return
@@ -360,9 +360,9 @@ def assign_by_pattern(paths: list, library: list, header) -> None:
360
360
  def _modes() -> list:
361
361
  """The assignment modes this tag supports (schedules need a date frame)."""
362
362
  info = get_tag_info(state['tag'])
363
- modes = ["Ranges (from-to → value)", "Every N tracks → value"]
363
+ modes = ["Ranges: from-to → value", "Every N tracks → value"]
364
364
  if info and info.format_spec == 'ISO8601':
365
- modes += ["Date schedule", "Schedule per range (own start + interval)"]
365
+ modes += ["Date schedule", "Schedule per range: its own start and interval"]
366
366
  return modes
367
367
 
368
368
  def _ask_mode() -> bool:
@@ -576,7 +576,7 @@ def assign_by_pattern(paths: list, library: list, header) -> None:
576
576
  return False
577
577
  sub = f"{tag_id} · {ui.plural(len(choices), 'file')}" + (
578
578
  f" · {n_mp4} non-ID3 skipped" if n_mp4 else "")
579
- sel = prompt.select("Preview (↵ applies):", choices=choices,
579
+ sel = prompt.select("Preview, ↵ applies:", choices=choices,
580
580
  columns=_PATTERN_COLUMNS, header=header(sub), multi=True)
581
581
  if sel is None:
582
582
  return False
@@ -68,8 +68,8 @@ def renumber_tracks_op(paths: list, library: list, header) -> None:
68
68
  ui.show_status("No taggable tracks to renumber.")
69
69
  return
70
70
 
71
- _MODES = ["Continuous (album-relative): 1…N across all discs",
72
- "Per-disc (disc-relative): restart at 1 each disc"]
71
+ _MODES = ["Continuous: 1…N across all discs",
72
+ "Per disc: restart at 1 each disc"]
73
73
  state: dict = {'mode_sel': _MODES[0], 'apply_set': set()}
74
74
 
75
75
  def _ask_mode() -> bool:
@@ -99,7 +99,7 @@ def renumber_tracks_op(paths: list, library: list, header) -> None:
99
99
  for (pos, name, why), c in zip(bo.position_rows(plan), plan.changes)]
100
100
  sub = ui.plural(len(choices), "file") + (
101
101
  f" · {skipped_fmt} unsupported skipped" if skipped_fmt else "")
102
- sel = prompt.select("Preview (↵ applies):", choices=choices,
102
+ sel = prompt.select("Preview, ↵ applies:", choices=choices,
103
103
  columns=_RENUMBER_COLUMNS, header=header(sub), multi=True)
104
104
  if sel is None:
105
105
  return False
@@ -320,7 +320,7 @@ def bulk_id3_manager(library: list, album_name: str | None = None, paths: list |
320
320
  "Set album art from files",
321
321
  "Assign by range / schedule",
322
322
  "Apply sort orders",
323
- "Renumber tracks (disc ↔ continuous)",
323
+ "Renumber tracks",
324
324
  "Reflow disc numbering",
325
325
  "Remove single-disc numbering",
326
326
  "Strip stale length tags",
@@ -346,7 +346,7 @@ def bulk_id3_manager(library: list, album_name: str | None = None, paths: list |
346
346
  "Set album art from files": "Set Album Art",
347
347
  "Assign by range / schedule": "Assign By Pattern",
348
348
  "Apply sort orders": "Apply Sort Orders",
349
- "Renumber tracks (disc ↔ continuous)": "Renumber Tracks",
349
+ "Renumber tracks": "Renumber Tracks",
350
350
  "Reflow disc numbering": "Reflow Discs",
351
351
  "Remove single-disc numbering": "Strip Single Disc",
352
352
  "Strip stale length tags": "Strip Length Tags",
@@ -534,15 +534,15 @@ def bulk_id3_manager(library: list, album_name: str | None = None, paths: list |
534
534
 
535
535
  source = prompt.select(
536
536
  "Value source:",
537
- choices=["Enter a value", "Find & replace (regex)",
538
- "From file name / folder (regex)"],
537
+ choices=["Enter a value", "Find and replace with a regex",
538
+ "Regex on the file or folder name"],
539
539
  header=_bulk_header())
540
540
  if not source:
541
541
  return
542
542
 
543
543
  if source == "Enter a value":
544
544
  target_val = prompt_for_value(first_tag, current_value=fallback_val)
545
- elif source == "Find & replace (regex)":
545
+ elif source == "Find and replace with a regex":
546
546
  pat = prompt.text("Regex to find in each value:")
547
547
  if not pat:
548
548
  return
@@ -556,7 +556,7 @@ def bulk_id3_manager(library: list, album_name: str | None = None, paths: list |
556
556
  return
557
557
  set_spec = {'mode': 'replace', 'rx': rx, 'repl': repl}
558
558
  target_val = "_regex_" # sentinel so the None-guard below doesn't bail
559
- else: # From file name / folder (regex)
559
+ else: # regex on the file or folder name
560
560
  against = prompt.select("Match regex against:",
561
561
  choices=["File name", "Folder path"],
562
562
  header=_bulk_header())
@@ -686,7 +686,7 @@ def bulk_id3_manager(library: list, album_name: str | None = None, paths: list |
686
686
  return _bulk_header(" · ".join(bits))()
687
687
 
688
688
  picked_rows = prompt.select(
689
- "Preview (↵ applies):",
689
+ "Preview, ↵ applies:",
690
690
  choices=art_choices, columns=_COVER_PREVIEW_COLUMNS,
691
691
  header=_art_header, multi=True)
692
692
  if picked_rows is None:
@@ -64,7 +64,7 @@ def _detail_view(path, derived, plan: dict, present: dict, apply_fields: set,
64
64
  elif f == 'disc':
65
65
  val = _num_pair(d['disc'], d.get('total_discs'))
66
66
  if f not in supported:
67
- action = 'n/a (MP4)'
67
+ action = 'not in MP4'
68
68
  elif f in plan:
69
69
  action = 'write'
70
70
  else:
@@ -156,9 +156,9 @@ def derive_from_filename(paths: list, library: list, header) -> None:
156
156
 
157
157
  # 1) Which fields to write (Title on by default: the automatic baseline).
158
158
  _FIELDS = [("title", "Title: from file name"),
159
- ("track", "Track number (+ total)"),
160
- ("disc", "Disc number (+ total)"),
161
- ("disc_subtitle", "Disc subtitle: from folder (not MP4)"),
159
+ ("track", "Track number and total"),
160
+ ("disc", "Disc number and total"),
161
+ ("disc_subtitle", "Disc subtitle from the folder, not MP4"),
162
162
  ("album", "Album: from folder"),
163
163
  ("album_artist", "Album artist: from parent folder"),
164
164
  ("artist", "Track artist: from file name / folder"),
@@ -329,7 +329,7 @@ def derive_from_filename(paths: list, library: list, header) -> None:
329
329
  apply_fields, overwrite, header)
330
330
 
331
331
  selected = prompt.select(
332
- "Preview (↵ applies):",
332
+ "Preview, ↵ applies:",
333
333
  choices=preview_choices, columns=prev_cols,
334
334
  header=header(sub), multi=True,
335
335
  extra_hints={'tags.details': 'details'}, on_inspect=_show_detail,
@@ -447,7 +447,7 @@ def rename_files_op(paths: list, library: list, header) -> None:
447
447
  for i, (p, o, n) in enumerate(changed)]
448
448
  sub = f"{len(changed)} to rename" + (
449
449
  f" · {skipped_fmt} unsupported skipped" if skipped_fmt else "")
450
- sel = prompt.select("Preview (↵ renames):", choices=choices,
450
+ sel = prompt.select("Preview, ↵ renames:", choices=choices,
451
451
  columns=_RENAME_COLUMNS, header=header(sub), multi=True)
452
452
  if sel is None:
453
453
  return False
@@ -238,7 +238,7 @@ def _review_sort_people(plan: _SortPlan, header, note: str = "") -> set | None:
238
238
  choices = _choices()
239
239
  files = len({p for ps in plan.entries.values() for p in ps})
240
240
  sub = f"{ui.plural(len(choices), 'name')} · {ui.plural(files, 'file')}{note}"
241
- sel = prompt.select("Preview (↵ applies):", choices=choices,
241
+ sel = prompt.select("Preview, ↵ applies:", choices=choices,
242
242
  columns=_SORT_VALUE_COLUMNS, header=header(sub), multi=True,
243
243
  row_edit=_options, row_edit_commit=_commit, row_edit_col=1)
244
244
  return None if sel is None else set(sel)
@@ -37,7 +37,7 @@ _EXT_TO_MIME: dict[str, str] = {
37
37
  keys.define("tags", "Tag editor", [
38
38
  ("add", ("a",), "add a tag, or a person"),
39
39
  ("trim", ("t",), "trim the audio"),
40
- ("details", ("d",), "the row's details (description, cover, preview)"),
40
+ ("details", ("d",), "the row's details: description, cover, preview"),
41
41
  ], within=("list", "global"))
42
42
 
43
43
 
@@ -148,7 +148,7 @@ def pick_nearby_cover(file_path: str, *, tokens: dict | None = None,
148
148
  choices.append(prompt.Choice(title="No art for this file", value='__none__',
149
149
  cells=["No art for this file", '', '']))
150
150
 
151
- msg = title or "Album art (top row is the best guess):"
151
+ msg = title or "Album art, best guess first:"
152
152
  hdr = header(ui.plural(len(ranked), "candidate")) if header else None
153
153
  sel = prompt.select(msg, choices=choices, columns=_COVER_PICK_COLUMNS,
154
154
  header=hdr, index=cur_idx)
@@ -510,7 +510,7 @@ def _prompt_for_equalisation(current_value: Any) -> dict | None:
510
510
  def _prompt_for_rva2(current_value: Any) -> dict | None:
511
511
  """Interactive gain meter for a single-channel RVA2 frame."""
512
512
  cur = float(current_value.gain) if (current_value is not None and hasattr(current_value, 'gain')) else 0.0
513
- gain = prompt.rva2_edit("Volume adjustment (master channel):", gain=cur)
513
+ gain = prompt.rva2_edit("Master volume adjustment:", gain=cur)
514
514
  if gain is None:
515
515
  return None
516
516
  return {'__rva2__': True, 'gain': gain}
@@ -683,7 +683,7 @@ def _prompt_for_compilation(current_value: Any) -> str | None:
683
683
  """Yes/No toggle for the TCMP compilation flag (stored as '1'/'0')."""
684
684
  cur = _current_text(current_value).strip()
685
685
  is_comp = cur not in ('', '0')
686
- sel = prompt.select("Part of a compilation (various-artists album)?",
686
+ sel = prompt.select("Part of a various-artists compilation?",
687
687
  choices=[prompt.Choice(title='Yes', value='1'),
688
688
  prompt.Choice(title='No', value='0')],
689
689
  index=0 if is_comp else 1)
@@ -693,7 +693,7 @@ def _prompt_for_compilation(current_value: Any) -> str | None:
693
693
  def _prompt_for_rbuf(current_value: Any) -> dict | None:
694
694
  """Numeric editor for the RBUF recommended-buffer-size frame (bytes)."""
695
695
  cur = int(getattr(current_value, 'size', 0) or 0) if current_value is not None else 0
696
- n = prompt.number_edit("Recommended buffer size (bytes):", value=cur, minimum=0)
696
+ n = prompt.number_edit("Recommended buffer size in bytes:", value=cur, minimum=0)
697
697
  if not isinstance(n, int):
698
698
  return None
699
699
  return {'__rbuf__': True, 'size': int(n)}
@@ -376,7 +376,7 @@ class _KeyHandlers:
376
376
  _restore_term_attrs(self.fd, self.old)
377
377
  sys.stdout.write("\033[?1000l\033[?1006l")
378
378
  _ans = _prompt_text(
379
- f"Write spoken words (Whisper format) to "
379
+ f"Write spoken words in Whisper format to "
380
380
  f"{os.path.basename(self.aux['jpath'])}? (y/N)")
381
381
  _set_raw(self.fd)
382
382
  sys.stdout.write("\033[?1000h\033[?1006h")
@@ -468,7 +468,7 @@ class _KeyHandlers:
468
468
  _restore_term_attrs(self.fd, self.old)
469
469
  sys.stdout.write("\033[?1000l\033[?1006l")
470
470
  _prompt = ("Stage direction text:" if self.segs[self.cursor].get("kind") == "stage_dir"
471
- else "Stage direction (blank = silence):")
471
+ else "Stage direction, or blank for silence:")
472
472
  _new_lbl = _prompt_text(_prompt, default=_cur_lbl)
473
473
  _set_raw(self.fd)
474
474
  sys.stdout.write("\033[?1000h\033[?1006h")
@@ -36,8 +36,8 @@ keys.define("lyrics", "Lyrics editor", [
36
36
  ("play", ("p",), "play / stop"),
37
37
  ("save", ("s",), "save the working copy"),
38
38
  ("undo", ("u",), "undo"),
39
- ("verify", ("V",), "verify against the markdown (transcripts)"),
40
- ("write", ("W",), "write the transcript file (transcripts)"),
39
+ ("verify", ("V",), "verify against the markdown, for transcripts"),
40
+ ("write", ("W",), "write the transcript file, for transcripts"),
41
41
  ("back", ("ESC",), "back"),
42
42
  ("quit", ("q",), "quit the app"),
43
43
  ])
@@ -48,7 +48,7 @@ keys.define("lyrics_lines", "Lyrics editor: lines", [
48
48
  ("move_up", ("J",), "move the line up"),
49
49
  ("move_down", ("K",), "move the line down"),
50
50
  ("split", ("/",), "split the line"),
51
- ("words", ("w",), "open the line's words (transcripts)"),
51
+ ("words", ("w",), "open the line's words, for transcripts"),
52
52
  ("tap", ("t",), "tap sync"),
53
53
  ("audition", ("b",), "audition"),
54
54
  ("dead_air", ("a",), "add dead air"),
@@ -60,7 +60,7 @@ keys.define("lyrics_lines", "Lyrics editor: lines", [
60
60
  ("overlay", ("m",), "import or remove the markdown"),
61
61
  ("commit_dirs", ("M",), "commit stage directions"),
62
62
  ("credits", ("c",), "add credits"),
63
- ("speaker_split", ("S",), "split by speaker (transcripts)"),
63
+ ("speaker_split", ("S",), "split by speaker, for transcripts"),
64
64
  ("review", ("R",), "review issues"),
65
65
  ("review_dirs", ("D",), "review directions"),
66
66
  ("review_long", ("L",), "review long lines"),
@@ -343,7 +343,7 @@ def _draw(segs, cursor, seg_cursor, mode, prev_mode, selected, viewport,
343
343
  (K('lyrics_lines.label'), 'label'), (K('lyrics_lines.air_dir'), 'air↔dir'),
344
344
  (K('lyrics_lines.fill_gaps'), 'fill gaps'), (K('lyrics_lines.overlay'), _md_label)]
345
345
  if is_sdir:
346
- pairs.append((K('lyrics_lines.kind'), 'kind (inline/tone/external)'))
346
+ pairs.append((K('lyrics_lines.kind'), 'kind'))
347
347
  if _has_sdir: pairs.append((K('lyrics_lines.commit_dirs'), 'commit stage dirs'))
348
348
  pairs += [(K('lyrics_lines.credits'), 'credits'), (K('lyrics_lines.mark'), 'mark')]
349
349
  if _HAS_VLC: pairs.append((K('lyrics.play'), 'preview'))
@@ -491,7 +491,8 @@ def _draw_tap(B, avail, cursor, hit_map, indent, mode, n, out, play_pos, playing
491
491
  if total_s > 0:
492
492
  pct = min(play_pos / total_s, 1.0)
493
493
  _clock = f"{timefmt.clock(play_pos)} / {timefmt.clock(total_s)}"
494
- bar = ui.get_progress_bar(pct, max(4, B - len(_clock) - 4)) # -4: bar's own [ ] + 2-space gap
494
+ caps = not _promptmod.box_fits()
495
+ bar = ui.get_progress_bar(pct, max(4, B - len(_clock) - 2 - ui.progress_caps_width(caps)), caps=caps) # 2: gap before the clock
495
496
  out.append(f"{indent}{C.DIM}{bar} {_clock}{C.RESET}")
496
497
  else:
497
498
  out.append("")
@@ -563,11 +564,12 @@ def _draw_audition(B, aud_editing, aud_now, avail, cursor, edit, hit_map, indent
563
564
  if total_s > 0:
564
565
  pct = min(play_pos / total_s, 1.0)
565
566
  _clock = f"{timefmt.clock(play_pos)} / {timefmt.clock(total_s)}"
566
- _bar_w = max(4, B - len(_clock) - 4) # -4: bar's own [ ] + 2-space gap
567
- bar = ui.get_progress_bar(pct, _bar_w)
567
+ caps = not _promptmod.box_fits()
568
+ _bar_w = max(4, B - len(_clock) - 2 - ui.progress_caps_width(caps)) # 2: gap before the clock
569
+ bar = ui.get_progress_bar(pct, _bar_w, caps=caps)
568
570
  out.append(f"{indent}{C.DIM}{bar} {_clock}{C.RESET}")
569
- # Clickable: the bar's first cell sits one column past its '[' cap.
570
- prog_geo = (len(out) - 1, ui.MARGIN_H + 2, _bar_w)
571
+ # Clickable: the bar's first cell sits just past its left cap.
572
+ prog_geo = (len(out) - 1, ui.MARGIN_H + 1 + len(ui.progress_caps(caps)[0]), _bar_w)
571
573
  else:
572
574
  out.append("")
573
575
 
@@ -177,6 +177,10 @@ def _wire_playback() -> None:
177
177
  a.next()
178
178
  elif action == 'prev':
179
179
  a.prev()
180
+ elif action in ('vol_up', 'vol_down'):
181
+ a.set_volume(max(0, min(100, a.get_volume() + (5 if action == 'vol_up' else -5))))
182
+ from backtrack.playback.player_ui import volume_changed
183
+ volume_changed() # the level, for a moment, over whatever's showing
180
184
  elif action == 'time':
181
185
  from backtrack.playback.player_ui import toggle_chapter_time
182
186
  toggle_chapter_time()
@@ -12,9 +12,10 @@ def main_menu(library_ref: list, on_start=None) -> None:
12
12
  """Run the app as tabs, starting on Browse, until q (or Settings → Exit).
13
13
  `on_start()` runs first, on Browse (the offer to resume), so what it opens
14
14
  has the tabs too."""
15
- from backtrack.playback.player import NOW_PLAYING_TAB
15
+ from backtrack.playback.player import NOW_PLAYING_TAB, cramped_view
16
16
  from backtrack.menus.command_line import command_line
17
- prompt.set_command_line(lambda: command_line(library_ref))
17
+ prompt.set_command_line(lambda around=None: command_line(library_ref, around))
18
+ prompt.core.set_cramped_view(cramped_view)
18
19
  pending = [on_start]
19
20
 
20
21
  def browse() -> None:
@@ -63,7 +63,7 @@ def activity_centre() -> None:
63
63
  key = ''
64
64
  if key == '\x1b' or keys.action(key, 'list') in ('list.back', 'list.quit'):
65
65
  break
66
- time.sleep(0.08)
66
+ pc._wait_for_keypress(0.08) # the shared tick: the miniplayer when too small
67
67
  finally:
68
68
  sys.stdout.write("\033[?1000l\033[?1006l") # disable mouse
69
69
  sys.stdout.flush()