From f2f78e85f6137c8688e1a8b9541bc4a4b0cefd57 Mon Sep 17 00:00:00 2001 From: Rodrigo Rosa <889811+rlrosa@users.noreply.github.com> Date: Mon, 22 Sep 2025 09:34:30 -0700 Subject: [PATCH] feat: Create script to automate virtual second screen This commit introduces a comprehensive bash script, `tablet-vnc.sh`, that automates the entire process of creating, managing, and tearing down a virtual second screen as detailed in README.md. The script is designed to be user-friendly, replacing the manual multi-step process with a single command. It includes features such as: * Named arguments for all options (resolution, display, position). * Automatic detection of the primary display. * Optional password protection for the VNC session. * Robust cleanup of all display modes and temporary files on exit, including * when interrupted with Ctrl+C. ==== Usage Examples ==== 1. Start the virtual screen with default settings (1920x1200 on DisplayPort-0): ./tablet-vnc.sh start 2. To start a custom 1280x800 screen on HDMI-1 to the right of the primary display, protected with a password: ./tablet-vnc.sh start --width 1280 --height 800 \ --display HDMI-1 --position right-of --password "your_secret_password" If you really want a "secret" password you'll want to modify the script to read it from CLI. As is right now it'll be saved in the bash history, so don't use an important password. 3. To stop and clean up the virtual screen use `ctrl+c` or: ./tablet-vnc.sh stop --- tablet-vnc.sh | 254 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100755 tablet-vnc.sh diff --git a/tablet-vnc.sh b/tablet-vnc.sh new file mode 100755 index 0000000..f6c783b --- /dev/null +++ b/tablet-vnc.sh @@ -0,0 +1,254 @@ +#!/bin/bash + +# --- +# A script to create a virtual second screen on an unused display output. +# Ideal for use with an Android tablet and a VNC client. +# Based on the tutorial from: https://github.com/santiagofdezg/linux-extend-screen +# --- + +# --- Configuration --- +# You can change the default values here or pass them as arguments. +DEFAULT_WIDTH=1920 +DEFAULT_HEIGHT=1200 +DEFAULT_REFRESH=60 +DEFAULT_UNUSED_DISPLAY="DisplayPort-0" +DEFAULT_POSITION="left-of" # Options: left-of, right-of, above, below + +# Temporary file to store the state of the created mode +STATE_FILE="/tmp/extended_screen_state" +PASS_FILE="/tmp/vnc_pass_$$" + +# --- Functions --- + +# Function to clean up temporary files and screen configuration on exit/interruption +cleanup() { + echo "" # Add a newline for cleaner output after ^C + echo "--- Interrupted: Cleaning up virtual screen and temporary files ---" + + # Clean up the password file + rm -f "$PASS_FILE" + + # Clean up the screen configuration if the state file exists + if [ -f "$STATE_FILE" ]; then + read -r UNUSED_DISPLAY MODE_NAME <"$STATE_FILE" + + if [ -n "$UNUSED_DISPLAY" ] && [ -n "$MODE_NAME" ]; then + echo "Disabling display '$UNUSED_DISPLAY' and removing mode '$MODE_NAME'..." + # Turn off the display and remove the mode, redirecting output to prevent error spam + xrandr --output "$UNUSED_DISPLAY" --off >/dev/null 2>&1 + xrandr --delmode "$UNUSED_DISPLAY" "$MODE_NAME" >/dev/null 2>&1 + # Check if mode is still in use by another monitor before removing globally + if ! xrandr | grep -B1 "$MODE_NAME" | grep -q " connected"; then + xrandr --rmmode "$MODE_NAME" >/dev/null 2>&1 + fi + fi + rm -f "$STATE_FILE" + fi + echo "Cleanup complete." + # Exit the script to prevent it from continuing after cleanup + exit 0 +} + +# Trap interruption signals (like Ctrl+C) to run the cleanup function +trap cleanup HUP INT QUIT TERM + +# Function to display how to use the script +usage() { + echo "Usage: $0 {start|stop} [options]" + echo "" + echo "Actions:" + echo " start : Sets up and enables the virtual screen." + echo " stop : Disables and removes the virtual screen configuration." + echo "" + echo "Options for 'start':" + echo " --width : Horizontal resolution (Default: $DEFAULT_WIDTH)" + echo " --height : Vertical resolution (Default: $DEFAULT_HEIGHT)" + echo " --refresh : Refresh rate (Default: $DEFAULT_REFRESH)" + echo " --display : Unused display output (e.g., HDMI-1) (Default: $DEFAULT_UNUSED_DISPLAY)" + echo " --position : Position relative to primary screen (Default: $DEFAULT_POSITION)" + echo " --password : (Optional) Set a password for the VNC connection." + echo " -h, --help : Show this help message." + echo "" + echo "Example: $0 start --width 1280 --height 800 --display HDMI-1 --password mysecret" + exit 1 +} + +# Function to check for required commands +check_dependencies() { + for cmd in xrandr gtf x11vnc ip hostname; do + if ! command -v "$cmd" &> /dev/null; then + echo "Error: Required command '$cmd' not found." + echo "Please install it to continue. (e.g., 'sudo apt install x11vnc iproute2')" + exit 1 + fi + done +} + +# --- Main Script Logic --- + +# Check for dependencies first +check_dependencies + +ACTION="$1" +shift # Consume the action (start/stop) + +# Handle the action +case "$ACTION" in + start) + # Set default values + WIDTH=$DEFAULT_WIDTH + HEIGHT=$DEFAULT_HEIGHT + REFRESH=$DEFAULT_REFRESH + UNUSED_DISPLAY=$DEFAULT_UNUSED_DISPLAY + POSITION=$DEFAULT_POSITION + PASSWORD="" + + # Parse named arguments + while [ "$#" -gt 0 ]; do + case "$1" in + --width) WIDTH="$2"; shift 2 ;; + --height) HEIGHT="$2"; shift 2 ;; + --refresh) REFRESH="$2"; shift 2 ;; + --display) UNUSED_DISPLAY="$2"; shift 2 ;; + --position) POSITION="$2"; shift 2 ;; + --password) PASSWORD="$2"; shift 2 ;; + -h|--help) usage ;; + *) echo "Unknown option: $1"; usage ;; + esac + done + + # Auto-detect the primary display + PRIMARY_DISPLAY=$(xrandr | grep ' primary ' | awk '{print $1}') + if [ -z "$PRIMARY_DISPLAY" ]; + then + echo "Error: Could not automatically detect the primary display." + exit 1 + fi + + echo "--- Starting Virtual Screen Setup ---" + echo "Primary Display: $PRIMARY_DISPLAY" + echo "Virtual Display: $UNUSED_DISPLAY at ${WIDTH}x${HEIGHT}@${REFRESH}Hz" + echo "Position: $POSITION $PRIMARY_DISPLAY" + + # 1. Generate the modeline + REFRESH_FORMATTED=$(printf "%.2f" "$REFRESH") + MODE_NAME=$(printf "%sx%s_%s" "$WIDTH" "$HEIGHT" "${REFRESH_FORMATTED//./_}") + + # 2. Check if the mode already exists. If not, create it. + if ! xrandr | grep -q "$MODE_NAME"; then + echo "Mode '$MODE_NAME' not found. Creating it..." + FULL_MODELINE=$(gtf "$WIDTH" "$HEIGHT" "$REFRESH" | grep 'Modeline') + if [ -z "$FULL_MODELINE" ]; then + echo "Error: Could not generate a modeline for the given resolution." + exit 1 + fi + MODE_DETAILS=$(echo "$FULL_MODELINE" | awk '{$1=$2=""; print $0}') + + echo "Creating new mode..." + xrandr --newmode "$MODE_NAME" $MODE_DETAILS || { + echo "Failed to create new mode." + exit 1 + } + else + echo "Mode '$MODE_NAME' already exists. Using existing mode." + fi + + # 3. Add the mode to the unused display + echo "Adding mode to display '$UNUSED_DISPLAY'..." + xrandr --addmode "$UNUSED_DISPLAY" "$MODE_NAME" || { + echo "Failed to add mode to display." + exit 1 + } + + # 4. Enable the output and position it + echo "Enabling and positioning display..." + xrandr --output "$UNUSED_DISPLAY" --mode "$MODE_NAME" --"$POSITION" "$PRIMARY_DISPLAY" || { + echo "Failed to enable and position display." + exit 1 + } + + sleep 2 + + # 5. Calculate the VNC clip region dynamically + echo "Calculating VNC clip region..." + VNC_X=0 + VNC_Y=0 + SCREEN_INFO=$(xrandr | grep "$UNUSED_DISPLAY" | grep -o '[0-9]*x[0-9]*+[0-9]*+[0-9]*') + if [[ $SCREEN_INFO =~ \+([0-9]+)\+([0-9]+)$ ]]; then + VNC_X=${BASH_REMATCH[1]} + VNC_Y=${BASH_REMATCH[2]} + echo "Detected screen position at ${VNC_X}, ${VNC_Y}" + else + echo "Warning: Could not automatically determine screen position for VNC. Defaulting to 0,0." + fi + VNC_CLIP="${WIDTH}x${HEIGHT}+${VNC_X}+${VNC_Y}" + + # Save the state for the 'stop' command and the cleanup trap + echo "$UNUSED_DISPLAY $MODE_NAME" >"$STATE_FILE" + + # 6. Start the VNC server + echo "" + echo "--- Starting VNC Server ---" + echo "Clipping region: $VNC_CLIP" + echo "Hostname: $(hostname)" + echo "Your IP addresses are:" + ip -4 addr show | grep -oP 'inet \K[\d.]+' + echo "" + echo "Connect your VNC client to one of the IPs above on port 5900." + echo "Press Ctrl+C in this terminal to stop the VNC server and clean up." + echo "Or run '$0 stop' in another terminal." + + VNC_CMD="x11vnc -clip \"$VNC_CLIP\" -repeat -noxdamage -forever" + if [ -n "$PASSWORD" ]; then + echo "Using password protection." + # Use x11vnc's utility to create a properly formatted password file. + x11vnc -storepasswd "$PASSWORD" "$PASS_FILE" + VNC_CMD+=" -rfbauth $PASS_FILE" + fi + + eval $VNC_CMD + + echo "VNC server stopped." + # If VNC server stops for any reason other than Ctrl+C, run cleanup + # The trap handles Ctrl+C and exits, so this code won't be reached in that case. + cleanup + ;; + + stop) + echo "--- Stopping Virtual Screen ---" + if [ ! -f "$STATE_FILE" ]; then + echo "Warning: State file not found. Try running 'start' first." + exit 1 + fi + + read -r UNUSED_DISPLAY MODE_NAME <"$STATE_FILE" + + if [ -z "$UNUSED_DISPLAY" ] || [ -z "$MODE_NAME" ]; then + echo "Error: Could not read valid settings from state file." + exit 1 + fi + + echo "Disabling display '$UNUSED_DISPLAY'..." + xrandr --output "$UNUSED_DISPLAY" --off + + echo "Removing mode '$MODE_NAME' from display..." + xrandr --delmode "$UNUSED_DISPLAY" "$MODE_NAME" || echo "Warning: Could not delete mode from display. It might not have been added or is stuck." + + if ! xrandr | grep -B1 "$MODE_NAME" | grep -q " connected"; then + echo "Deleting global mode '$MODE_NAME'..." + xrandr --rmmode "$MODE_NAME" || echo "Warning: Could not remove global mode. It may be stuck or was already removed." + else + echo "Mode '$MODE_NAME' is still in use by another monitor; not removing it." + fi + + rm "$STATE_FILE" + echo "Virtual screen has been disabled and cleaned up." + ;; + + *) + usage + ;; +esac + +exit 0 +