@involvex/youtube-music-cli 0.0.6 → 0.0.8

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/CHANGELOG.md CHANGED
@@ -1,3 +1,20 @@
1
+ ## [0.0.8](https://github.com/involvex/youtube-music-cli/compare/v0.0.7...v0.0.8) (2026-02-18)
2
+
3
+ ### Features
4
+
5
+ - **ui:** add keyboard blocker to search bar ([ee533fe](https://github.com/involvex/youtube-music-cli/commit/ee533fe716a203d432170e36149389f5bd48d687))
6
+
7
+ ## [0.0.7](https://github.com/involvex/youtube-music-cli/compare/v0.0.6...v0.0.7) (2026-02-18)
8
+
9
+ ### Features
10
+
11
+ - **ui:** add plugins and enhance playlist management ([83a043c](https://github.com/involvex/youtube-music-cli/commit/83a043c0956da7023ea198468db8bbc2ce3acb0d))
12
+
13
+ ### BREAKING CHANGES
14
+
15
+ - **ui:** The keybinding for playlists has changed from 'p' to
16
+ 'shift+p' to accommodate the new plugins feature.
17
+
1
18
  ## [0.0.6](https://github.com/involvex/youtube-music-cli/compare/v0.0.5...v0.0.6) (2026-02-18)
2
19
 
3
20
  ### Features
@@ -6,5 +6,5 @@ import { useNavigation } from "../../hooks/useNavigation.js";
6
6
  export default function Help() {
7
7
  const { theme } = useTheme();
8
8
  const { dispatch: _dispatch } = useNavigation();
9
- return (_jsxs(Box, { flexDirection: "column", gap: 1, padding: 1, children: [_jsx(Box, { borderStyle: "single", borderColor: theme.colors.secondary, paddingX: 1, children: _jsx(Text, { bold: true, color: theme.colors.primary, children: "Keyboard Shortcuts" }) }), _jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "Global" }), _jsx(Box, { paddingX: 2, children: _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.text, children: "q / Esc" }), " - Quit", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "?" }), " - Help", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "/" }), " - Search", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "p" }), " - Playlists", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "g" }), " - Suggestions", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "," }), " - Settings"] }) }), _jsx(Text, { bold: true, color: theme.colors.secondary, children: "Player" }), _jsx(Box, { paddingX: 2, children: _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.text, children: "Space" }), " - Play/Pause", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "n" }), " - Next", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "b" }), " - Previous", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "=" }), " - Volume Up", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "-" }), " - Volume Down", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "s" }), " - Toggle Shuffle", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "r" }), " - Toggle Repeat"] }) }), _jsx(Text, { bold: true, color: theme.colors.secondary, children: "Navigation" }), _jsx(Box, { paddingX: 2, children: _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.text, children: "Up" }), " /", _jsx(Text, { children: " " }), _jsx(Text, { color: theme.colors.text, children: "k" }), " - Move Up", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "Down" }), " /", _jsx(Text, { children: " " }), _jsx(Text, { color: theme.colors.text, children: "j" }), " - Move Down", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "Enter" }), " - Select", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "Esc" }), " - Go Back"] }) }), _jsx(Text, { bold: true, color: theme.colors.secondary, children: "Search" }), _jsx(Box, { paddingX: 2, children: _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.text, children: "Tab" }), " - Switch Search Type", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "Esc" }), " - Clear Search", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "[ / ]" }), " - Results Limit"] }) }), _jsx(Text, { bold: true, color: theme.colors.secondary, children: "Playlist" }), _jsx(Box, { paddingX: 2, children: _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.text, children: "a" }), " - Add to Playlist", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "d" }), " - Remove from Playlist", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "c" }), " - Create Playlist", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "D" }), " - Delete Playlist"] }) }), _jsxs(Text, { color: theme.colors.dim, children: ["Press ", _jsx(Text, { color: theme.colors.text, children: "Esc" }), " or", ' ', _jsx(Text, { color: theme.colors.text, children: "?" }), " to close"] })] })] }));
9
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, padding: 1, children: [_jsx(Box, { borderStyle: "single", borderColor: theme.colors.secondary, paddingX: 1, children: _jsx(Text, { bold: true, color: theme.colors.primary, children: "Keyboard Shortcuts" }) }), _jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "Global" }), _jsx(Box, { paddingX: 2, children: _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.text, children: "q / Esc" }), " - Quit", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "?" }), " - Help", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "/" }), " - Search", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "Shift+P" }), " - Playlists", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "p" }), " - Plugins", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "g" }), " - Suggestions", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "," }), " - Settings"] }) }), _jsx(Text, { bold: true, color: theme.colors.secondary, children: "Player" }), _jsx(Box, { paddingX: 2, children: _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.text, children: "Space" }), " - Play/Pause", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "n" }), " - Next", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "b" }), " - Previous", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "=" }), " - Volume Up", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "-" }), " - Volume Down", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "s" }), " - Toggle Shuffle", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "r" }), " - Toggle Repeat"] }) }), _jsx(Text, { bold: true, color: theme.colors.secondary, children: "Navigation" }), _jsx(Box, { paddingX: 2, children: _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.text, children: "Up" }), " /", _jsx(Text, { children: " " }), _jsx(Text, { color: theme.colors.text, children: "k" }), " - Move Up", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "Down" }), " /", _jsx(Text, { children: " " }), _jsx(Text, { color: theme.colors.text, children: "j" }), " - Move Down", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "Enter" }), " - Select", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "Esc" }), " - Go Back"] }) }), _jsx(Text, { bold: true, color: theme.colors.secondary, children: "Search" }), _jsx(Box, { paddingX: 2, children: _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.text, children: "Tab" }), " - Switch Search Type", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "Esc" }), " - Clear Search", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "[ / ]" }), " - Results Limit"] }) }), _jsx(Text, { bold: true, color: theme.colors.secondary, children: "Playlist" }), _jsx(Box, { paddingX: 2, children: _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.text, children: "a" }), " - Add to Playlist", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "d" }), " - Remove from Playlist", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "c" }), " - Create Playlist", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "D" }), " - Delete Playlist"] }) }), _jsxs(Text, { color: theme.colors.dim, children: ["Press ", _jsx(Text, { color: theme.colors.text, children: "Esc" }), " or", ' ', _jsx(Text, { color: theme.colors.text, children: "?" }), " to close"] })] })] }));
10
10
  }
