Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
254 changes: 254 additions & 0 deletions tablet-vnc.sh
Original file line number Diff line number Diff line change
@@ -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 <px> : Horizontal resolution (Default: $DEFAULT_WIDTH)"
echo " --height <px> : Vertical resolution (Default: $DEFAULT_HEIGHT)"
echo " --refresh <hz> : Refresh rate (Default: $DEFAULT_REFRESH)"
echo " --display <name> : Unused display output (e.g., HDMI-1) (Default: $DEFAULT_UNUSED_DISPLAY)"
echo " --position <pos> : Position relative to primary screen (Default: $DEFAULT_POSITION)"
echo " --password <pass> : (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