A flexible, XML-driven control panel for Batocera that provides an on-screen interface for system configuration and control. Works on both X11 and Wayland (Sway) with gamepad, touchscreen and keyboard support
- Cross-platform: Works on X11 and Wayland/Sway
- Multiple input methods: Keyboard, mouse, touchscreen, and gamepad (via evdev)
- XML-driven UI: Define your interface in a simple XML file
- Live updates: Display values update automatically from shell commands
- Customizable styling: GTK3 CSS for complete visual control
- Auto-close: Optional inactivity timeout
- Modal dialogs: Confirmation dialogs and choice popups
- Flexible layout: Horizontal and vertical groups with nested containers
# Run with default configuration
./controlcenter.py
# Run with custom XML and CSS
./controlcenter.py /path/to/config.xml /path/to/style.css
# Run with 10-second inactivity timeout
./controlcenter.py controlcenter.xml style.css 10When run without arguments, the application searches for configuration files in this priority order:
- User overrides:
/userdata/system/configs/controlcenter/ - System defaults:
/usr/share/batocera/controlcenter/ - Local directory: Same directory as
controlcenter.py
This allows users to customize their configuration without modifying system files.
./controlcenter.py [--fullscreen] [--window WIDTHxHEIGHT] [--hidden] [timeout] [xml_path] [css_path]
Options:
--fullscreen: Run in fullscreen mode (covers entire screen)--window WIDTHxHEIGHT: Set custom window size (e.g.,--window 800x600)--hidden: Start with window hidden (useful for background processes)
Positional arguments:
timeout: Inactivity timeout in seconds (default: 0 = never close)- Timer resets on any user interaction (navigation, button clicks)
- Window also closes when losing focus (clicking outside)
xml_path: Path to XML configuration file (default: auto-detected)css_path: Path to CSS stylesheet (default: auto-detected)
Examples:
# Run in fullscreen mode
./controlcenter.py --fullscreen
# Run with custom window size
./controlcenter.py --window 1024x768
# Run fullscreen with 30-second timeout
./controlcenter.py --fullscreen 30
# Run with custom size and configuration files
./controlcenter.py --window 800x600 config.xml style.css
# Start hidden (can be shown later with SIGUSR1)
./controlcenter.py --hiddenWindow Modes:
- Default: Responsive window size based on screen resolution (70-90% of screen width)
- Fullscreen: Covers entire screen, removes window decorations
- Custom size: Fixed dimensions, allows resizing for custom window sizes
- Hidden: Window exists but is not visible (useful for daemon-like operation)
<features>
<hgroup display="Group Title">
<vgroup>
<feature display="Feature Name">
<!-- Controls go here -->
</feature>
</vgroup>
</hgroup>
</features>These attributes can be used on any element:
Assigns a unique identifier to an element for conditional rendering.
<text id="public_ip" display="${curl ifconfig.me}" />Controls whether an element is rendered based on a condition.
Supported conditions:
-
Check if another element is rendered:
<!-- Show only if element with id="cheevos" is rendered --> <text if="id(cheevos)" display="Achievements enabled!" />
-
Check if another element is NOT rendered:
<!-- Show only if element with id="cheevos" is NOT rendered --> <text if="!id(cheevos)" display="Achievements disabled" />
-
Check shell command output:
<!-- Show only if command returns non-empty string --> <feature if="${pgrep emulatorlauncher}" display="Game Running"> <text display="A game is currently running" /> </feature>
-
Feature-level conditionals:
<!-- Entire feature is hidden if condition fails --> <feature display="Brightness Control" if='${printf "%s" $(batocera-brightness 2>/dev/null)}'> <button display="Bright -" action="batocera-brightness - 20" /> <text display="${batocera-brightness}%" refresh="1" /> <button display="Bright +" action="batocera-brightness + 20" /> </feature>
- Feature-level conditionals: When applied to
<feature>elements, the entire feature (including label and all controls) is hidden if the condition fails - Performance: Feature-level conditionals are more efficient than individual element conditionals when hiding entire sections
- Null handling: Commands that return "null" (case-insensitive) are treated as empty results and will cause the condition to fail
Examples:
<!-- Conditional game info - only show if emulator is running -->
<hgroup display="Game Info" if="${pgrep emulatorlauncher}">
<vgroup>
<feature display="Current Game">
<text display="${get-current-game}" />
</feature>
</vgroup>
</hgroup>
<!-- Show different messages based on achievement status -->
<text id="achievements" display="${check-achievements}" />
<text if="id(achievements)" display="Achievements Active" />
<text if="!id(achievements)" display="Achievements Disabled" />Creates a titled section with a frame border.
<hgroup display="Sound Parameters">
<!-- Content -->
</hgroup>Attributes:
display: Group title (optional, omit for no frame)
Creates a row of cells displayed horizontally. Can be used at root level or inside <hgroup>.
<vgroup>
<feature display="Volume">
<button display="Vol -" action="amixer set Master 5%-" />
<text display="${amixer get Master | grep -o '[0-9]*%' | head -1}" />
<button display="Vol +" action="amixer set Master 5%+" />
</feature>
</vgroup>Special attribute:
role="header": Makes the vgroup non-selectable and displays at the top (for status bars)role="footer": Same as "header" but displays at the bottom
A single row containing a label and controls.
<feature display="Power Mode">
<text display="${cat /sys/devices/system/cpu/cpufreq/policy0/scaling_governor}" />
<choice display="Performance" action="cpufreq-set -g performance" />
<choice display="Powersave" action="cpufreq-set -g powersave" />
</feature>Attributes:
display: Label textname: Alternative to display (deprecated)
Executes a shell command when clicked.
<button display="Reboot" action="systemctl reboot" />
<button display="Restart and Close" action="systemctl restart batocera" afterclick="bcc_close" />
<button display="Update and Reboot" action="batocera-upgrade" afterclick="${reboot}" />Attributes:
display: Button labelaction: Shell command to executeafterclick: Command or action to execute after the main action completes (optional)- Use
afterclick="bcc_close"to close all BCC windows - Use
afterclick="${command}"to execute a shell command - Use
afterclick="direct_command"to execute a direct command
- Use
align: Button alignment -left,center(default), orright
Shows a confirmation dialog before executing the action.
<button_confirm display="Kill Emulator" action="killall emulatorlauncher" />
<button_confirm display="Touchscreen keyboard" action="onscreen-keyboard-toggle" afterclick="bcc_close" />Attributes:
display: Button label (also used in confirmation message)action: Shell command to execute after confirmationafterclick: Command or action to execute after the main action completes (optional)
A switch that executes different commands for ON/OFF states.
<toggle
value="${batocera-audio getSystemMute}"
action_on="batocera-audio setSystemVolume mute"
action_off="batocera-audio setSystemVolume unmute" />
<toggle
value="${wifi-status}"
action_on="enable-wifi"
action_off="disable-wifi"
afterclick="bcc_close" />Attributes:
value: Command to get current state (returns "true"/"false", "1"/"0", "on"/"off", etc.)display: Command to get display value (optional, shows as label if provided)action_on: Command to execute when turning ONaction_off: Command to execute when turning OFFafterclick: Command or action to execute after the main action completes (optional)refresh: Update interval in seconds (default: 0 = no refresh). Can be integer or float (e.g.,1,0.5,2.5)align: Toggle alignment -left,center(default), orright
A modern switch widget (GtkSwitch) that executes different commands for ON/OFF states. Has the same functionality as <toggle> but with a different visual appearance.
<switch
value="${batocera-planemode status}"
action_on="batocera-planemode enable"
action_off="batocera-planemode disable" />
<switch
value="${bluetooth-status}"
action_on="enable-bluetooth"
action_off="disable-bluetooth"
afterclick="${restart-services}" />Attributes:
value: Command to get current state (returns "true"/"false", "1"/"0", "on"/"off", etc.)display: Command to get display value (optional, shows as label if provided)action_on: Command to execute when turning ONaction_off: Command to execute when turning OFFafterclick: Command or action to execute after the main action completes (optional)refresh: Update interval in seconds (default: 0 = no refresh). Can be integer or float (e.g.,1,0.5,2.5)align: Switch alignment -left,center(default), orright
Notes:
- Identical functionality to
<toggle>but uses a modern switch appearance - Better for binary on/off settings
- Supports the same state detection as toggle (true/false, 1/0, on/off, yes/no, enabled/disabled)
The afterclick attribute is available on all interactive elements (button, button_confirm, toggle, switch, choice) and allows you to execute additional commands after the main action completes.
Special Values:
afterclick="bcc_close": Hides the Batocera Control Center window (keeps app running in background)afterclick="${command}": Executes a shell command with variable substitutionafterclick="direct_command": Executes a direct shell command
Examples:
<!-- Hide BCC after action -->
<button display="Toggle Keyboard" action="onscreen-keyboard-toggle" afterclick="bcc_close" />
<!-- Chain commands -->
<button display="Update System" action="batocera-upgrade" afterclick="${systemctl reboot}" />
<!-- Hide BCC after changing power mode -->
<choice display="Performance" action="cpufreq-set -g performance" afterclick="bcc_close" />
<!-- Restart services after toggle -->
<toggle action_on="enable-wifi" action_off="disable-wifi" afterclick="${systemctl restart networking}" />Shows static text or dynamic output from a command.
<!-- Static text -->
<text display="Hello World" />
<!-- Dynamic text from command -->
<text display="${date +'%H:%M:%S'}" refresh="1" />
<!-- Command expansion in text -->
<text display="Load avg: ${cat /proc/loadavg | cut -d' ' -f1}%" />Attributes:
display: Text to display or${command}for dynamic contentrefresh: Update interval in seconds (default: 0 = no refresh). Can be integer or float (e.g.,1,0.5,2.5)align: Text alignment -left,center(default), orright
Command formats:
${command}: Single command, output replaces entire textText ${cmd1} more ${cmd2}: Multiple commands embedded in text
Creates a "Select" button that opens a popup with multiple choices.
<feature display="Power Mode">
<text display="${cat /sys/devices/system/cpu/cpufreq/policy0/scaling_governor}" />
<choice display="Performance" action="cpufreq-set -g performance" />
<choice display="Powersave" action="cpufreq-set -g powersave" />
<choice display="Ondemand" action="cpufreq-set -g ondemand" afterclick="bcc_close" />
</feature>Attributes:
display: Option label in the popupaction: Shell command to execute when selectedafterclick: Command or action to execute after the main action completes (optional)
Creates clickable tabs that switch between different content sections. Tabs must be defined in a feature, and each tab targets an <hgroup> by its name attribute.
<!-- Define tabs -->
<feature name="main_tabs" display="Navigation">
<tab display="System" target="System" />
<tab display="Games" target="Games" />
<tab display="Network" target="Network" />
</feature>
<!-- Define tab content - each hgroup is a tab panel -->
<hgroup name="System" display="System Settings">
<vgroup>
<feature display="CPU">
<text display="${cat /proc/cpuinfo | grep 'model name' | head -1 | cut -d: -f2}" />
</feature>
</vgroup>
</hgroup>
<hgroup name="Games" display="Game Library">
<vgroup>
<feature display="Total Games">
<text display="${find /userdata/roms -name '*.zip' | wc -l}" />
</feature>
</vgroup>
</hgroup>
<hgroup name="Network" display="Network Status">
<vgroup>
<feature display="IP Address">
<text display="${hostname -I | awk '{print $1}'}" />
</feature>
</vgroup>
</hgroup>Attributes:
display: Tab label text (shown on the tab button)target: Name of the<hgroup>to show when this tab is selected (must match an hgroup'snameattribute)
Notes:
- Tabs are defined in a
<feature>element, typically at the top of your XML - Each tab's
targetmust match thenameattribute of an<hgroup> - Only one tab's content is visible at a time
- The first tab is selected by default
- Tab content is stacked vertically when multiple vgroups are present
- Tabs can be navigated with keyboard (Left/Right arrows) or gamepad (D-Pad Left/Right)
- Clicking a tab or pressing Enter/A button activates it
Example with multiple content sections:
<features>
<!-- Tab navigation -->
<feature name="tabs" display="Quick Access">
<tab display="Audio" target="audio_settings" />
<tab display="Video" target="video_settings" />
<tab display="Controls" target="control_settings" />
</feature>
<!-- Audio tab content -->
<hgroup name="audio_settings" display="Audio Settings">
<vgroup>
<feature display="Volume">
<button display="Vol -" action="amixer set Master 5%-" />
<text display="${amixer get Master | grep -o '[0-9]*%' | head -1}" />
<button display="Vol +" action="amixer set Master 5%+" />
</feature>
</vgroup>
</hgroup>
<!-- Video tab content -->
<hgroup name="video_settings" display="Video Settings">
<vgroup>
<feature display="Resolution">
<text display="${xrandr | grep '*' | awk '{print $1}'}" />
</feature>
</vgroup>
</hgroup>
<!-- Controls tab content -->
<hgroup name="control_settings" display="Control Settings">
<vgroup>
<feature display="Gamepad">
<text display="${ls /dev/input/js* 2>/dev/null | wc -l} connected" />
</feature>
</vgroup>
</hgroup>
</features>Shows an image from a file, URL, or command output. Supports animated GIFs with CPU optimization and scaling!
<!-- Static image file -->
<img display="/usr/share/pixmaps/logo.png" width="100" height="100" />
<!-- Animated GIF (optimized for low CPU usage) -->
<img display="/path/to/animation.gif" width="200" />
<!-- Animated GIF with height constraint (frames scaled on-the-fly) -->
<img display="/path/to/spinner.gif" height="5%" />
<!-- Animated GIF with custom settings -->
<img display="/path/to/spinner.gif" animate="true" />
<!-- Disable animation (show first frame only) -->
<img display="/path/to/animation.gif" animate="false" />
<!-- Image from URL -->
<img display="http://example.com/image.png" width="200" />
<!-- Dynamic image path from command -->
<img display="${echo /path/to/image.png}" height="150" />
<!-- Images with percentage dimensions -->
<img display="/path/to/banner.jpg" width="50%" height="20%" />
<img display="/path/to/logo.png" height="10%" />Attributes:
display: File path, URL, or${command}that returns a pathwidth: Image width in pixels or percentage (e.g.,100or50%) (optional)height: Image height in pixels or percentage (e.g.,150or20%) (optional)refresh: Update interval in seconds (default: 0 = no refresh). Can be integer or float (e.g.,1,0.5,2.5)align: Image alignment -left,center(default), orrightanimate: Enable/disable GIF animation -true(default) orfalse
Notes:
- If only width or height is specified, aspect ratio is preserved
- Supports common formats: PNG, JPEG, GIF, etc.
- Animated GIFs: Automatically detected and played with full animation support
- Animated GIF Scaling: Frames are scaled on-the-fly to match specified dimensions (width/height)
- CPU Optimization: GIF animations are frame-rate limited (default: 15 FPS) and paused when window is hidden
- Percentage dimensions: Width/height can be specified as percentages (e.g.,
width="50%") relative to the window size - Percentages are calculated based on actual window dimensions when available, with fallback to 800x600 reference
- Performance Note: Scaling animated GIFs requires per-frame scaling which uses more CPU than static images. For best performance, pre-scale GIF files to desired size.
Environment Variables for GIF Optimization:
BCC_MAX_GIF_FPS=15- Maximum frames per second for animated GIFs (default: 15). Set to 0 for unlimited.BCC_ENABLE_GIF_ANIMATIONS=0- Disable all GIF animations globally (shows first frame only). Default: enabled.
Generates and displays a QR code from text, URL, or command output. Requires the qrcode Python library (installed by default on Batocera).
<!-- Static QR code from URL -->
<qrcode display="https://batocera.org" width="150" height="150" />
<!-- Static QR code from text, with a dark background -->
<qrcode display="Hello World" bg="#141821" />
<!-- Dynamic QR code from command -->
<qrcode display="${echo https://example.com/status}" refresh="1" />
<!-- QR codes with percentage dimensions -->
<qrcode display="https://batocera.org" width="15%" />
<qrcode display="${get_wifi_qr}" height="25%" style="card" logo="wifi.png" text="Connect" font="/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" />Attributes:
display: Text, URL, or${command}that returns data to encode as QR codewidth: QR code width in pixels or percentage (e.g.,200or15%) (optional, default: 200)height: QR code height in pixels or percentage (e.g.,200or25%) (optional, default: 200)refresh: Update interval in seconds (default: 0 = no refresh). Can be integer or float (e.g.,1,0.5,2.5)align: QR code alignment -left,center(default), orrightbg: HTML hex code for the background of the QR code - foreground color will be contrasting white or black automaticallystyle: you can embed the QR code in acardwith an optionallogoon top and footertextrendered with the providedfonttype
Notes:
- Requires
qrcodePython library (already installed on Batocera) - QR codes are generated as black on white background
- QR codes are always square - if only width or height is specified, both dimensions will use that value
- If neither width nor height is specified, defaults to 200x200 pixels
- Useful for sharing URLs, WiFi credentials, or dynamic status information
- Percentage dimensions: Width/height can be specified as percentages (e.g.,
width="15%") relative to the window size - Percentages are calculated based on actual window dimensions when available, with fallback to 800x600 reference
textandlogoare valid only withstyle="card"
Displays a progress bar with a numeric value below it. Shows progress as a visual bar and text value.
<!-- Static progress bar -->
<progressbar display="75" min="0" max="100" />
<!-- Dynamic progress bar from command -->
<progressbar display="${df -h /userdata | awk 'NR==2 {print $5}' | sed 's/%//'}" min="0" max="100" refresh="5" />
<!-- Battery level with custom range -->
<progressbar display="${cat /sys/class/power_supply/BAT0/capacity}" min="0" max="100" refresh="10" />
<!-- Temperature with custom range -->
<progressbar display="${sensors | grep 'Core 0' | awk '{print $3}' | sed 's/+//;s/°C//'}" min="20" max="80" refresh="2" />Attributes:
display: Static value or${command}that returns a numeric valuemin: Minimum value for the progress bar range (optional, default: 0)max: Maximum value for the progress bar range (optional, default: 100)refresh: Update interval in seconds (default: 0 = no refresh). Can be integer or float (e.g.,1,0.5,2.5)align: Progress bar alignment -left,center(default), orright
Notes:
- Values are automatically clamped to the min/max range
- If the command returns non-numeric text, the function tries to extract the first number found
- The progress bar shows both a visual bar and the numeric value below it
- Values are displayed as integers when whole numbers, or with one decimal place for fractals
- Progress bar uses the same color scheme as other UI elements (blue gradient)
Creates a button that opens a fullscreen viewer for documents including PDFs, images, comic book archives (CBZ), and plain text files.
<!-- View a local PDF -->
<doc display="View Manual" content="/userdata/roms/atari2600/manuals/manual.pdf" />
<!-- View a comic book archive -->
<doc display="View Comic" content="/userdata/library/comic.cbz" />
<!-- View a text file -->
<doc display="View Log" content="/var/log/system.log" />
<!-- View from URL -->
<doc display="Online Doc" content="https://example.com/document.pdf" />
<!-- Dynamic path from command -->
<doc display="Latest screenshot" content="${find /userdata/screenshots -name 'screenshot*.png' | head -1}" />Attributes:
display: Button label text (required)content: File path or URL to document (required). Can be${command}for dynamic pathsalign: Button alignment -left,center(default), orrightrefresh: Update interval in seconds (default: 0 = no refresh). Can be integer or float (e.g.,1,0.5)
Supported formats:
- PDF: Requires
pdftoppmandpdfinfo(usually pre-installed on Batocera)- Multi-page navigation with Previous/Next buttons
- Gamepad: Left/Right or A button to navigate, Up/Down to zoom in/out, Right analog stick for continuous panning, B to close
- CBZ: Comic Book Archive (ZIP file containing images)
- Multi-page navigation with Previous/Next buttons
- Images sorted naturally by filename
- Gamepad: Left/Right or A button to navigate, Up/Down to zoom in/out, Right analog stick for continuous panning, B to close
- Images: JPG, PNG, GIF, and other formats supported by GdkPixbuf
- Gamepad: Up/Down to zoom in/out, Right analog stick for continuous panning, A or B button to close
- Text files: TXT, LOG, MD, CONF, CFG, INI, JSON, XML, YAML, YML
- Scrollable text view with monospace font
- Font size controlled by CSS (
.doc-viewer-textclass) - Gamepad: Up/Down to zoom in/out (font size), Right analog stick for continuous panning/scrolling, A or B button to close
Notes:
- Opens in fullscreen overlay window
- PDFs are rendered at 120 DPI for good quality
- CBZ files are extracted and images displayed in natural sort order
- Images and text are automatically scaled/formatted to fit screen
- Supports both local files and HTTP/HTTPS URLs
- Text files use UTF-8 encoding with error replacement for invalid characters
- Zoom functionality: Use gamepad Up/Down or keyboard to zoom in/out on all content types
- Images/PDFs/CBZ: Zoom range 20% to 500%
- Text files: Font size zoom for better readability
- Pan functionality: Use right analog stick to pan around zoomed content
- Continuous panning: Hold the right analog stick in any direction for smooth, continuous scrolling
- Works with all content types when zoomed in
- Smooth scrolling for precise navigation
By default, elements do not refresh automatically (refresh="0"). This reduces CPU usage for static content. For dynamic elements that need periodic updates, explicitly set a refresh interval in seconds (e.g., refresh="1" or refresh="0.5").
Elements that typically need refresh:
- System information (CPU usage, memory, temperature)
- Time displays
- Running game information
- Volume levels
- Toggle states that can change externally
- Dynamic QR codes (e.g., for changing URLs or status)
Elements that don't need refresh:
- Static text and labels
- Buttons (they execute commands on click)
- Choice options
- Static images and QR codes
Example:
<!-- Static text - no refresh needed -->
<text display="System Settings" />
<!-- Dynamic CPU usage - refresh every second -->
<text display="${top -bn1 | grep 'Cpu(s)' | awk '{print $2}'}%" refresh="1" />
<!-- Fast refresh for time display (twice per second) -->
<text display="${date +'%H:%M:%S.%N' | cut -c1-12}" refresh="0.5" />
<!-- Volume that updates when buttons are clicked - needs refresh to show external changes -->
<text display="${batocera-audio getSystemVolume}%" refresh="1" />
<!-- Static QR code - no refresh needed -->
<qrcode display="https://batocera.org" width="150" height="150" />
<!-- Dynamic QR code that updates every 5 seconds -->
<qrcode display="${echo http://192.168.1.1:8080/status}" refresh="5" />
<!-- Slow refresh for less critical info (every 2.5 seconds) -->
<text display="${uptime -p}" refresh="2.5" />Generates and displays a QR code from text, URL, or command output. Requires the qrcode Python library (installed by default on Batocera).
<!-- Static QR code from URL -->
<qrcode display="https://batocera.org" width="150" height="150" />
<!-- Static QR code from text -->
<qrcode display="Hello World" />
<!-- Dynamic QR code from command -->
<qrcode display="${echo https://example.com/status}" refresh="1" />Attributes:
display: Text, URL, or${command}that returns data to encode as QR codewidth: QR code width in pixels (optional, default: 200)height: QR code height in pixels (optional, default: 200)refresh: Update interval in seconds (default: 0 = no refresh). Can be integer or float (e.g.,1,0.5,2.5)align: QR code alignment -left,center(default), orright
Notes:
- Requires
qrcodePython library (already installed on Batocera) - QR codes are generated as black on white background
- QR codes are always square - if only width or height is specified, both dimensions will use that value
- If neither width nor height is specified, defaults to 200x200 pixels
- Useful for sharing URLs, WiFi credentials, or dynamic status information
Creates a button that opens a fullscreen viewer for PDFs or images.
<!-- View a local PDF -->
<pdf name="View Manual" display="/usr/share/docs/manual.pdf" />
<!-- View an image -->
<pdf name="View Screenshot" display="/tmp/screenshot.png" />
<!-- View from URL -->
<pdf name="Online Doc" display="https://example.com/document.pdf" />
<!-- Dynamic path from command -->
<pdf name="Latest Log" display="${find /var/log -name '*.pdf' | head -1}" />Attributes:
name: Button label text (required)display: File path or URL to PDF/image (required). Can be${command}for dynamic pathsalign: Button alignment -left,center(default), orright
Supported formats:
- PDF: Requires
pdftoppmandpdfinfo(from poppler-utils package)- Multi-page navigation with Previous/Next buttons
- Gamepad: Left/Right or A button to navigate, B to close
- Images: JPG, PNG, GIF, and other formats supported by GdkPixbuf
- Gamepad: A or B button to close
Notes:
- Opens in fullscreen overlay window
- PDFs are rendered at 150 DPI for good quality
- Images are automatically scaled to fit screen
- Supports both local files and HTTP/HTTPS URLs
- Requires
pdftoppmandpdfinfocommands (usually pre-installed on Batocera)
By default, elements do not refresh automatically (refresh="0"). This reduces CPU usage for static content. For dynamic elements that need periodic updates, explicitly set a refresh interval in seconds (e.g., refresh="1" or refresh="0.5").
Elements that typically need refresh:
- System information (CPU usage, memory, temperature)
- Time displays
- Running game information
- Volume levels
- Toggle states that can change externally
- Dynamic QR codes (e.g., for changing URLs or status)
Elements that don't need refresh:
- Static text and labels
- Buttons (they execute commands on click)
- Choice options
- Static images and QR codes
Example:
<!-- Static text - no refresh needed -->
<text display="System Settings" />
<!-- Dynamic CPU usage - refresh every second -->
<text display="${top -bn1 | grep 'Cpu(s)' | awk '{print $2}'}%" refresh="1" />
<!-- Fast refresh for time display (twice per second) -->
<text display="${date +'%H:%M:%S.%N' | cut -c1-12}" refresh="0.5" />
<!-- Volume that updates when buttons are clicked - needs refresh to show external changes -->
<text display="${batocera-audio getSystemVolume}%" refresh="1" />
<!-- Static QR code - no refresh needed -->
<qrcode display="https://batocera.org" width="150" height="150" />
<!-- Dynamic QR code that updates every 5 seconds -->
<qrcode display="${echo http://192.168.1.1:8080/status}" refresh="5" />
<!-- Slow refresh for less critical info (every 2.5 seconds) -->
<text display="${uptime -p}" refresh="2.5" /><vgroup role="header">
<feature name="Time">
<text display="${date +'%H:%M:%S'}" refresh="1" />
</feature>
<feature display="CPU">
<text display="${top -bn1 | grep 'Cpu(s)' | awk '{print $2}'}%" refresh="1" />
</feature>
</vgroup><hgroup display="Sound Parameters">
<vgroup>
<feature>
<button display="Vol -" action="batocera-audio setSystemVolume -5" />
<text display="${batocera-audio getSystemVolume}%" refresh="1" />
<button display="Vol +" action="batocera-audio setSystemVolume +5" />
</feature>
<feature display="Mute Sound">
<toggle
value="${batocera-audio getSystemMute}"
action_on="batocera-audio setSystemVolume mute"
action_off="batocera-audio setSystemVolume unmute"
refresh="1" />
</feature>
</vgroup>
</hgroup><hgroup display="Power Parameters">
<vgroup>
<feature display="Power Mode">
<text display="${cat /sys/devices/system/cpu/cpufreq/policy0/scaling_governor}" refresh="1" />
<choice display="Performance" action="cpufreq-set -g performance" />
<choice display="Powersave" action="cpufreq-set -g powersave" />
<choice display="Ondemand" action="cpufreq-set -g ondemand" />
</feature>
</vgroup>
</hgroup><hgroup display="Network Info">
<vgroup>
<feature display="ES Web Interface">
<qrcode display="${echo http://$(hostname -s | awk '{print $1}'):1234}" width="150" height="150" refresh="5" />
</feature>
</vgroup>
</hgroup>The interface uses GTK3 CSS for styling. All elements have CSS classes for customization.
/* Main window */
.popup-root { }
/* Groups */
.group-frame { }
.group-title { }
/* Rows */
.vgroup-row { }
.vgroup-cell { }
.vgroup-cell-first { }
/* Text and labels */
.item-text { }
.value { }
.header { }
/* Buttons */
.cc-button { }
.cc-toggle { }
.cc-choice { }
.cc-button-confirm { }
/* Selection states */
.focused { }
.focused-cell { }
.choice-selected { }
/* Dialogs */
.confirm-dialog { }
.choice-option { }
/* Separators */
.section-separator { }/* Main window background */
.popup-root {
background-color: rgba(20, 20, 20, 0.95);
color: #ffffff;
}
/* Group frames */
.group-frame {
border: 2px solid #444444;
border-radius: 8px;
background-color: rgba(30, 30, 30, 0.8);
margin: 8px;
padding: 8px;
}
.group-title {
color: #00d4ff;
font-size: 16px;
font-weight: bold;
}
/* Buttons */
.cc-button {
background-color: #333333;
color: #ffffff;
border: 2px solid #555555;
border-radius: 6px;
padding: 8px 16px;
min-width: 80px;
}
.cc-button:hover {
background-color: #444444;
border-color: #00d4ff;
}
/* Selected button */
.focused-cell,
.choice-selected {
background-color: #00a8cc !important;
border-color: #00d4ff !important;
}
/* Toggle switches */
.cc-toggle {
background-color: #555555;
border: 2px solid #777777;
border-radius: 20px;
padding: 6px 20px;
min-width: 60px;
}
.cc-toggle:checked {
background-color: #00cc66;
border-color: #00ff88;
}
/* Text values */
.value {
color: #00d4ff;
font-size: 14px;
font-weight: bold;
}- Arrow Keys: Navigate between controls
- Up/Down: Move between rows
- Left/Right: Move between controls in a row or vgroup
- Enter/Space: Activate selected control
- Escape: Close window or dialog
The application uses evdev for gamepad support with exclusive access (prevents EmulationStation from receiving inputs while the control center is open).
- D-Pad/Left Stick: Navigate between controls
- Up/Down: Move between rows
- Left/Right: Move between controls in a row
- A Button (South): Activate/Confirm selected control
- B Button (East) / Start: Close/Cancel
- L1/R1 (Page Up/Down): Switch between tabs (if tabs are present)
Navigation in the main window uses single-action with debouncing for precise control.
- D-Pad/Left Stick Up/Down: Zoom in/out
- Continuous zoom: Hold Up/Down for continuous zooming at medium speed (5 times per second)
- Images/PDFs/CBZ: Zoom range 20% to 500%
- Text files: Font size zoom for better readability
- D-Pad/Left Stick Left/Right: Navigate pages (PDFs and CBZ files)
- Continuous page turning: Hold Left/Right for continuous page navigation at slower speed (3.3 times per second)
- Right Analog Stick: Pan around zoomed content
- Continuous panning: Hold any direction for smooth, continuous scrolling at fast speed (10 times per second)
- Works with all content types when zoomed in
- A or B Button: Close document viewer
Continuous actions are automatically enabled when viewing documents and disabled when returning to the main window:
- Panning (Right analog stick): 100ms intervals (fast, 10 times per second)
- Zooming (D-Pad/Left stick Up/Down): 200ms intervals (medium, 5 times per second)
- Page turning (D-Pad/Left stick Left/Right): 300ms intervals (slower, 3.3 times per second)
Multiple continuous actions can run simultaneously (e.g., panning while zooming).
Supported controllers: Xbox, PlayStation, and most standard gamepads.
- Click any button or control to activate
- Click outside the window to close (if focus-out is enabled)
- Width: 70% of screen width
- Height: Automatically sized to content, up to 70% of screen height
- Content is scrollable if it exceeds the maximum height
- X11: Centered horizontally, 20px from top
- Wayland/Sway: Centered by compositor
The window closes automatically in these situations:
- Inactivity timeout: If configured (3rd command line parameter)
- Timer resets on any user interaction
- Does not close while dialogs are open
- Focus loss: When clicking outside the window
- Does not close when opening dialogs (choice/confirm)
On Wayland/Sway, the window uses a special technique to ensure visibility:
- Briefly enters fullscreen mode on startup
- Returns to floating mode and centers
- This works around Sway's visibility handling for floating windows
batocera-controlcenter/
├── controlcenter.py # Main entry point
├── ui_core.py # UI rendering and window management
├── xml_utils.py # XML parsing and validation
├── shell.py # Shell command execution utilities
├── refresh.py # Background refresh tasks
├── controlcenter.xml # Default UI configuration
├── style.css # Default stylesheet
└── README.md # This file
- Add XML validation in
xml_utils.py - Add rendering logic in
ui_core.py(in_build_vgroup_rowor_build_feature_row) - Add CSS classes in
style.css - Update this README with documentation
# Test with custom config
./controlcenter.py test.xml test.css
# Test with auto-close
./controlcenter.py test.xml test.css 5
# Test on X11
DISPLAY=:0 ./controlcenter.py
# Test on Wayland
WAYLAND_DISPLAY=wayland-0 ./controlcenter.pyEnable debug output by checking the console. The application prints:
- Backend detection (X11/Wayland)
- Window sizing information
- CSS loading status
- Gamepad detection
- Python 3.7+
- GTK 3.0
- GLib
- python-evdev (for gamepad support)
- Wayland/Sway or X11
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3.
Copyright (c) 2025 lbrpdx for the Batocera team
Contributions are welcome! Please ensure:
- XML validation passes for new elements
- CSS classes are documented
- Code follows existing style
- README is updated for new features
- Ensure Sway is running:
echo $WAYLAND_DISPLAY - Check Sway logs:
journalctl -u sway - Try with decorations: The window uses decorated mode on Wayland by default
- Check evdev is installed:
python3 -c "import evdev" - Verify gamepad is detected:
ls /dev/input/event* - Check permissions: User must have access to
/dev/input/event*
- Test commands in terminal first
- Check command output: Commands should return clean text
- Escape special characters in XML: Use
&for&,<for<, etc.
- Verify CSS file exists and is readable
- Check console for CSS parsing errors
- Ensure GTK 3.0 compatible syntax (no GTK 4 features)
- Check if auto-close timeout is set
- Verify focus-out behavior (clicking outside closes window)
- Ensure dialogs are not triggering premature closure
