# touchdeck touchdeck is a touch-friendly "Stream Deck-like" user interface designed for small landscape displays (800x480), providing quick access to music controls and system monitoring. It offers a unified interface for media playback via MPRIS (D-Bus on Linux) or Spotify, synchronized lyrics from LRCLIB, real-time system statistics including CPU/RAM and optional NVIDIA GPU monitoring, network speed testing, and customizable quick actions accessible via swipe gestures. The application is built with Python and PySide6, featuring an async architecture using qasync for Qt/asyncio integration. It includes multiple themed pages with swipe navigation, a pull-up drawer for quick actions, system notification display, and persistent settings storage. The modular design supports multiple media providers, extensible themes, and custom shell command actions with live output streaming. ## APIs and Functions ### Settings Management Load and save application configuration from JSON file at `~/.config/touchdeck/settings.json`. ```python from touchdeck.settings import load_settings, save_settings, Settings # Load existing settings or create defaults settings = load_settings() # Modify settings settings.theme = "glacier" settings.clock_24h = True settings.enable_gpu_stats = False settings.music_poll_ms = 750 settings.quick_actions = ["play_pause", "next_track", "run_speedtest"] # Save changes save_settings(settings) # Example settings JSON structure: # { # "media_source": "mpris", # "enable_gpu_stats": true, # "clock_24h": false, # "show_clock_seconds": false, # "music_poll_ms": 500, # "stats_poll_ms": 1000, # "ui_opacity_percent": 90, # "ui_scale_percent": 100, # "theme": "midnight", # "quick_actions": ["play_pause", "next_track", "run_speedtest"], # "enabled_pages": ["music", "stats", "clock", "emoji", "speedtest", "settings"] # } ``` ### MPRIS Media Control Control media players via D-Bus MPRIS interface for Linux desktop integration. ```python import asyncio from touchdeck.services.mpris import MprisService, MprisProvider async def control_mpris(): service = MprisService() # List available MPRIS players players = await service.list_players() # Returns: ['org.mpris.MediaPlayer2.spotify', 'org.mpris.MediaPlayer2.vlc'] # Get current playback state state = await service.now_playing() print(f"Title: {state.title}") print(f"Artist: {state.artist}") print(f"Album: {state.album}") print(f"Status: {state.status}") # Playing, Paused, Stopped print(f"Progress: {state.progress_ms}ms / {state.duration_ms}ms") print(f"Can seek: {state.can_seek}") # Control playback if state.bus_name: await service.play_pause(state.bus_name) await service.next(state.bus_name) await service.previous(state.bus_name) # Seek to position (requires track_id) if state.can_seek and state.track_id: await service.set_position(state.bus_name, state.track_id, 60000) # 60 seconds # Use provider wrapper provider = MprisProvider(service) await provider.play_pause() await provider.next() await provider.seek(120000) # 2 minutes asyncio.run(control_mpris()) ``` ### Spotify Integration Control Spotify playback with OAuth authentication and device management. ```python import asyncio from pathlib import Path from touchdeck.services.spotify_provider import SpotifyProvider async def control_spotify(): provider = SpotifyProvider( client_id="your_spotify_client_id", client_secret="your_spotify_client_secret", redirect_port=8765, device_id=None, cache_path=Path.home() / ".config/touchdeck/spotify_token.json" ) # Authenticate (opens browser, starts local server) try: await provider.authenticate() except Exception as exc: print(f"Authentication failed: {exc}") return # Get playback state state = await provider.get_state() print(f"Playing: {state.title} by {state.artist}") print(f"Album: {state.album}") print(f"Device: {state.device_name}") print(f"Volume: {state.volume_percent}%") print(f"Progress: {state.progress_ms}ms / {state.duration_ms}ms") # Control playback await provider.play_pause() await provider.next() await provider.previous() await provider.seek(30000) # 30 seconds await provider.set_volume(75) # 75% # List and transfer devices devices = await provider.list_devices() for device in devices: print(f"Device: {device.name} ({device.type})") print(f" Active: {device.is_active}, Volume: {device.volume_percent}%") if devices: await provider.transfer_playback(devices[0].id, play=True) asyncio.run(control_spotify()) ``` ### Synchronized Lyrics Fetch time-synchronized lyrics from LRCLIB API with automatic cleanup and fallback queries. ```python import asyncio from touchdeck.LRCLIB import LrclibClient, LyricsNotFoundError async def fetch_lyrics(): client = LrclibClient() # Fetch synced lyrics try: lyrics = await client.fetch_synced( track_name="Bohemian Rhapsody", artist_name="Queen", album_name="A Night at the Opera", duration_ms=354000 # 5:54 ) except LyricsNotFoundError: print("Lyrics not found") return if lyrics: print(f"Found {len(lyrics.lines)} lyric lines") # Display lyrics at specific playback positions for position_ms in [0, 30000, 60000, 120000]: line = lyrics.line_at(position_ms) if line: print(f"{position_ms // 1000}s: {line}") # Example output: # Found 47 lyric lines # 0s: Is this the real life? # 30s: Mama, just killed a man # 60s: Too late, my time has come # 120s: I see a little silhouetto of a man asyncio.run(fetch_lyrics()) ``` ### System Statistics Monitor CPU, RAM, and optional NVIDIA GPU statistics. ```python from touchdeck.services.stats import StatsService # Initialize with GPU monitoring stats_service = StatsService(enable_gpu=True) # Read current stats stats = stats_service.read() print(f"CPU Usage: {stats.cpu_percent:.1f}%") print(f"RAM: {stats.ram_used_gb:.2f}GB / {stats.ram_total_gb:.2f}GB ({stats.ram_percent:.1f}%)") if stats.gpu_percent is not None: print(f"GPU Usage: {stats.gpu_percent:.1f}%") print(f"VRAM: {stats.vram_used_gb:.2f}GB / {stats.vram_total_gb:.2f}GB ({stats.vram_percent:.1f}%)") else: print("GPU stats unavailable (requires nvidia-ml-py)") # Disable GPU monitoring at runtime stats_service.set_gpu_enabled(False) # Example output: # CPU Usage: 23.5% # RAM: 8.42GB / 16.00GB (52.6%) # GPU Usage: 45.2% # VRAM: 2.15GB / 8.00GB (26.9%) ``` ### Network Speed Test Run speed tests using speedtest-cli library. ```python import asyncio from touchdeck.services.speedtest import SpeedtestService async def run_speedtest(): service = SpeedtestService() try: result = await service.run() print(f"Download: {result.download_mbps:.2f} Mbps") print(f"Upload: {result.upload_mbps:.2f} Mbps") print(f"Ping: {result.ping_ms:.2f} ms") # Example output: # Download: 245.67 Mbps # Upload: 89.34 Mbps # Ping: 12.45 ms except Exception as exc: print(f"Speed test failed: {exc}") asyncio.run(run_speedtest()) ``` ### Theme Management Apply and customize color themes across the application. ```python from touchdeck.themes import get_theme, theme_options, build_qss, THEMES # Get a specific theme theme = get_theme("glacier") print(f"Theme: {theme.label}") print(f"Background: {theme.background}") print(f"Text: {theme.text}") print(f"Accent: {theme.accent}") # List all available themes for theme in theme_options(): print(f"{theme.key}: {theme.label}") # Available themes: midnight, glacier, sunset, dawn, aurora, berry, neon # Generate Qt stylesheet qss = build_qss(theme) # Returns CSS-like stylesheet for PySide6 QApplication # Theme color properties: # - background, gradient_top, gradient_bottom # - text, subtle # - panel, panel_border # - accent, accent_pressed # - neutral, neutral_hover, neutral_pressed # - slider_track, slider_fill, slider_handle # - progress_bg, progress_chunk # Apply to QApplication from PySide6.QtWidgets import QApplication app = QApplication([]) app.setStyleSheet(build_qss(theme)) ``` ### Quick Actions Configure and execute quick actions including custom shell commands. ```python from touchdeck.quick_actions import ( AVAILABLE_QUICK_ACTIONS, CustomQuickAction, quick_action_lookup, filter_quick_action_keys, generate_custom_action_key ) # Built-in actions for action in AVAILABLE_QUICK_ACTIONS: print(f"{action.key}: {action.label} - {action.description}") # Output: # play_pause: Play/Pause - Toggle playback for the current player # next_track: Next Track - Skip to the next song # prev_track: Previous Track - Go back to the last song # run_speedtest: Run Speed Test - Start a new speed test # toggle_gpu: Toggle GPU Stats - Enable or disable GPU monitoring # Create custom action custom = CustomQuickAction( key="custom-notify", title="Send Notification", command='notify-send "Now Playing" "{title} by {artist}"', timeout_ms=5000 ) # Template variables available: # {title}, {artist}, {album}, {status} # {position_ms}, {length_ms}, {position_mmss}, {length_mmss} # {track_id}, {bus_name} # Generate unique key for new action existing_keys = ["play_pause", "next_track", "custom-notify"] new_key = generate_custom_action_key("My Action", existing_keys) # Returns: "custom-my-action" # Validate and filter action keys custom_actions = [custom] valid_keys = filter_quick_action_keys( ["play_pause", "invalid_key", "custom-notify"], custom_actions ) # Returns: ["play_pause", "custom-notify"] ``` ### Media Provider Manager Unified interface for multiple media providers with automatic error handling. ```python import asyncio from touchdeck.media import MediaManager, MediaProvider from touchdeck.services.mpris import MprisProvider from touchdeck.services.spotify_provider import SpotifyProvider async def manage_media(): mpris = MprisProvider() spotify = SpotifyProvider( client_id="client_id", client_secret="client_secret", redirect_port=8765, device_id=None, cache_path=Path(".spotify_cache.json") ) # Create manager with provider selection callback manager = MediaManager( providers={"mpris": mpris, "spotify": spotify}, settings_source=lambda: "mpris" # or "spotify" ) # Get state (automatically uses selected provider) state = await manager.get_state() print(f"{state.title} by {state.artist}") print(f"Source: {state.source}") # Control playback (returns error string or None) error = await manager.play_pause() if error: print(f"Error: {error}") await manager.next() await manager.previous() await manager.seek(60000) await manager.set_volume(80) # Device management (Spotify only) devices = await manager.list_devices() if devices: await manager.transfer_playback(devices[0].id, play=True) asyncio.run(manage_media()) ``` ### Application Entry Point Launch the touchdeck application with Qt/asyncio integration. ```python from touchdeck.main import main # Run application (handles all initialization) main() # What happens: # 1. Loads settings from ~/.config/touchdeck/settings.json # 2. Applies UI scale factor from settings # 3. Initializes PySide6 QApplication with touch support # 4. Sets up qasync event loop for asyncio integration # 5. Shows display selection dialog if not configured # 6. Creates DeckWindow with all pages and services # 7. Applies theme and shows fullscreen (or windowed in demo mode) # 8. Starts polling timers for music and stats # 9. Runs event loop until quit # Or run as module: # python -m touchdeck ``` ### Utility Functions Helper functions for media state handling and formatting. ```python from touchdeck.utils import MediaState, ms_to_mmss, clamp, first_str, unvariant # Format milliseconds to MM:SS formatted = ms_to_mmss(125000) # "2:05" formatted = ms_to_mmss(3661000) # "61:01" # Clamp values to range value = clamp(150, 0, 100) # 100 value = clamp(-10, 0, 100) # 0 # Extract first string from D-Bus variants title = first_str(["Song Title", "Alternate"]) # "Song Title" title = first_str("Single Value") # "Single Value" # Create media state state = MediaState( source="mpris", title="Stairway to Heaven", artist="Led Zeppelin", album="Led Zeppelin IV", is_playing=True, progress_ms=180000, duration_ms=482000, can_seek=True, can_control=True ) # Backward compatible properties print(state.position_ms) # Same as progress_ms print(state.length_ms) # Same as duration_ms ``` ## Integration and Usage touchdeck serves as a comprehensive touchscreen control panel for Linux desktop systems, particularly suited for secondary displays like small HDMI touchscreens. The application integrates with existing desktop media players through MPRIS D-Bus, providing universal control without requiring specific player support. For Spotify users, direct API integration offers advanced features like device switching and higher reliability. The synchronized lyrics feature enhances the music experience by fetching and displaying time-aligned lyrics from LRCLIB's community database. The modular architecture supports extension through custom quick actions, which can execute shell commands with access to current playback metadata. System administrators can use this for automation tasks, while end users benefit from one-tap shortcuts for common operations. The theme system provides visual customization with seven built-in color schemes optimized for both dark and light environments. Settings persist automatically, and the application includes safeguards like input validation, timeout handling, and graceful error recovery. Integration patterns include direct Python imports for library usage, D-Bus communication for system integration, and REST API calls for external services like LRCLIB and Spotify.