@@ -31,5 +31,5 @@ export default function ShortcutsBar() {
31
31
  useKeyBinding(KEYBINDINGS.VOLUME_FINE_UP, volumeFineUp);
32
32
  useKeyBinding(KEYBINDINGS.VOLUME_FINE_DOWN, volumeFineDown);
33
33
  useKeyBinding(KEYBINDINGS.SETTINGS, goConfig);
34
- return (_jsxs(Box, { borderStyle: "single", borderColor: theme.colors.dim, paddingX: 1, justifyContent: "space-between", children: [_jsxs(Text, { color: theme.colors.dim, children: ["Shortcuts: ", _jsx(Text, { color: theme.colors.text, children: "Space" }), " Play/Pause |", ' ', _jsx(Text, { color: theme.colors.text, children: "n" }), " Next |", ' ', _jsx(Text, { color: theme.colors.text, children: "p" }), " Previous |", ' ', _jsx(Text, { color: theme.colors.text, children: "/" }), " Search |", ' ', _jsx(Text, { color: theme.colors.text, children: "," }), " Settings |", ' ', _jsx(Text, { color: theme.colors.text, children: "?" }), " Help |", ' ', _jsx(Text, { color: theme.colors.text, children: "q" }), " Quit"] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.dim, children: "[=/" }), "-", _jsx(Text, { color: theme.colors.dim, children: "]" }), " Vol:", ' ', _jsxs(Text, { color: theme.colors.primary, children: [playerState.volume, "%"] })] })] }));
34
+ return (_jsxs(Box, { borderStyle: "single", borderColor: theme.colors.dim, paddingX: 1, justifyContent: "space-between", children: [_jsxs(Text, { color: theme.colors.dim, children: ["Shortcuts: ", _jsx(Text, { color: theme.colors.text, children: "Space" }), " Play/Pause |", ' ', _jsx(Text, { color: theme.colors.text, children: "\u2192" }), " Next |", ' ', _jsx(Text, { color: theme.colors.text, children: "\u2190" }), " Previous |", ' ', _jsx(Text, { color: theme.colors.text, children: "Shift+P" }), " Playlists |", ' ', _jsx(Text, { color: theme.colors.text, children: "p" }), " Plugins |", ' ', _jsx(Text, { color: theme.colors.text, children: "/" }), " Search |", ' ', _jsx(Text, { color: theme.colors.text, children: "," }), " Settings |", ' ', _jsx(Text, { color: theme.colors.text, children: "?" }), " Help |", ' ', _jsx(Text, { color: theme.colors.text, children: "q" }), " Quit"] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.dim, children: "[=/" }), "-", _jsx(Text, { color: theme.colors.dim, children: "]" }), " Vol:", ' ', _jsxs(Text, { color: theme.colors.primary, children: [playerState.volume, "%"] })] })] }));
35
35
  }
