Skip to content

Instantly share code, notes, and snippets.

@NaviVani-dev
Last active November 22, 2025 18:01
Show Gist options
  • Select an option

  • Save NaviVani-dev/9a8a704a31313fd5ed5fa68babf7bc3a to your computer and use it in GitHub Desktop.

Select an option

Save NaviVani-dev/9a8a704a31313fd5ed5fa68babf7bc3a to your computer and use it in GitHub Desktop.
DualScope: Run two steam instances with gamescope with different user accounts (split screen on any game!)
#!/bin/bash
# a little script i made to open two gamescope sessions of steam
# i SUCK at making bash script, so expect a lot of bugs or issues :<
# made for arch linux in mind
# you need another account on your device to use this
# if u use pipewire and your audio is not working, take a look at this:
# https://wiki.archlinux.org/title/PipeWire#Multi-user_audio_sharing
# The gamescope params for both of the processes
GAMESCOPE_PARAMS="-e -b -w 960 -h 360 -W 1440 -H 540"
# Params for both steams
STEAM_PARAMS="-gamepadui"
# The secondary account you will use
RUNAS_ACC="YOUR SECONDARY ACCOUNT"
GAMESCOPE_BIN="/usr/bin/gamescope"
RUNAS_BIN="/usr/bin/run-as"
STEAM_BIN="/usr/bin/steam"
CHANGED_DEVICES=()
GAMESCOPE_PRIMARY_PID=""
GAMESCOPE_SECONDARYPID=""
# hiding kde panels using this:
# https://github.com/luisbocanegra/plasma-panel-colorizer
toggle_panels() {
local state="$1"
local value="false"
if [ "$state" = "true" ]; then
value="true"
fi
local status="string:stockPanelSettings.visible {\"enabled\": true, \"value\": $value}"
dbus-send --session --type=signal /preset "luisbocanegra.panel.colorizer.all.property" "$status"
}
startup() {
echo "Hiding the KDE panels!"
toggle_panels "false"
}
cleanup() {
echo "Showing the KDE panels again"
toggle_panels "true"
echo "Returning ownership to every device"
return_devices_ownership
if [[ -n "$GAMESCOPE_PRIMARY_PID" ]]; then
echo "Killing primary gamescope..."
kill -TERM "$GAMESCOPE_PRIMARY_PID" 2>/dev/null || true
fi
if [[ -n "$GAMESCOPE_SECONDARY_PID" ]]; then
echo "Killing secondary gamescope..."
kill -TERM "$GAMESCOPE_SECONDARY_PID" 2>/dev/null || true
fi
}
show_devices_list() {
local devices=""
local name=""
while IFS= read -r line; do
if [[ $line == N:\ Name=* ]]; then
name=$(echo "$line" | sed 's/N: Name=//' | tr -d '"')
fi
if [[ $line == *Handlers=* ]]; then
local event_dev=$(echo "$line" | grep -o 'event[0-9]*' | sed 's/event//')
if [[ -n $name && -n $event_dev ]]; then
# aditional info 4 controllers
local device_info=""
if [[ $name == *"Xbox"* || $name == *"Controller"* || $name == *"Gamepad"* || $name == *"Joystick"* ]]; then
device_info=" 󰊗"
elif [[ $name == *"Keyboard"* || $name == *"keyboard"* ]]; then
device_info=" ⌨️"
elif [[ $name == *"Mouse"* || $name == *"mouse"* ]]; then
device_info=" 󰍽"
fi
devices+="$event_dev - $name$device_info"$'\n'
name=""
fi
fi
done < /proc/bus/input/devices
echo -e "$devices"
}
change_device_ownership() {
local profile="$1"
local device="$2"
local device_path="/dev/input/event$device"
sudo chown "$profile" "$device_path"
sudo chmod 600 "$device_path"
}
get_devices_to_change() {
local profile="$1"
echo "Select the devices to transfer for the $profile profile:"
show_devices_list
echo "Please, input your device list divided by a comma (1,2,3,4,etc):"
read input_string
IFS=',' read -r -a NEW_DEVICES <<< "$input_string"
CHANGED_DEVICES+=("${NEW_DEVICES[@]}")
for device in "${NEW_DEVICES[@]}"; do
change_device_ownership "$profile" "$device"
done
}
return_devices_ownership() {
for device in "${CHANGED_DEVICES[@]}"; do
local device_path="/dev/input/event$device"
sudo chown root:input "$device_path" "$device_path"
sudo chmod 660 "$device_path" "$device_path"
done
}
primary_gamescope() {
eval "$GAMESCOPE_BIN $GAMESCOPE_PARAMS -- $STEAM_BIN $STEAM_PARAMS"
GAMESCOPE_PRIMARY_PID=$!
}
secondary_gamescope() {
eval "sudo $RUNAS_BIN -X $RUNAS_ACC -- env PULSE_SERVER=tcp:127.0.0.1:4713 $GAMESCOPE_BIN $GAMESCOPE_PARAMS -- $STEAM_BIN $STEAM_PARAMS"
GAMESCOPE_SECONDARY_PID=$!
}
trap cleanup EXIT INT TERM
main() {
echo "DualScope by NaviVani "
if [[ $EUID -eq 0 ]]; then
echo "Running as sudo, exiting..."
exit 1
fi
for bin in "$GAMESCOPE_BIN" "$STEAM_BIN" "$RUNAS_BIN"; do
if [[ ! -x "$bin" ]]; then
echo "The $bin executable was not found, exiting..."
exit 1
fi
done
get_devices_to_change "$(whoami)"
get_devices_to_change "$RUNAS_ACC"
echo "Killing steam before starting..."
pkill steam
while pgrep steam > /dev/null; do
sleep 1
done
startup
echo "Opening gamescope sessions!"
secondary_gamescope &
primary_gamescope
}
main "$@"
@Jaktifer
Copy link

Jaktifer commented Nov 22, 2025

Hey! discovered your script and thought. Yes! that's an idea! so I took a few minutes and made it compatible with arch. I thought since it was your original idea ill give you my script based on yours. Free use for anyone. oh and fyi, managing input through system paths is stupid difficult with little pay off. It's easier to use steams controller isolation and simply disable input from that specific controller.

#!/usr/bin/env bash

DualScope (single-user, Garuda/Arch, two Steam instances)

Launch two Gamescope + Steam sessions with isolated runtimes.

set -euo pipefail

PRIMARY_W=1280
PRIMARY_H=720
SECONDARY_W=1280
SECONDARY_H=720

PRIMARY_RATE="60"
SECONDARY_RATE="60"

STEAM_PARAMS="-gamepadui"

GAMESCOPE_BIN="/usr/bin/gamescope"
STEAM_BIN="/usr/bin/steam"

COMMON_GAMESCOPE_FLAGS=(-e -b)

PRIMARY_PID=""
SECONDARY_PID=""

log() { printf "[DualScope] %s\n" "$*"; }

require_bin() {
local bin="$1"
if [[ ! -x "$bin" ]]; then
log "Missing binary: $bin"
exit 1
fi
}

cleanup() {
[[ -n "${PRIMARY_PID}" ]] && kill -TERM "${PRIMARY_PID}" 2>/dev/null || true
[[ -n "${SECONDARY_PID}" ]] && kill -TERM "${SECONDARY_PID}" 2>/dev/null || true
}
trap cleanup EXIT INT TERM

build_gs_args() {
local w="$1" h="$2" rate="${3:-}"
local args=("${COMMON_GAMESCOPE_FLAGS[@]}" -w "${w}" -h "${h}" -W "${w}" -H "${h}")
[[ -n "${rate}" ]] && args+=(-r "${rate}")
echo "${args[@]}"
}

launch_primary() {
local args
args=$(build_gs_args "${PRIMARY_W}" "${PRIMARY_H}" "${PRIMARY_RATE}")
log "Launching primary Steam..."
"${GAMESCOPE_BIN}" ${args} -- "${STEAM_BIN}" ${STEAM_PARAMS} &
PRIMARY_PID=$!
}

launch_secondary() {
local args
args=$(build_gs_args "${SECONDARY_W}" "${SECONDARY_H}" "${SECONDARY_RATE}")
log "Launching secondary Steam with isolated runtime..."

Create a separate runtime dir for the second instance

mkdir -p "$HOME/.steam-second"
HOME="$HOME/.steam-second" "${GAMESCOPE_BIN}" ${args} -- "${STEAM_BIN}" ${STEAM_PARAMS} &
SECONDARY_PID=$!
}

main() {
if [[ $EUID -eq 0 ]]; then
log "Do not run as root."
exit 1
fi

require_bin "${GAMESCOPE_BIN}"
require_bin "${STEAM_BIN}"

log "Launching two independent Steam+Gamescope sessions..."
launch_primary
sleep 1
launch_secondary

log "Primary PID: ${PRIMARY_PID}"
log "Secondary PID: ${SECONDARY_PID}"

wait "${PRIMARY_PID}" || true
wait "${SECONDARY_PID}" || true
}

main "$@"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment