Skip to content

Conversation

@ruffsl
Copy link
Owner

@ruffsl ruffsl commented Dec 23, 2025

Summary

This PR refactors the --hide flag to support multiple hiding strategies, enabling automatic Steam controller blacklist configuration. This makes CtrlAssist fully functional in Flatpak environments without requiring root access or manual configuration.

Follow up to:

Motivation

The current --hide implementation uses system-level device permissions, which has limitations:

  1. Requires root access - Not possible in Flatpak due to sandboxing
  2. Manual workaround needed - Users must manually edit Steam's config.vdf to blacklist controllers
  3. Error-prone - Manual editing requires finding vendor/product IDs via lsusb and reformatting them
  4. Not automatically restored - If users forget to restore the config, controllers remain blacklisted

This PR automates the Steam configuration workaround and provides a clean abstraction for future hiding strategies.

Changes

Core Changes

  • Refactored --hide flag: Changed from bool to HideType enum with three variants:

    • none (default): No hiding applied
    • steam: Automatically manages Steam's controller blacklist
    • system: Restricts device permissions (original behavior, requires root)
  • New Steam hiding implementation:

    • Parses ~/.local/share/Steam/config/config.vdf
    • Extracts vendor/product IDs directly from evdev::Device::input_id()
    • Adds controllers to controller_blacklist in the format Steam expects (045e/02dd,054c/05c4)
    • Automatically restores original blacklist on exit via RAII
  • Enhanced ScopedDeviceHider:

    • Maintains separate state for system and steam hiding strategies
    • Implements proper cleanup for both strategies via Drop
    • Gracefully handles missing Steam configs and other error conditions
  • Reverts default variant for SpoofTarget enum to None:

    • As when using steam input hiding, users are more likely to wonder why steam is ignoring the virtual gamepad as well

Files Modified

  • src/main.rs: Updated HideType enum and hide logic
  • src/udev_helpers.rs: Refactored ScopedDeviceHider with Steam support
  • Cargo.toml: Added dirs dependency for home directory resolution
  • README.md: Updated documentation with new hide strategies and comparison table
  • Flatpak updates for new cargo.lock and added read/write config file permission

Benefits

For Flatpak Users

✅ Works without root access
✅ No manual config editing required
✅ Automatic cleanup on exit
✅ Steam restart is the only requirement

For Native Linux Users

✅ Choice between Steam and system-level hiding
✅ Existing system hiding still available for per-device control
✅ System hiding supports complex scenarios (e.g., 2v1 with same controller models)
✅ Consistent interface across both methods

For All Users

✅ Explicit behavior through enum variants
✅ Automatic restoration prevents orphaned configs
✅ Better error messages and logging

Usage Examples

Steam Hiding (Recommended for Flatpak)

# Automatically configures Steam's controller blacklist
flatpak run io.github.ruffsl.ctrlassist mux --hide steam

# Remember to restart Steam for changes to take effect

System Hiding (Requires Root)

# Original behavior: restrict device permissions
sudo ctrlassist mux --hide system

No Hiding (Default)

# No automatic hiding, manual launcher configuration needed
ctrlassist mux

Choosing the Right Strategy

Use Steam Hiding when:

  • ✅ Running in Flatpak (only option without root)
  • ✅ You have a single pair of controllers
  • ✅ All controllers of the same model should be hidden
  • ✅ You want the simplest setup without sudo

Use System Hiding when:

  • ✅ You have root access
  • ✅ You need per-device granularity (multiple controllers of same model)
  • ✅ You're in a 2v1 scenario where third player uses same controller model
  • ✅ You want maximum control over which devices are hidden

Use No Hiding when:

  • ✅ Testing or development
  • ✅ Manually configuring game launchers
  • ✅ Troubleshooting hiding issues

Comparison Table

Strategy Root Required Flatpak Compatible Granularity Persists After Exit Restart Required
None (default) No Yes N/A N/A N/A
Steam No Yes Vendor/Product ID No (auto-restores) Steam only
System Yes No Per-device No (auto-restores) Game/Launcher

Key Differences

Steam Hiding operates at the vendor/product ID level:

  • Blacklists ALL controllers matching the same vendor/product ID
  • Example: Hiding an Xbox One controller hides all Xbox One controllers
  • ⚠️ Trade-off: Cannot selectively hide specific controller instances
  • Best for: Single controller pairs, or when all controllers of same model should be hidden

System Hiding operates at the device node level:

  • Hides specific controller instances (e.g., /dev/input/event16)
  • Example: Can hide primary/assist controllers while leaving a third identical controller visible
  • Advantage: Granular per-device control
  • Best for: Scenarios with multiple controllers of the same model (e.g., 2v1 gameplay where the third player uses the same controller model)

Migration Guide

Breaking Change

The --hide flag now requires a value instead of being a boolean flag.

Before:

sudo ctrlassist mux --hide

After:

sudo ctrlassist mux --hide system

For Flatpak users who were manually editing config.vdf:

# No more manual editing needed!
flatpak run io.github.ruffsl.ctrlassist mux --hide steam --spoof none

Testing

Tested Scenarios

  • ✅ Steam hiding with Xbox One + PS4 controllers
  • ✅ Automatic vendor/product ID extraction
  • ✅ Steam config parsing and modification
  • ✅ Automatic restoration on Ctrl+C exit
  • ✅ System hiding backwards compatibility
  • ✅ No hiding (default behavior)

Testing Instructions

Test Steam Hiding

  1. Run with --hide steam
  2. Verify ~/.local/share/Steam/config/config.vdf contains new controller_blacklist entries
  3. Restart Steam and verify controllers are ignored by Steam Input
  4. Exit CtrlAssist (Ctrl+C) and verify blacklist is restored to original state
  5. Restart Steam again and verify controllers are no longer blacklisted

Test System Hiding

  1. Run with sudo and --hide system
  2. Check device permissions: ls -l /dev/input/event*
  3. Verify hidden devices have 0600 permissions (root-only)
  4. Exit CtrlAssist and verify permissions restored to 0660

Test Default Behavior

  1. Run without --hide flag
  2. Verify no changes to Steam config or device permissions
  3. Verify virtual gamepad still works correctly

Implementation Notes

RAII Pattern

Both hide strategies use RAII (Resource Acquisition Is Initialization) to ensure cleanup:

  • Changes are applied when hide_gamepad_devices() is called
  • Automatic restoration occurs when ScopedDeviceHider is dropped
  • Cleanup happens even if the program panics or exits unexpectedly

Granularity Trade-offs

The two hiding strategies operate at different levels of granularity:

Steam Hiding (Vendor/Product ID Level)

Input: Xbox One controller at /dev/input/event16
Action: Adds "045e/02dd" to Steam blacklist
Result: ALL Xbox One controllers are hidden from Steam Input

This is because Steam's controller_blacklist format only supports vendor/product ID pairs, not specific device paths. While this simplifies configuration, it means all controllers of the same make/model are affected.

Scenario where this matters: 2v1 gameplay

  • Player 1 & 2 use CtrlAssist with two Xbox controllers → Hidden via Steam
  • Player 3 uses a third Xbox controller independently → Also hidden! ❌
  • Solution: Use --hide system for per-device granularity

System Hiding (Device Node Level)

Input: Xbox One controller at /dev/input/event16
Action: Restricts permissions on /dev/input/event16 (and related nodes)
Result: Only this specific controller instance is hidden

This introspects the device tree to find all related nodes (input events, hidraw) and hides them individually. Other controllers of the same model remain accessible.

Scenario where this matters: 2v1 gameplay

  • Player 1 & 2 use CtrlAssist with two Xbox controllers → Hidden via system
  • Player 3 uses a third Xbox controller independently → Still visible! ✅
  • Trade-off: Requires root access, not available in Flatpak

Steam Config Format

Steam's VDF format requires specific formatting:

"InstallConfigStore"
{
	"controller_blacklist"	"045e/02dd,054c/05c4"
	"Software"
	{
		...
	}
}

The implementation preserves indentation and structure while updating the blacklist value.

Future Enhancements