@@ -39,6 +39,9 @@ function MainLayout() {
39
39
  const goToSuggestions = useCallback(() => {
40
40
  dispatch({ category: 'NAVIGATE', view: VIEW.SUGGESTIONS });
41
41
  }, [dispatch]);
42
+ const goToPlugins = useCallback(() => {
43
+ dispatch({ category: 'NAVIGATE', view: VIEW.PLUGINS });
44
+ }, [dispatch]);
42
45
  const goToSettings = useCallback(() => {
43
46
  dispatch({ category: 'NAVIGATE', view: VIEW.SETTINGS });
44
47
  }, [dispatch]);
@@ -69,6 +72,7 @@ function MainLayout() {
69
72
  useKeyBinding(KEYBINDINGS.QUIT, handleQuit);
70
73
  useKeyBinding(KEYBINDINGS.SEARCH, goToSearch);
71
74
  useKeyBinding(KEYBINDINGS.PLAYLISTS, goToPlaylists);
75
+ useKeyBinding(KEYBINDINGS.PLUGINS, goToPlugins);
72
76
  useKeyBinding(KEYBINDINGS.SUGGESTIONS, goToSuggestions);
73
77
  useKeyBinding(KEYBINDINGS.SETTINGS, goToSettings);
74
78
  useKeyBinding(KEYBINDINGS.HELP, goToHelp);
@@ -1,20 +1,82 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // Playlist list component
3
3
  import { Box, Text } from 'ink';
4
- import { useTheme } from "../../hooks/useTheme.js";
5
- import { usePlaylist } from "../../hooks/usePlaylist.js";
4
+ import TextInput from 'ink-text-input';
5
+ import { useCallback, useState } from 'react';
6
+ import { useNavigation } from "../../hooks/useNavigation.js";
6
7
  import { useKeyBinding } from "../../hooks/useKeyboard.js";
8
+ import { usePlayer } from "../../hooks/usePlayer.js";
9
+ import { usePlaylist } from "../../hooks/usePlaylist.js";
10
+ import { useTheme } from "../../hooks/useTheme.js";
11
+ import { useKeyboardBlocker } from "../../hooks/useKeyboardBlocker.js";
7
12
  import { KEYBINDINGS } from "../../utils/constants.js";
8
- import { useState, useCallback } from 'react';
9
13
  export default function PlaylistList() {
10
14
  const { theme } = useTheme();
11
- const { playlists, createPlaylist } = usePlaylist();
15
+ const { play, setQueue } = usePlayer();
16
+ const { dispatch } = useNavigation();
17
+ const { playlists, createPlaylist, renamePlaylist } = usePlaylist();
18
+ const [selectedIndex, setSelectedIndex] = useState(0);
12
19
  const [lastCreated, setLastCreated] = useState(null);
20
+ const [renamingPlaylistId, setRenamingPlaylistId] = useState(null);
21
+ const [renameValue, setRenameValue] = useState('');
22
+ useKeyboardBlocker(renamingPlaylistId !== null);
13
23
  const handleCreate = useCallback(() => {
14
24
  const name = `Playlist ${playlists.length + 1}`;
15
25
  createPlaylist(name);
16
26
  setLastCreated(name);
17
- }, [playlists.length, createPlaylist]);
27
+ setSelectedIndex(playlists.length);
28
+ }, [createPlaylist, playlists.length]);
29
+ const navigateUp = useCallback(() => {
30
+ setSelectedIndex(prev => Math.max(0, prev - 1));
31
+ }, []);
32
+ const navigateDown = useCallback(() => {
33
+ setSelectedIndex(prev => Math.min(playlists.length === 0 ? 0 : playlists.length - 1, prev + 1));
34
+ }, [playlists.length]);
35
+ const startPlaylist = useCallback(() => {
36
+ if (renamingPlaylistId)
37
+ return;
38
+ const playlist = playlists[selectedIndex];
39
+ if (!playlist || playlist.tracks.length === 0)
40
+ return;
41
+ setQueue([...playlist.tracks]);
42
+ const firstTrack = playlist.tracks[0];
43
+ if (!firstTrack)
44
+ return;
45
+ play(firstTrack);
46
+ }, [play, playlists, selectedIndex, renamingPlaylistId, setQueue]);
47
+ const handleRename = useCallback(() => {
48
+ const playlist = playlists[selectedIndex];
49
+ if (!playlist)
50
+ return;
51
+ setRenamingPlaylistId(playlist.playlistId);
52
+ setRenameValue(playlist.name);
53
+ }, [playlists, selectedIndex]);
54
+ const handleRenameSubmit = useCallback((value) => {
55
+ if (!renamingPlaylistId)
56
+ return;
57
+ const trimmedValue = value.trim() || `Playlist ${selectedIndex + 1}`;
58
+ renamePlaylist(renamingPlaylistId, trimmedValue);
59
+ setRenamingPlaylistId(null);
60
+ setRenameValue('');
61
+ }, [renamePlaylist, renamingPlaylistId, selectedIndex]);
62
+ const handleBack = useCallback(() => {
63
+ if (renamingPlaylistId) {
64
+ setRenamingPlaylistId(null);
65
+ setRenameValue('');
66
+ return;
67
+ }
68
+ dispatch({ category: 'GO_BACK' });
69
+ }, [dispatch, renamingPlaylistId]);
70
+ useKeyBinding(KEYBINDINGS.UP, navigateUp);
71
+ useKeyBinding(KEYBINDINGS.DOWN, navigateDown);
72
+ useKeyBinding(KEYBINDINGS.SELECT, startPlaylist);
73
+ useKeyBinding(['r'], handleRename);
18
74
  useKeyBinding(KEYBINDINGS.CREATE_PLAYLIST, handleCreate);
19
- return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Box, { borderStyle: "double", borderColor: theme.colors.secondary, paddingX: 1, marginBottom: 1, children: _jsx(Text, { bold: true, color: theme.colors.primary, children: "Playlists" }) }), playlists.length === 0 ? (_jsx(Text, { color: theme.colors.dim, children: "No playlists yet" })) : (playlists.map((playlist, index) => (_jsxs(Box, { paddingX: 1, children: [_jsxs(Text, { color: theme.colors.primary, children: [index + 1, "."] }), _jsx(Text, { children: " " }), _jsx(Text, { color: theme.colors.text, children: playlist.name }), _jsxs(Text, { color: theme.colors.dim, children: [_jsx(Text, { children: " " }), "(", playlist.tracks?.length || 0, " tracks)"] })] }, playlist.playlistId || index)))), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: theme.colors.dim, children: ["Press ", _jsx(Text, { color: theme.colors.text, children: "c" }), " to create playlist", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "Esc" }), " to go back"] }), lastCreated && (_jsxs(Text, { color: theme.colors.accent, children: [" Created ", lastCreated] }))] })] }));
75
+ useKeyBinding(KEYBINDINGS.BACK, handleBack);
76
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Box, { borderStyle: "double", borderColor: theme.colors.secondary, paddingX: 1, marginBottom: 1, children: _jsx(Text, { bold: true, color: theme.colors.primary, children: "Playlists" }) }), playlists.length === 0 ? (_jsx(Text, { color: theme.colors.dim, children: "No playlists yet" })) : (playlists.map((playlist, index) => {
77
+ const isSelected = index === selectedIndex;
78
+ const isRenaming = renamingPlaylistId === playlist.playlistId && isSelected;
79
+ const rowBackground = isSelected ? theme.colors.secondary : undefined;
80
+ return (_jsxs(Box, { paddingX: 1, backgroundColor: rowBackground, children: [_jsxs(Text, { color: isSelected ? theme.colors.background : theme.colors.primary, bold: isSelected, children: [index + 1, "."] }), _jsx(Text, { children: " " }), _jsxs(Box, { flexDirection: "column", children: [isRenaming ? (_jsx(TextInput, { value: renameValue, onChange: setRenameValue, onSubmit: handleRenameSubmit, placeholder: "Playlist name", focus: true })) : (_jsx(Text, { color: isSelected ? theme.colors.background : theme.colors.text, bold: isSelected, children: playlist.name })), _jsx(Text, { color: theme.colors.dim, children: ` (${playlist.tracks.length} tracks)` })] })] }, playlist.playlistId));
81
+ })), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: theme.colors.dim, children: [_jsx(Text, { color: theme.colors.text, children: "Enter" }), " to play playlist |", ' ', _jsx(Text, { color: theme.colors.text, children: "r" }), " to rename |", ' ', _jsx(Text, { color: theme.colors.text, children: "c" }), " to create |", ' ', _jsx(Text, { color: theme.colors.text, children: "Esc" }), " to go back"] }), lastCreated && (_jsxs(Text, { color: theme.colors.accent, children: [" Created ", lastCreated] }))] })] }));
20
82
  }
