osu-play 1.3.1 → 1.3.3

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.
package/README.md CHANGED
@@ -54,15 +54,17 @@ osu-play --loop
54
54
  - `Left/Right` or `h/l`: Seek backward or forward by 5 seconds
55
55
  - `Enter`: Play the selected track
56
56
  - `Space`: Pause or resume playback
57
+ - `o`: Reveal the selected track in the native file manager
57
58
  - `n` / `p`: Next or previous track
58
59
  - `PageUp` / `PageDown`: Jump faster through the playlist
59
60
  - `r`: Toggle looping
60
61
  - `s`: Stop playback
61
62
  - `q`: Quit
62
63
  - `/`: Enter search mode
63
- - Type in search mode: Jump the selection by track title
64
+ - Type in search mode: Filter tracks by title
65
+ - `Up`/`Down` or `Ctrl+P`/`Ctrl+N` in search mode: Navigate matching tracks
64
66
  - `Backspace`: Edit the search query
65
- - `Enter`: Leave search mode and keep the current query
67
+ - `Enter` in search mode: Play the selected match, leave search entry, and keep the filter
66
68
  - `Esc`: Leave search mode, or clear the current query outside search mode
67
69
 
68
70
  ### API
package/dist/cli.cjs CHANGED
@@ -42,6 +42,30 @@ var __export = (target, all) => {
42
42
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
43
43
 
44
44
  // src/core/utils/mod.ts
45
+ var exports_mod = {};
46
+ __export(exports_mod, {
47
+ revealFile: () => revealFile,
48
+ hashedFilePath: () => hashedFilePath,
49
+ getRealmDBPath: () => getRealmDBPath,
50
+ getDefaultOsuDataDir: () => getDefaultOsuDataDir,
51
+ getDefaultConfigPath: () => getDefaultConfigPath,
52
+ getDataDir: () => getDataDir,
53
+ getConfigDir: () => getConfigDir
54
+ });
55
+ function revealFile(filePath) {
56
+ const command = process.platform === "darwin" ? { args: ["-R", filePath], executable: "open" } : process.platform === "win32" ? { args: ["/select,", filePath], executable: "explorer.exe" } : { args: [import_node_path.default.dirname(filePath)], executable: "xdg-open" };
57
+ return new Promise((resolve, reject) => {
58
+ const child = import_node_child_process.spawn(command.executable, command.args, {
59
+ detached: true,
60
+ stdio: "ignore"
61
+ });
62
+ child.once("error", reject);
63
+ child.once("spawn", () => {
64
+ child.unref();
65
+ resolve();
66
+ });
67
+ });
68
+ }
45
69
  function getDataDir() {
46
70
  switch (process.platform) {
47
71
  case "linux":
@@ -109,10 +133,11 @@ function getRealmDBPath(legacyAppConfigDirOrOptions = {}, maybeOptions = {}) {
109
133
  }
110
134
  return osuDBPath;
111
135
  }
112
- var import_node_path, import_node_fs;
136
+ var import_node_path, import_node_fs, import_node_child_process;
113
137
  var init_mod = __esm(() => {
114
138
  import_node_path = __toESM(require("node:path"));
115
139
  import_node_fs = require("node:fs");
140
+ import_node_child_process = require("node:child_process");
116
141
  });
117
142
 
118
143
  // src/core/lazer/compat.ts
@@ -415,8 +440,8 @@ var init_mod2 = __esm(() => {
415
440
  });
416
441
 
417
442
  // src/core/lazer/mod.ts
418
- var exports_mod = {};
419
- __export(exports_mod, {
443
+ var exports_mod2 = {};
444
+ __export(exports_mod2, {
420
445
  inspectLazerSchemaEntries: () => inspectLazerSchemaEntries,
421
446
  inspectLazerSchema: () => inspectLazerSchema,
422
447
  hashedFilePath: () => hashedFilePath,
@@ -555,7 +580,7 @@ function buildPlaylist(beatmapSets, osuDataDir) {
555
580
  }
556
581
 
557
582
  // src/core/player/mpv.ts
558
- var import_node_child_process = require("node:child_process");
583
+ var import_node_child_process2 = require("node:child_process");
559
584
  var import_node_fs2 = require("node:fs");
560
585
  var import_node_net = __toESM(require("node:net"));
561
586
  var import_node_os = __toESM(require("node:os"));
@@ -710,7 +735,7 @@ class MpvPlayerBackend {
710
735
  this.cleanupSocketPath();
711
736
  this.disposing = false;
712
737
  this.stderrBuffer = "";
713
- const processHandle = import_node_child_process.spawn("mpv", [
738
+ const processHandle = import_node_child_process2.spawn("mpv", [
714
739
  "--no-config",
715
740
  "--no-terminal",
716
741
  "--idle=yes",
@@ -989,6 +1014,13 @@ function findTrackIndexByQuery(playlist, query) {
989
1014
  }
990
1015
  return playlist.findIndex((track) => normalizeSearchText(track.title).includes(normalizedQuery));
991
1016
  }
1017
+ function findTrackIndicesByQuery(playlist, query) {
1018
+ const normalizedQuery = normalizeSearchText(query);
1019
+ if (!normalizedQuery) {
1020
+ return playlist.map((_, index) => index);
1021
+ }
1022
+ return playlist.flatMap((track, index) => normalizeSearchText(track.title).includes(normalizedQuery) ? [index] : []);
1023
+ }
992
1024
 
993
1025
  class PlaylistPlayerSession {
994
1026
  backend;
@@ -999,6 +1031,7 @@ class PlaylistPlayerSession {
999
1031
  deleteTrack;
1000
1032
  playlist;
1001
1033
  reloadPlaylist;
1034
+ revealTrack;
1002
1035
  searchQuery = "";
1003
1036
  selectedIndex = 0;
1004
1037
  operationQueue = Promise.resolve();
@@ -1009,6 +1042,7 @@ class PlaylistPlayerSession {
1009
1042
  this.deleteTrack = options.deleteTrack;
1010
1043
  this.loop = options.loop ?? false;
1011
1044
  this.reloadPlaylist = options.reloadPlaylist;
1045
+ this.revealTrack = options.revealTrack;
1012
1046
  this.unsubscribeBackend = backend.subscribe((event) => {
1013
1047
  this.handleBackendEvent(event);
1014
1048
  });
@@ -1049,24 +1083,44 @@ class PlaylistPlayerSession {
1049
1083
  if (this.playlist.length === 0) {
1050
1084
  return;
1051
1085
  }
1086
+ if (this.searchQuery) {
1087
+ this.moveSearchSelection(delta);
1088
+ return;
1089
+ }
1052
1090
  this.selectedIndex = wrapIndex(this.selectedIndex + delta, this.playlist.length);
1053
1091
  this.emit();
1054
1092
  }
1055
1093
  moveSelectionPage(delta, pageSize) {
1094
+ if (this.searchQuery) {
1095
+ this.moveSearchSelection(delta * Math.max(1, pageSize));
1096
+ return;
1097
+ }
1056
1098
  this.moveSelection(delta * Math.max(1, pageSize));
1057
1099
  }
1100
+ moveSearchSelection(delta) {
1101
+ const matches = findTrackIndicesByQuery(this.playlist, this.searchQuery);
1102
+ if (matches.length === 0) {
1103
+ return;
1104
+ }
1105
+ const currentMatchIndex = matches.indexOf(this.selectedIndex);
1106
+ const nextMatchIndex = wrapIndex(currentMatchIndex + delta, matches.length);
1107
+ this.selectedIndex = matches[nextMatchIndex] ?? this.selectedIndex;
1108
+ this.emit();
1109
+ }
1058
1110
  selectHome() {
1059
1111
  if (this.playlist.length === 0) {
1060
1112
  return;
1061
1113
  }
1062
- this.selectedIndex = 0;
1114
+ const matches = findTrackIndicesByQuery(this.playlist, this.searchQuery);
1115
+ this.selectedIndex = matches[0] ?? 0;
1063
1116
  this.emit();
1064
1117
  }
1065
1118
  selectEnd() {
1066
1119
  if (this.playlist.length === 0) {
1067
1120
  return;
1068
1121
  }
1069
- this.selectedIndex = this.playlist.length - 1;
1122
+ const matches = findTrackIndicesByQuery(this.playlist, this.searchQuery);
1123
+ this.selectedIndex = matches.at(-1) ?? this.playlist.length - 1;
1070
1124
  this.emit();
1071
1125
  }
1072
1126
  setSelectionIndex(index) {
@@ -1172,6 +1226,22 @@ class PlaylistPlayerSession {
1172
1226
  }
1173
1227
  });
1174
1228
  }
1229
+ async revealSelectedTrack() {
1230
+ const track = this.playlist[this.selectedIndex];
1231
+ if (!track) {
1232
+ return;
1233
+ }
1234
+ if (!this.revealTrack) {
1235
+ this.reportError(new Error("Opening the containing folder is unavailable."));
1236
+ return;
1237
+ }
1238
+ try {
1239
+ this.clearError();
1240
+ await this.revealTrack(track);
1241
+ } catch (error) {
1242
+ this.reportError(error);
1243
+ }
1244
+ }
1175
1245
  async deleteSelectedTrack() {
1176
1246
  const selectedIndex = this.selectedIndex;
1177
1247
  await this.enqueueOperation(async () => {
@@ -1473,6 +1543,10 @@ class PlaylistPlayerScreen {
1473
1543
  this.session.toggleLoop();
1474
1544
  return;
1475
1545
  }
1546
+ if (import_pi_tui.matchesKey(data, "o")) {
1547
+ this.session.revealSelectedTrack();
1548
+ return;
1549
+ }
1476
1550
  if (import_pi_tui.matchesKey(data, import_pi_tui.Key.backspace)) {
1477
1551
  this.session.deleteSearchCharacter();
1478
1552
  return;
@@ -1484,9 +1558,11 @@ class PlaylistPlayerScreen {
1484
1558
  }
1485
1559
  render(width) {
1486
1560
  const { playlist } = this.snapshot;
1561
+ const visibleIndices = this.snapshot.searchQuery ? findTrackIndicesByQuery(playlist, this.snapshot.searchQuery) : playlist.map((_, index) => index);
1562
+ const selectedVisibleIndex = Math.max(0, visibleIndices.indexOf(this.snapshot.selectedIndex));
1487
1563
  const viewportHeight = Math.max(this.getViewportHeight(), RESERVED_ROWS);
1488
1564
  const listHeight = this.getListHeight(viewportHeight);
1489
- const { start, end } = getVisibleTrackRange(this.snapshot.selectedIndex, playlist.length, listHeight);
1565
+ const { start, end } = getVisibleTrackRange(selectedVisibleIndex, visibleIndices.length, listHeight);
1490
1566
  const position = formatSeconds(this.snapshot.timePositionSeconds);
1491
1567
  const duration = formatSeconds(this.snapshot.durationSeconds);
1492
1568
  const glyph = STATUS_GLYPH[this.snapshot.status] ?? "·";
@@ -1508,8 +1584,14 @@ class PlaylistPlayerScreen {
1508
1584
  }
1509
1585
  if (playlist.length === 0) {
1510
1586
  lines.push(import_pi_tui.truncateToWidth("No tracks were found in your osu!lazer library.", width));
1587
+ } else if (visibleIndices.length === 0) {
1588
+ lines.push(import_pi_tui.truncateToWidth("No tracks match the current search.", width));
1511
1589
  } else {
1512
- for (let index = start;index < end; index += 1) {
1590
+ for (let visibleIndex = start;visibleIndex < end; visibleIndex += 1) {
1591
+ const index = visibleIndices[visibleIndex];
1592
+ if (index === undefined) {
1593
+ continue;
1594
+ }
1513
1595
  const track = playlist[index];
1514
1596
  if (!track) {
1515
1597
  continue;
@@ -1524,7 +1606,7 @@ class PlaylistPlayerScreen {
1524
1606
  while (lines.length < viewportHeight - 1) {
1525
1607
  lines.push("");
1526
1608
  }
1527
- lines.push(import_pi_tui.truncateToWidth(style(this.searchMode ? "type to jump · backspace edit · enter keep · esc leave" : "j/k move · ⏎ play · space pause · n/p track · h/l seek · r loop · d delete · / search · q quit", DIM), width));
1609
+ lines.push(import_pi_tui.truncateToWidth(style(this.searchMode ? "type to filter · ↑/↓ results · enter play · backspace edit · esc leave" : "j/k move · ⏎ play · space pause · n/p track · h/l seek · o reveal · r loop · d delete · / search · q quit", DIM), width));
1528
1610
  return lines;
1529
1611
  }
1530
1612
  armPendingGoToTop() {
@@ -1566,6 +1648,7 @@ class PlaylistPlayerScreen {
1566
1648
  }
1567
1649
  if (import_pi_tui.matchesKey(data, import_pi_tui.Key.enter)) {
1568
1650
  this.searchMode = false;
1651
+ this.session.playSelected();
1569
1652
  return;
1570
1653
  }
1571
1654
  if (import_pi_tui.matchesKey(data, import_pi_tui.Key.escape)) {
@@ -1580,8 +1663,16 @@ class PlaylistPlayerScreen {
1580
1663
  this.session.clearSearch();
1581
1664
  return;
1582
1665
  }
1666
+ if (import_pi_tui.matchesKey(data, import_pi_tui.Key.up) || import_pi_tui.matchesKey(data, import_pi_tui.Key.ctrl("p"))) {
1667
+ this.session.moveSearchSelection(-1);
1668
+ return;
1669
+ }
1670
+ if (import_pi_tui.matchesKey(data, import_pi_tui.Key.down) || import_pi_tui.matchesKey(data, import_pi_tui.Key.ctrl("n"))) {
1671
+ this.session.moveSearchSelection(1);
1672
+ return;
1673
+ }
1583
1674
  const printable = decodePrintableText(data);
1584
- if (!printable || printable === " ") {
1675
+ if (!printable) {
1585
1676
  return;
1586
1677
  }
1587
1678
  this.session.appendSearchQuery(printable);
@@ -1624,7 +1715,7 @@ async function loadRealmDependencies() {
1624
1715
  try {
1625
1716
  const [{ default: Realm8 }, lazerModule] = await Promise.all([
1626
1717
  import("realm"),
1627
- Promise.resolve().then(() => (init_mod3(), exports_mod))
1718
+ Promise.resolve().then(() => (init_mod3(), exports_mod2))
1628
1719
  ]);
1629
1720
  return {
1630
1721
  Realm: Realm8,
@@ -1689,11 +1780,13 @@ async function deleteTrackFromCollection(track, osuDataDir) {
1689
1780
  await deleteBeatmapSet2(track, osuDataDir);
1690
1781
  }
1691
1782
  async function runTuiPlayer(playlist, osuDataDir, loop) {
1783
+ const { revealFile: revealFile2 } = await Promise.resolve().then(() => (init_mod(), exports_mod));
1692
1784
  const backend = new MpvPlayerBackend;
1693
1785
  const session = new PlaylistPlayerSession(playlist, backend, {
1694
1786
  deleteTrack: (track) => deleteTrackFromCollection(track, osuDataDir),
1695
1787
  loop,
1696
- reloadPlaylist: () => createPlaylist(osuDataDir)
1788
+ reloadPlaylist: () => createPlaylist(osuDataDir),
1789
+ revealTrack: (track) => revealFile2(track.path)
1697
1790
  });
1698
1791
  const terminal = new import_pi_tui2.ProcessTerminal;
1699
1792
  const tui = new import_pi_tui2.TUI(terminal);
@@ -1752,4 +1845,4 @@ main().then(() => {
1752
1845
  process.exit(1);
1753
1846
  });
1754
1847
 
1755
- //# debugId=F18831A6D4239A5F64756E2164756E21
1848
+ //# debugId=9FB5B0E40D3B9A4364756E2164756E21