- New TUI using Textual framework with responsive interface - Search, results, download, and queue screens for YouTube video management - Download queue system with sequential background downloads - Status bar with queue count and download progress indicators - Comprehensive test suite (97 tests) with 100% pass rate - Modern Python packaging with pyproject.toml and uv support - Added requirements-tui.txt for TUI-specific dependencies - Updated setup.py and install.sh for TUI integration - Enhanced README.md with TUI usage documentation
200 lines
5.4 KiB
Python
200 lines
5.4 KiB
Python
"""
|
|
Command Palette Widget for YouTube TUI
|
|
"""
|
|
|
|
from textual.app import ComposeResult
|
|
from textual.containers import Container
|
|
from textual.screen import ModalScreen
|
|
from textual.widgets import Footer, Header, Input, ListView, ListItem, Static
|
|
|
|
|
|
class CommandPalette(ModalScreen):
|
|
"""Command palette for quick access to actions"""
|
|
|
|
DEFAULT_COMMANDS = [
|
|
("Search", "Search for videos"),
|
|
("Download", "Quick download mode"),
|
|
("History", "Show search history"),
|
|
("Settings", "Open settings"),
|
|
("Help", "Show help screen"),
|
|
("Quit", "Quit application"),
|
|
]
|
|
|
|
CSS = """
|
|
CommandPalette {
|
|
align: center middle;
|
|
}
|
|
|
|
#palette-container {
|
|
width: 60%;
|
|
height: auto;
|
|
border: solid #555555;
|
|
background: $surface;
|
|
padding: 1;
|
|
}
|
|
|
|
#palette-title {
|
|
width: 100%;
|
|
height: 3;
|
|
dock: top;
|
|
background: $primary;
|
|
content-align: center middle;
|
|
color: $text;
|
|
}
|
|
|
|
#palette-input {
|
|
width: 100%;
|
|
margin: 1 0;
|
|
}
|
|
|
|
#palette-list {
|
|
width: 100%;
|
|
height: 20;
|
|
margin: 1 0;
|
|
}
|
|
|
|
#palette-actions {
|
|
width: 100%;
|
|
height: auto;
|
|
dock: bottom;
|
|
margin-top: 1;
|
|
}
|
|
|
|
ListItem {
|
|
height: 3;
|
|
padding: 0 1;
|
|
}
|
|
|
|
ListItem:hover {
|
|
background: $primary-darken-2;
|
|
}
|
|
|
|
ListItem.--highlight {
|
|
background: $primary;
|
|
}
|
|
|
|
.command-description {
|
|
color: $text-muted;
|
|
}
|
|
"""
|
|
|
|
BINDINGS = [
|
|
("escape", "close_palette", "Close"),
|
|
("up", "cursor_up", "Cursor Up"),
|
|
("down", "cursor_down", "Cursor Down"),
|
|
("enter", "select_command", "Select"),
|
|
]
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.selected_index = 0
|
|
self.commands = self.DEFAULT_COMMANDS.copy()
|
|
|
|
def compose(self) -> ComposeResult:
|
|
"""Compose the command palette"""
|
|
yield Header()
|
|
yield Container(
|
|
Static("Command Palette", id="palette-title"),
|
|
Input(placeholder="Type to filter commands...", id="palette-input"),
|
|
ListView(id="palette-list-view"),
|
|
id="palette-container",
|
|
)
|
|
yield Footer()
|
|
|
|
def on_mount(self) -> None:
|
|
"""Called when palette is mounted"""
|
|
self.update_list()
|
|
self.query_one(Input).focus()
|
|
|
|
def update_list(self) -> None:
|
|
"""Update the command list"""
|
|
input_widget = self.query_one("#palette-input", Input)
|
|
filter_text = input_widget.value.lower()
|
|
|
|
# Filter commands
|
|
if filter_text:
|
|
self.commands = [
|
|
(cmd, desc)
|
|
for cmd, desc in self.DEFAULT_COMMANDS
|
|
if filter_text in cmd.lower() or filter_text in desc.lower()
|
|
]
|
|
else:
|
|
self.commands = self.DEFAULT_COMMANDS.copy()
|
|
|
|
# Update the list view
|
|
list_view = self.query_one("#palette-list-view", ListView)
|
|
list_view.clear()
|
|
|
|
for i, (command, description) in enumerate(self.commands):
|
|
item = ListItem(
|
|
Static(f"[bold]{command}[/bold]\n{description}"),
|
|
)
|
|
list_view.append(item)
|
|
|
|
# Update selected index if needed
|
|
if self.selected_index >= len(self.commands):
|
|
self.selected_index = max(0, len(self.commands) - 1)
|
|
|
|
# Highlight selected item
|
|
self.highlight_selected()
|
|
|
|
def highlight_selected(self) -> None:
|
|
"""Highlight the selected item"""
|
|
list_view = self.query_one("#palette-list-view", ListView)
|
|
|
|
for i, item in enumerate(list_view.children):
|
|
if i == self.selected_index:
|
|
item.add_class("--highlight")
|
|
else:
|
|
item.remove_class("--highlight")
|
|
|
|
def action_cursor_up(self) -> None:
|
|
"""Move selection up"""
|
|
if self.commands:
|
|
self.selected_index = max(0, self.selected_index - 1)
|
|
self.highlight_selected()
|
|
|
|
def action_cursor_down(self) -> None:
|
|
"""Move selection down"""
|
|
if self.commands:
|
|
self.selected_index = min(len(self.commands) - 1, self.selected_index + 1)
|
|
self.highlight_selected()
|
|
|
|
def action_select_command(self) -> None:
|
|
"""Execute the selected command"""
|
|
if not self.commands:
|
|
return
|
|
|
|
command = self.commands[self.selected_index][0]
|
|
self.execute_command(command)
|
|
|
|
def execute_command(self, command: str) -> None:
|
|
"""Execute a command"""
|
|
if command == "Search":
|
|
self.app.push_screen("search")
|
|
self.app.pop_screen()
|
|
elif command == "Download":
|
|
# Go to search for quick download
|
|
self.app.push_screen("search")
|
|
self.app.pop_screen()
|
|
elif command == "History":
|
|
self.app.push_screen("history")
|
|
elif command == "Settings":
|
|
self.app.push_screen("settings")
|
|
elif command == "Help":
|
|
self.app.push_screen("help")
|
|
elif command == "Quit":
|
|
self.app.exit()
|
|
|
|
def action_close_palette(self) -> None:
|
|
"""Close the palette"""
|
|
self.app.pop_screen()
|
|
|
|
def on_input_changed(self, event: Input.Changed) -> None:
|
|
"""Handle input changes"""
|
|
self.update_list()
|
|
|
|
def on_data_table_row_selected(self, event) -> None:
|
|
"""Handle row selection"""
|
|
self.action_select_command()
|