@@ -6,6 +6,7 @@ import React from 'react';
6
6
  import { SEARCH_TYPE } from "../../utils/constants.js";
7
7
  import { useTheme } from "../../hooks/useTheme.js";
8
8
  import { useKeyBinding } from "../../hooks/useKeyboard.js";
9
+ import { useKeyboardBlocker } from "../../hooks/useKeyboardBlocker.js";
9
10
  import { Box, Text } from 'ink';
10
11
  import TextInput from 'ink-text-input';
11
12
  import { getConfigService } from "../../services/config/config.service.js";
@@ -46,6 +47,7 @@ function SearchBar({ onInput, isActive = true }) {
46
47
  }, [isActive, onInput]);
47
48
  useKeyBinding(['tab'], cycleType);
48
49
  useKeyBinding(['escape'], clearSearch);
50
+ useKeyboardBlocker(isActive);
49
51
  return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.secondary, padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: theme.colors.dim, children: "Type: " }), searchTypes.map((type, index) => (_jsxs(Text, { color: navState.searchType === type
50
52
  ? theme.colors.primary
51
53
  : theme.colors.dim, bold: navState.searchType === type, children: [type, index < searchTypes.length - 1 && ' '] }, type))), _jsx(Text, { color: theme.colors.dim, children: " (Tab to switch)" })] }), isActive && (_jsxs(Box, { children: [_jsx(Text, { color: theme.colors.primary, children: "Search: " }), _jsx(TextInput, { value: input, onChange: setInput, onSubmit: handleSubmit, placeholder: "Type to search...", focus: isActive })] })), !isActive && (_jsxs(Box, { children: [_jsx(Text, { color: theme.colors.primary, children: "Search: " }), _jsx(Text, { color: theme.colors.dim, children: input || 'Type to search...' })] })), _jsx(Text, { color: theme.colors.dim, children: "Type to search, Enter to search, Tab to change type, Esc to clear" })] }));
@@ -2,6 +2,7 @@
2
2
  import { useCallback, useEffect } from 'react';
3
3
  import { useInput } from 'ink';
4
4
  import { logger } from "../services/logger/logger.service.js";
5
+ import { useKeyboardBlockContext } from "./useKeyboardBlocker.js";
5
6
  // Global registry for key handlers
6
7
  const registry = new Set();
7
8
  /**
@@ -23,7 +24,13 @@ export function useKeyBinding(keys, handler) {
23
24
  * This should be rendered once at the root of the app.
24
25
  */
25
26
  export function KeyboardManager() {
27
+ const { blockCount } = useKeyboardBlockContext();
26
28
  useInput((input, key) => {
29
+ if (blockCount > 0) {
30
+ // When keyboard input is blocked (e.g., within a focused text input),
31
+ // we deliberately skip executing global shortcuts.
32
+ return;
33
+ }
27
34
  // Debug logging for key presses (helps diagnose binding issues)
28
35
  if (input || key.ctrl || key.meta || key.shift) {
29
36
  logger.debug('KeyboardManager', 'Key pressed', {
@@ -74,6 +81,8 @@ export function KeyboardManager() {
74
81
  return false;
75
82
  if (hasShift && !key.shift)
76
83
  return false;
84
+ if (!hasShift && key.shift)
85
+ return false;
77
86
  // Check the actual key
78
87
  if (mainKey === 'up' && key.upArrow)
79
88
  return true;
@@ -0,0 +1,13 @@
1
+ import { type ReactNode } from 'react';
2
+ type KeyboardBlockContextValue = {
3
+ blockCount: number;
4
+ increment: () => void;
5
+ decrement: () => void;
6
+ };
7
+ export declare function KeyboardBlockProvider({ children }: {
8
+ children: ReactNode;
9
+ }): import("react/jsx-runtime").JSX.Element;
10
+ export declare function useKeyboardBlockContext(): KeyboardBlockContextValue;
11
+ export declare function useKeyboardBlocker(shouldBlock: boolean): void;
12
+ export declare function useIsKeyboardBlocked(): boolean;
13
+ export {};
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react';
3
+ const KeyboardBlockContext = createContext(null);
4
+ export function KeyboardBlockProvider({ children }) {
5
+ const [blockCount, setBlockCount] = useState(0);
6
+ const increment = useCallback(() => {
7
+ setBlockCount(prev => prev + 1);
8
+ }, []);
9
+ const decrement = useCallback(() => {
10
+ setBlockCount(prev => Math.max(0, prev - 1));
11
+ }, []);
12
+ const value = useMemo(() => ({ blockCount, increment, decrement }), [blockCount, increment, decrement]);
13
+ return (_jsx(KeyboardBlockContext.Provider, { value: value, children: children }));
14
+ }
15
+ export function useKeyboardBlockContext() {
16
+ const context = useContext(KeyboardBlockContext);
17
+ if (!context) {
18
+ throw new Error('useKeyboardBlockContext must be used within KeyboardBlockProvider');
19
+ }
20
+ return context;
21
+ }
22
+ export function useKeyboardBlocker(shouldBlock) {
23
+ const { increment, decrement } = useKeyboardBlockContext();
24
+ const blockedRef = useRef(false);
25
+ useEffect(() => {
26
+ if (shouldBlock && !blockedRef.current) {
27
+ increment();
28
+ blockedRef.current = true;
29
+ }
30
+ else if (!shouldBlock && blockedRef.current) {
31
+ decrement();
32
+ blockedRef.current = false;
33
+ }
34
+ return () => {
35
+ if (blockedRef.current) {
36
+ decrement();
37
+ blockedRef.current = false;
38
+ }
39
+ };
40
+ }, [shouldBlock, increment, decrement]);
41
+ }
42
+ export function useIsKeyboardBlocked() {
43
+ const { blockCount } = useKeyboardBlockContext();
44
+ return blockCount > 0;
45
+ }
@@ -4,6 +4,7 @@ export declare function usePlaylist(): {
4
4
  playlists: Playlist[];
5
5
  createPlaylist: (name: string) => void;
6
6
  deletePlaylist: (playlistId: string) => void;
7
+ renamePlaylist: (playlistId: string, newName: string) => void;
7
8
  addTrackToPlaylist: (playlistId: string, track: Track, force?: boolean) => AddTrackResult;
8
9
  removeTrackFromPlaylist: (playlistId: string, trackIndex: number) => void;
9
10
  };
@@ -37,6 +37,13 @@ export function usePlaylist() {
37
37
  configService.set('playlists', updatedPlaylists);
38
38
  return 'added';
39
39
  }, [playlists, configService]);
40
+ const renamePlaylist = useCallback((playlistId, newName) => {
41
+ const updatedPlaylists = playlists.map(playlist => playlist.playlistId === playlistId
42
+ ? { ...playlist, name: newName }
43
+ : playlist);
44
+ setPlaylists(updatedPlaylists);
45
+ configService.set('playlists', updatedPlaylists);
46
+ }, [playlists, configService]);
40
47
  const removeTrackFromPlaylist = useCallback((playlistId, trackIndex) => {
41
48
  const playlistIndex = playlists.findIndex(p => p.playlistId === playlistId);
42
49
  if (playlistIndex === -1)
@@ -50,6 +57,7 @@ export function usePlaylist() {
50
57
  playlists,
51
58
  createPlaylist,
52
59
  deletePlaylist,
60
+ renamePlaylist,
53
61
  addTrackToPlaylist,
54
62
  removeTrackFromPlaylist,
55
63
  };
@@ -7,6 +7,7 @@ import { ThemeProvider } from "./contexts/theme.context.js";
7
7
  import { PlayerProvider } from "./stores/player.store.js";
8
8
  import { ErrorBoundary } from "./components/common/ErrorBoundary.js";
9
9
  import { KeyboardManager } from "./hooks/useKeyboard.js";
10
+ import { KeyboardBlockProvider } from "./hooks/useKeyboardBlocker.js";
10
11
  import { Box, Text } from 'ink';
11
12
  import { useEffect } from 'react';
12
13
  import { useNavigation } from "./hooks/useNavigation.js";
@@ -65,5 +66,5 @@ function HeadlessLayout({ flags }) {
65
66
  return (_jsx(Box, { padding: 1, children: _jsx(Text, { color: "green", children: "Headless mode active." }) }));
66
67
  }
67
68
  export default function Main({ flags }) {
68
- return (_jsx(ErrorBoundary, { children: _jsx(ThemeProvider, { children: _jsx(PlayerProvider, { children: _jsx(NavigationProvider, { children: _jsx(PluginsProvider, { children: _jsxs(Box, { flexDirection: "column", children: [_jsx(KeyboardManager, {}), flags?.headless ? (_jsx(HeadlessLayout, { flags: flags })) : (_jsxs(_Fragment, { children: [_jsx(Initializer, { flags: flags }), _jsx(MainLayout, {})] }))] }) }) }) }) }) }));
69
+ return (_jsx(ErrorBoundary, { children: _jsx(ThemeProvider, { children: _jsx(PlayerProvider, { children: _jsx(NavigationProvider, { children: _jsx(PluginsProvider, { children: _jsx(KeyboardBlockProvider, { children: _jsxs(Box, { flexDirection: "column", children: [_jsx(KeyboardManager, {}), flags?.headless ? (_jsx(HeadlessLayout, { flags: flags })) : (_jsxs(_Fragment, { children: [_jsx(Initializer, { flags: flags }), _jsx(MainLayout, {})] }))] }) }) }) }) }) }) }));
69
70
  }
@@ -30,9 +30,10 @@ export declare const KEYBINDINGS: {
30
30
  readonly QUIT: readonly ["q", "escape"];
31
31
  readonly HELP: readonly ["?"];
32
32
  readonly SEARCH: readonly ["/"];
33
- readonly PLAYLISTS: readonly ["p"];
33
+ readonly PLAYLISTS: readonly ["shift+p"];
34
34
  readonly SUGGESTIONS: readonly ["g"];
35
35
  readonly SETTINGS: readonly [","];
36
+ readonly PLUGINS: readonly ["p"];
36
37
  readonly PLAY_PAUSE: readonly [" "];
37
38
  readonly NEXT: readonly ["n", "right"];
38
39
  readonly PREVIOUS: readonly ["b", "left"];
@@ -38,9 +38,10 @@ export const KEYBINDINGS = {
38
38
  QUIT: ['q', 'escape'],
39
39
  HELP: ['?'],
40
40
  SEARCH: ['/'],
41
- PLAYLISTS: ['p'],
41
+ PLAYLISTS: ['shift+p'],
42
42
  SUGGESTIONS: ['g'],
43
43
  SETTINGS: [','],
44
+ PLUGINS: ['p'],
44
45
  // Player
45
46
  PLAY_PAUSE: [' '],
46
47
  NEXT: ['n', 'right'],
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@involvex/youtube-music-cli",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "- A Commandline music player for youtube-music",
5
5
  "repository": {
6
6
  "type": "git",
@@ -62,6 +62,7 @@
62
62
  "jiti": "^2.6.1",
63
63
  "meow": "^14.0.0",
64
64
  "node-notifier": "^10.0.1",
65
+ "discord-rpc": "^4.0.1",
65
66
  "node-youtube-music": "^0.10.3",
66
67
  "play-sound": "^1.1.6",
67
68
  "react": "^19.2.4",