Potential improvements for future PRs:

  • Auto-detect Flatpak environment and suggest --hide steam
  • Support for other launchers (Lutris, Heroic, etc.)
  • Warning if Steam is running when using --hide steam
  • Persistent mode with udev rules for system hiding
  • Unit tests for Steam config parsing

Refactors the --hide flag from a boolean to an enum supporting multiple
hiding strategies. This enables automatic Steam controller blacklist
configuration, making CtrlAssist fully functional in Flatpak environments
without requiring root access.

Changes:
- Replace --hide boolean flag with HideType enum (none/steam/system)
- Add Steam config.vdf parsing and modification for controller_blacklist
- Refactor ScopedDeviceHider to support multiple hide strategies
- Add automatic restoration of original configs on exit via RAII
- Extract vendor/product IDs directly from evdev::Device::input_id()
- Add dirs dependency for cross-platform home directory resolution

Hide Strategies:
- none (default): No hiding, manual launcher configuration required
- steam: Automatically manages Steam's controller_blacklist (Flatpak-compatible)
  * Blacklists by vendor/product ID (affects all controllers of same model)
- system: Restricts device permissions (requires root, original behavior)
  * Hides specific device instances for granular control

Usage:
  # Steam hiding (recommended for Flatpak)
  ctrlassist mux --hide steam

  # System hiding (requires root)
  sudo ctrlassist mux --hide system

BREAKING CHANGE: The --hide flag now requires a value (steam/system/none)
instead of being a boolean flag. Migration: --hide → --hide system
as when using steam input hiding, users are more likely to wonder why steam is ignoring the virtual gamepad as well
@ruffsl ruffsl marked this pull request as ready for review December 23, 2025 22:17
@ruffsl ruffsl requested a review from Copilot December 23, 2025 22:17
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the --hide flag from a boolean to an enum supporting multiple hiding strategies, with a new Steam controller blacklist feature that enables CtrlAssist to work in Flatpak environments without requiring root access. The Steam hiding strategy automatically modifies Steam's config.vdf to blacklist physical controllers and restores the original configuration on exit.

Key Changes:

  • Introduced HideType enum with three variants: None (default), Steam, and System
  • Implemented automatic Steam controller blacklist management via VDF config parsing
  • Changed SpoofTarget enum default from Primary to None to better complement Steam hiding

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
src/main.rs Adds HideType enum and updates hide logic to dispatch to appropriate strategy; changes SpoofTarget default to None
src/udev_helpers.rs Refactors ScopedDeviceHider to support multiple hiding strategies with separate state tracking and Steam VDF config parsing/modification
Cargo.toml Adds dirs dependency for home directory resolution
Cargo.lock Updates lock file with new dependencies (dirs, dirs-sys, getrandom, libredox, option-ext, redox_users, thiserror, thiserror-impl, wasi)
flatpak/io.github.ruffsl.ctrlassist.yml Adds filesystem permission for Steam config directory (~/.local/share/Steam/config:rw)
flatpak/cargo-sources.json Adds cargo source entries for new dependencies
README.md Updates documentation with new hiding strategies, usage examples, comparison table, and limitations; fixes outdated Flatpak workaround instructions

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

ruffsl and others added 10 commits December 23, 2025 16:24
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
Makes ScopedDeviceHider constructor infallible by deferring Steam config
path resolution until it's actually needed. This follows Rust idioms of
keeping constructors cheap and simple while moving error handling to the
point of use.

Changes:
- Remove Result return type from ScopedDeviceHider::new()
- Implement lazy initialization in hide_steam() method
- Resolve home directory only when Steam hiding is first attempted
- Cache resolved config path in Option<PathBuf> for subsequent uses

Benefits:
- Simpler API: no error handling required at construction site
- No premature failures: constructor succeeds even if home unavailable
- Works in headless environments when only system hiding is used
- Error occurs at point of use with proper context
- More idiomatic Rust: cheap constructors, deferred fallible ops

Before:
  let mut hider = ScopedDeviceHider::new(args.hide.clone())?;

After:
  let mut hider = ScopedDeviceHider::new(args.hide.clone());
@ruffsl ruffsl merged commit a508f8a into main Dec 24, 2025
1 check passed
@ruffsl ruffsl deleted the feature/steam branch December 24, 2025 04:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants