Jarian Cottingham 06877fb2fc feat: add TUI (Textual User Interface) with download queue system
- 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
2026-02-25 15:59:15 -06:00

187 lines
5.1 KiB
Python

"""
Search History Screen for YouTube TUI
"""
import json
from pathlib import Path
from textual.app import ComposeResult
from textual.containers import Container
from textual.screen import ModalScreen
from textual.widgets import Footer, Header, ListItem, ListView, Static
class SearchHistoryScreen(ModalScreen):
"""Screen showing search history"""
CSS = """
SearchHistoryScreen {
align: center middle;
}
#history-container {
width: 70%;
height: 70%;
border: solid #555555;
background: $surface;
padding: 1;
}
#history-title {
width: 100%;
height: 3;
dock: top;
background: $primary;
content-align: center middle;
color: $text;
}
ListView {
width: 100%;
height: 100%;
}
ListItem {
height: 3;
padding: 0 1;
}
ListItem:hover {
background: $primary-darken-2;
}
ListItem.--highlight {
background: $primary;
}
.history-item {
height: 3;
}
.history-time {
color: $text-muted;
}
#clear-btn {
margin: 1 1;
}
"""
BINDINGS = [
("escape", "close_history", "Close"),
("q", "close_history", "Close"),
("d", "delete_selected", "Delete Selected"),
("c", "clear_all", "Clear All"),
]
def __init__(self):
super().__init__()
self.history_file = (
Path.home() / ".config" / "youtube_cli" / "search_history.json"
)
self.search_history = []
def compose(self) -> ComposeResult:
"""Compose the history screen"""
yield Header()
yield Container(
Static("Search History", id="history-title"),
ListView(id="history-list"),
id="history-container",
)
yield Footer()
def on_mount(self) -> None:
"""Load history on mount"""
self.load_history()
def load_history(self) -> None:
"""Load search history from file"""
self.search_history = []
if self.history_file.exists():
try:
with open(self.history_file, "r") as f:
self.search_history = json.load(f)
except Exception:
self.search_history = []
# Reverse to show newest first
self.search_history = list(reversed(self.search_history))
list_view = self.query_one("#history-list", ListView)
list_view.clear()
for item in self.search_history:
search_term = item.get("search_term", "")
timestamp = item.get("timestamp", "")
# Format timestamp
if timestamp:
try:
from datetime import datetime
dt = datetime.fromisoformat(timestamp)
time_str = dt.strftime("%Y-%m-%d %H:%M")
except Exception:
time_str = timestamp
else:
time_str = "Unknown time"
list_view.append(
ListItem(Static(f"[bold]{search_term}[/bold]\n[dim]{time_str}[/dim]"))
)
def action_delete_selected(self) -> None:
"""Delete selected history item"""
list_view = self.query_one("#history-list", ListView)
if list_view.children:
# Get the selected item
selected_index = list_view.index
if (
selected_index is not None
and selected_index >= 0
and selected_index < len(self.search_history)
):
# Remove from history
del self.search_history[selected_index]
self.save_history()
self.load_history()
def action_clear_all(self) -> None:
"""Clear all history"""
self.search_history = []
self.save_history()
self.load_history()
self.notify("Search history cleared", timeout=2)
def save_history(self) -> None:
"""Save history to file"""
try:
# Ensure directory exists
self.history_file.parent.mkdir(parents=True, exist_ok=True)
with open(self.history_file, "w") as f:
json.dump(list(reversed(self.search_history)), f, indent=2)
except Exception as e:
self.notify(f"Error saving history: {e}", severity="error", timeout=3)
def action_close_history(self) -> None:
"""Close the history screen"""
self.app.pop_screen()
def on_list_view_selected(self, event: ListView.Selected) -> None:
"""Handle item selection"""
# Use event.index directly, ignore list_view
item_index = event.index
if 0 <= item_index < len(self.search_history):
# Get the search term
search_term = self.search_history[item_index].get("search_term", "")
# Push search screen with the term
self.app.push_screen("search")
# Note: We'd need to expose the search screen to set the term
# For now, just notify
self.notify(f"Selected: {search_term}", timeout=2)