From 5813a0b266a8e6cec96b6c7603138447d6fa512a Mon Sep 17 00:00:00 2001 From: Tony Dursun Date: Fri, 21 Aug 2026 00:40:19 -0400 Subject: [PATCH 1/2] feat: pass the exact display scale to FreeRDP RDP_SCALE was rounded to 100, 140 or 180 on the premise that FreeRDP only supports those values. That is true of /scale, but not of /scale-desktop, which accepts any integer from 100 to 500. A user asking for 125 silently got 140. Pass the configured value through as /scale-desktop, and derive the device scale separately as /scale-device. Both factors still get set: /scale set them together, so switching to /scale-desktop alone would have quietly reset DeviceScaleFactor to 100 and made Store apps worse. The device factor genuinely is limited to 100, 140 and 180, so it is still approximated - but only it, and the debug output now says so rather than claiming the whole value was unsupported. The per-launch notify-send is gone. waFixScale runs on every configuration load, so it fired on every launch to report something the user cannot act on. RDP_SCALE is now validated, since the old rounding incidentally sanitised it. A non-integer is fatal; a value outside 100-500 is clamped with a warning rather than rejected, so an existing misconfigured setup keeps working instead of failing to launch. Co-Authored-By: Claude Opus 5 Signed-off-by: Tony Dursun --- README.md | 12 +++----- bin/winapps | 87 +++++++++++++++++++++++++++++++++++++---------------- setup.sh | 81 ++++++++++++++++++++++++++++++++++--------------- 3 files changed, 122 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 5e7e88a0..5ec8c33c 100644 --- a/README.md +++ b/README.md @@ -442,13 +442,11 @@ WAFLAVOR="docker" # [DISPLAY SCALING FACTOR] # NOTES: -# - If an unsupported value is specified, a warning will be displayed. -# - If an unsupported value is specified, WinApps will use the closest supported value. +# - RDP_SCALE must be an integer. Values outside the supported range will be clamped. +# - WinApps automatically approximates the device scale to 100, 140 or 180. # DEFAULT VALUE: '100' -# VALID VALUES: -# - '100' -# - '140' -# - '180' +# VALID RANGE: +# - '100' to '500' RDP_SCALE="100" # [MOUNTING REMOVABLE PATHS FOR FILES] @@ -571,7 +569,7 @@ HIDEF="on" - If using a pre-existing Windows RDP server on your LAN, you must use `RDP_IP` to specify the location of the Windows server. You may also wish to configure a static IP address for this server. - If running a Windows VM using `libvirt` with NAT enabled, leave `RDP_IP` commented out and WinApps will auto-detect the local IP address for the VM. - For domain users, you can uncomment and change `RDP_DOMAIN`. -- On high-resolution (UHD) displays, you can set `RDP_SCALE` to the scale you would like to use (100, 140 or 180). +- On high-resolution (UHD) displays, you can set `RDP_SCALE` to an integer from 100 to 500. WinApps passes that exact desktop scale to FreeRDP and approximates the device scale to the nearest supported value (100, 140 or 180). - To add additional flags to the FreeRDP call (e.g. `/prevent-session-lock 120`), uncomment and use the `RDP_FLAGS` configuration option. - For multi-monitor setups, you can try adding `/multimon` to `RDP_FLAGS`. A FreeRDP bug may result in a black screen however, in which case you should revert this change. - To enable non-English input and seamless language switching, you can try adding `/kbd:unicode` to `RDP_FLAGS`. This ensures client inputs are sent as Unicode sequences. diff --git a/bin/winapps b/bin/winapps index 0a074bf0..4dd329a7 100755 --- a/bin/winapps +++ b/bin/winapps @@ -17,6 +17,7 @@ readonly EC_NO_IP=12 readonly EC_BAD_PORT=13 readonly EC_UNSUPPORTED_APP=14 readonly EC_INVALID_FLAVOR=15 +readonly EC_INVALID_SCALE=16 # PATHS readonly APPDATA_PATH="${HOME}/.local/share/winapps" @@ -50,6 +51,7 @@ RDP_FLAGS="" FREERDP_COMMAND="" REMOVABLE_MEDIA="" RDP_SCALE=100 +RDP_DEVICE_SCALE=100 AUTOPAUSE="off" AUTOPAUSE_TIME="300" DEBUG="true" @@ -281,6 +283,10 @@ Please run: dprint "ERROR: INVALID FLAVOR. EXITING." notify-send --expire-time=8000 --icon="dialog-error" --app-name="WinApps" --urgency="low" "WinApps" "Invalid WinApps flavor.\nPlease ensure 'docker', 'podman' or 'libvirt' are specified as the flavor in the WinApps configuration file." ;; + "$EC_INVALID_SCALE") + dprint "ERROR: RDP_SCALE MUST BE AN INTEGER. EXITING." + notify-send --expire-time=8000 --icon="dialog-error" --app-name="WinApps" --urgency="low" "WinApps" "Invalid RDP_SCALE value '${RDP_SCALE}'.\nPlease specify an integer value." + ;; esac # Terminate the script. @@ -328,37 +334,62 @@ function waHelp() { } # Name: 'waFixScale' -# Role: Since FreeRDP only supports '/scale' values of 100, 140 or 180, find the closest supported argument to the user's configuration. +# Role: Validate the desktop scale and derive FreeRDP's nearest supported device scale. function waFixScale() { # Define variables. - local OLD_SCALE=100 + local OLD_SCALE="$RDP_SCALE" local VALID_SCALE_1=100 local VALID_SCALE_2=140 local VALID_SCALE_3=180 - # Check for an unsupported value. - if [ "$RDP_SCALE" != "$VALID_SCALE_1" ] && [ "$RDP_SCALE" != "$VALID_SCALE_2" ] && [ "$RDP_SCALE" != "$VALID_SCALE_3" ]; then - # Save the unsupported scale. - OLD_SCALE="$RDP_SCALE" - - # Calculate the absolute differences. - local DIFF_1=$(( RDP_SCALE > VALID_SCALE_1 ? RDP_SCALE - VALID_SCALE_1 : VALID_SCALE_1 - RDP_SCALE )) - local DIFF_2=$(( RDP_SCALE > VALID_SCALE_2 ? RDP_SCALE - VALID_SCALE_2 : VALID_SCALE_2 - RDP_SCALE )) - local DIFF_3=$(( RDP_SCALE > VALID_SCALE_3 ? RDP_SCALE - VALID_SCALE_3 : VALID_SCALE_3 - RDP_SCALE )) - - # Set the final scale to the valid scale value with the smallest absolute difference. - if (( DIFF_1 <= DIFF_2 && DIFF_1 <= DIFF_3 )); then - RDP_SCALE="$VALID_SCALE_1" - elif (( DIFF_2 <= DIFF_1 && DIFF_2 <= DIFF_3 )); then - RDP_SCALE="$VALID_SCALE_2" - else - RDP_SCALE="$VALID_SCALE_3" + # Reject values that cannot be passed to FreeRDP as integers. + if ! [[ "$RDP_SCALE" =~ ^[+-]?[0-9]+$ ]]; then + waThrowExit "$EC_INVALID_SCALE" + fi + + # Strip leading zeros so the length-based overflow guard uses the normalised magnitude. + local SCALE_DIGITS="${RDP_SCALE#[+-]}" + local SCALE_SIGN="${RDP_SCALE%"$SCALE_DIGITS"}" + while [[ "$SCALE_DIGITS" == 0* ]] && [ "$SCALE_DIGITS" != "0" ]; do + SCALE_DIGITS="${SCALE_DIGITS#0}" + done + + # Clamp the desktop scale to FreeRDP's supported range. + if [ "$SCALE_SIGN" = "-" ]; then + RDP_SCALE=100 + dprint "WARNING: RDP_SCALE value '${OLD_SCALE}' is below 100. Clamping desktop scale to '${RDP_SCALE}'." + elif (( ${#SCALE_DIGITS} > 3 )); then + RDP_SCALE=500 + dprint "WARNING: RDP_SCALE value '${OLD_SCALE}' is above 500. Clamping desktop scale to '${RDP_SCALE}'." + else + RDP_SCALE=$(( 10#$SCALE_DIGITS )) + if (( RDP_SCALE < 100 )); then + RDP_SCALE=100 + dprint "WARNING: RDP_SCALE value '${OLD_SCALE}' is below 100. Clamping desktop scale to '${RDP_SCALE}'." + elif (( RDP_SCALE > 500 )); then + RDP_SCALE=500 + dprint "WARNING: RDP_SCALE value '${OLD_SCALE}' is above 500. Clamping desktop scale to '${RDP_SCALE}'." fi + fi + + # Find the supported device scale nearest to the desktop scale. + local DIFF_1=$(( RDP_SCALE > VALID_SCALE_1 ? RDP_SCALE - VALID_SCALE_1 : VALID_SCALE_1 - RDP_SCALE )) + local DIFF_2=$(( RDP_SCALE > VALID_SCALE_2 ? RDP_SCALE - VALID_SCALE_2 : VALID_SCALE_2 - RDP_SCALE )) + local DIFF_3=$(( RDP_SCALE > VALID_SCALE_3 ? RDP_SCALE - VALID_SCALE_3 : VALID_SCALE_3 - RDP_SCALE )) + + if (( DIFF_1 <= DIFF_2 && DIFF_1 <= DIFF_3 )); then + RDP_DEVICE_SCALE="$VALID_SCALE_1" + elif (( DIFF_2 <= DIFF_1 && DIFF_2 <= DIFF_3 )); then + RDP_DEVICE_SCALE="$VALID_SCALE_2" + else + RDP_DEVICE_SCALE="$VALID_SCALE_3" + fi - # Print feedback. - dprint "WARNING: Unsupported RDP_SCALE value '${OLD_SCALE}'. Defaulting to '${RDP_SCALE}'." - notify-send --expire-time=4000 --icon="dialog-warning" --app-name="WinApps" --urgency="low" "WinApps" "Unsupported RDP_SCALE value '${OLD_SCALE}'.\nDefaulting to '${RDP_SCALE}'." + if [ "$RDP_DEVICE_SCALE" != "$RDP_SCALE" ]; then + dprint "NOTICE: Approximating only the RDP device scale from '${RDP_SCALE}' to '${RDP_DEVICE_SCALE}'; desktop scale remains '${RDP_SCALE}'." fi + + return 0 } # Name: 'waLoadConfig' @@ -811,7 +842,8 @@ function waRunCommand() { /d:"$RDP_DOMAIN" \ /u:"$RDP_USER" \ ${RDP_PASSWORD_ARG:+"$RDP_PASSWORD_ARG"} \ - /scale:"$RDP_SCALE" \ + /scale-desktop:"$RDP_SCALE" \ + /scale-device:"$RDP_DEVICE_SCALE" \ +auto-reconnect \ +dynamic-resolution \ /wm-class:"Microsoft Windows" \ @@ -829,7 +861,8 @@ function waRunCommand() { /d:"$RDP_DOMAIN" \ /u:"$RDP_USER" \ ${RDP_PASSWORD_ARG:+"$RDP_PASSWORD_ARG"} \ - /scale:"$RDP_SCALE" \ + /scale-desktop:"$RDP_SCALE" \ + /scale-device:"$RDP_DEVICE_SCALE" \ +auto-reconnect \ /app:program:"$2",hidef:"$HIDEF" \ /v:"$RDP_IP":"$RDP_PORT" &>/dev/null & @@ -863,7 +896,8 @@ function waRunCommand() { /d:"$RDP_DOMAIN" \ /u:"$RDP_USER" \ ${RDP_PASSWORD_ARG:+"$RDP_PASSWORD_ARG"} \ - /scale:"$RDP_SCALE" \ + /scale-desktop:"$RDP_SCALE" \ + /scale-device:"$RDP_DEVICE_SCALE" \ +auto-reconnect \ /wm-class:"$FULL_NAME" \ /app:program:"$WIN_EXECUTABLE",hidef:"$HIDEF",icon:"$ICON",name:"$FULL_NAME" \ @@ -886,7 +920,8 @@ function waRunCommand() { /d:"$RDP_DOMAIN" \ /u:"$RDP_USER" \ ${RDP_PASSWORD_ARG:+"$RDP_PASSWORD_ARG"} \ - /scale:"$RDP_SCALE" \ + /scale-desktop:"$RDP_SCALE" \ + /scale-device:"$RDP_DEVICE_SCALE" \ +auto-reconnect \ /drive:media,"$REMOVABLE_MEDIA" \ /wm-class:"$FULL_NAME" \ diff --git a/setup.sh b/setup.sh index 856f750f..b85a79df 100755 --- a/setup.sh +++ b/setup.sh @@ -32,6 +32,7 @@ readonly EC_BAD_PORT="13" # Windows is unreachable via RDP_PORT. readonly EC_RDP_FAIL="14" # FreeRDP failed to establish a connection with Windows. readonly EC_APPQUERY_FAIL="15" # Failed to query Windows for installed applications. readonly EC_INVALID_FLAVOR="16" # Backend specified is not 'libvirt', 'docker' or 'podman'. +readonly EC_INVALID_SCALE="17" # RDP_SCALE is not an integer. # PATHS # 'BIN' @@ -93,6 +94,7 @@ RDP_IP="" # Imported variable. VM_NAME="RDPWindows" # Name of the Windows VM (FOR 'libvirt' ONLY). WAFLAVOR="docker" # Imported variable. RDP_SCALE=100 # Imported variable. +RDP_DEVICE_SCALE=100 # Derived from RDP_SCALE. RDP_FLAGS="" # Imported variable. DEBUG="true" # Imported variable. FREERDP_COMMAND="" # Imported variable. @@ -501,36 +503,63 @@ function waCheckExistingInstall() { # Name: 'waFixScale' -# Role: Since FreeRDP only supports '/scale' values of 100, 140 or 180, find the closest supported argument to the user's configuration. +# Role: Validate the desktop scale and derive FreeRDP's nearest supported device scale. function waFixScale() { # Define variables. - local OLD_SCALE=100 + local OLD_SCALE="$RDP_SCALE" local VALID_SCALE_1=100 local VALID_SCALE_2=140 local VALID_SCALE_3=180 - # Check for an unsupported value. - if [ "$RDP_SCALE" != "$VALID_SCALE_1" ] && [ "$RDP_SCALE" != "$VALID_SCALE_2" ] && [ "$RDP_SCALE" != "$VALID_SCALE_3" ]; then - # Save the unsupported scale. - OLD_SCALE="$RDP_SCALE" - - # Calculate the absolute differences. - local DIFF_1=$(( RDP_SCALE > VALID_SCALE_1 ? RDP_SCALE - VALID_SCALE_1 : VALID_SCALE_1 - RDP_SCALE )) - local DIFF_2=$(( RDP_SCALE > VALID_SCALE_2 ? RDP_SCALE - VALID_SCALE_2 : VALID_SCALE_2 - RDP_SCALE )) - local DIFF_3=$(( RDP_SCALE > VALID_SCALE_3 ? RDP_SCALE - VALID_SCALE_3 : VALID_SCALE_3 - RDP_SCALE )) - - # Set the final scale to the valid scale value with the smallest absolute difference. - if (( DIFF_1 <= DIFF_2 && DIFF_1 <= DIFF_3 )); then - RDP_SCALE="$VALID_SCALE_1" - elif (( DIFF_2 <= DIFF_1 && DIFF_2 <= DIFF_3 )); then - RDP_SCALE="$VALID_SCALE_2" - else - RDP_SCALE="$VALID_SCALE_3" + # Reject values that cannot be passed to FreeRDP as integers. + if ! [[ "$RDP_SCALE" =~ ^[+-]?[0-9]+$ ]]; then + echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}RDP_SCALE must be an integer; received '${RDP_SCALE}'.${CLEAR_TEXT}" + return "$EC_INVALID_SCALE" + fi + + # Strip leading zeros so the length-based overflow guard uses the normalised magnitude. + local SCALE_DIGITS="${RDP_SCALE#[+-]}" + local SCALE_SIGN="${RDP_SCALE%"$SCALE_DIGITS"}" + while [[ "$SCALE_DIGITS" == 0* ]] && [ "$SCALE_DIGITS" != "0" ]; do + SCALE_DIGITS="${SCALE_DIGITS#0}" + done + + # Clamp the desktop scale to FreeRDP's supported range. + if [ "$SCALE_SIGN" = "-" ]; then + RDP_SCALE=100 + echo -e "${WARNING_TEXT}[WARNING]${CLEAR_TEXT} RDP_SCALE value '${OLD_SCALE}' is below 100. Clamping desktop scale to '${RDP_SCALE}'." + elif (( ${#SCALE_DIGITS} > 3 )); then + RDP_SCALE=500 + echo -e "${WARNING_TEXT}[WARNING]${CLEAR_TEXT} RDP_SCALE value '${OLD_SCALE}' is above 500. Clamping desktop scale to '${RDP_SCALE}'." + else + RDP_SCALE=$(( 10#$SCALE_DIGITS )) + if (( RDP_SCALE < 100 )); then + RDP_SCALE=100 + echo -e "${WARNING_TEXT}[WARNING]${CLEAR_TEXT} RDP_SCALE value '${OLD_SCALE}' is below 100. Clamping desktop scale to '${RDP_SCALE}'." + elif (( RDP_SCALE > 500 )); then + RDP_SCALE=500 + echo -e "${WARNING_TEXT}[WARNING]${CLEAR_TEXT} RDP_SCALE value '${OLD_SCALE}' is above 500. Clamping desktop scale to '${RDP_SCALE}'." fi + fi - # Print feedback. - echo -e "${WARNING_TEXT}[WARNING]${CLEAR_TEXT} Unsupported RDP_SCALE value '${OLD_SCALE}' detected. Defaulting to '${RDP_SCALE}'." + # Find the supported device scale nearest to the desktop scale. + local DIFF_1=$(( RDP_SCALE > VALID_SCALE_1 ? RDP_SCALE - VALID_SCALE_1 : VALID_SCALE_1 - RDP_SCALE )) + local DIFF_2=$(( RDP_SCALE > VALID_SCALE_2 ? RDP_SCALE - VALID_SCALE_2 : VALID_SCALE_2 - RDP_SCALE )) + local DIFF_3=$(( RDP_SCALE > VALID_SCALE_3 ? RDP_SCALE - VALID_SCALE_3 : VALID_SCALE_3 - RDP_SCALE )) + + if (( DIFF_1 <= DIFF_2 && DIFF_1 <= DIFF_3 )); then + RDP_DEVICE_SCALE="$VALID_SCALE_1" + elif (( DIFF_2 <= DIFF_1 && DIFF_2 <= DIFF_3 )); then + RDP_DEVICE_SCALE="$VALID_SCALE_2" + else + RDP_DEVICE_SCALE="$VALID_SCALE_3" fi + + if [ "$RDP_DEVICE_SCALE" != "$RDP_SCALE" ]; then + echo -e "${INFO_TEXT}Approximating only the RDP device scale from '${RDP_SCALE}' to '${RDP_DEVICE_SCALE}'; desktop scale remains '${RDP_SCALE}'.${CLEAR_TEXT}" + fi + + return 0 } # Name: 'waLoadConfig' @@ -1156,7 +1185,8 @@ function waCheckRDPAccess() { /d:"$RDP_DOMAIN" \ /u:"$RDP_USER" \ ${RDP_PASSWORD_ARG:+"$RDP_PASSWORD_ARG"} \ - /scale:"$RDP_SCALE" \ + /scale-desktop:"$RDP_SCALE" \ + /scale-device:"$RDP_DEVICE_SCALE" \ +auto-reconnect \ +home-drive \ /app:program:"C:\Windows\System32\cmd.exe",cmd:"/C type NUL > $TEST_PATH_WIN && tsdiscon" \ @@ -1289,7 +1319,8 @@ function waFindInstalled() { /d:"$RDP_DOMAIN" \ /u:"$RDP_USER" \ ${RDP_PASSWORD_ARG:+"$RDP_PASSWORD_ARG"} \ - /scale:"$RDP_SCALE" \ + /scale-desktop:"$RDP_SCALE" \ + /scale-device:"$RDP_DEVICE_SCALE" \ +auto-reconnect \ +home-drive \ /app:program:"C:\Windows\System32\cmd.exe",cmd:"/C $BATCH_SCRIPT_PATH_WIN" \ @@ -1724,7 +1755,7 @@ function waInstall() { waCheckInstallDependencies # Update $RDP_SCALE. - waFixScale + waFixScale || return "$?" # Append additional FreeRDP flags if required. if [[ -n $RDP_FLAGS ]]; then @@ -1921,7 +1952,7 @@ function waAddApps() { waCheckInstallDependencies # Update $RDP_SCALE. - waFixScale + waFixScale || return "$?" # Append additional FreeRDP flags if required. if [[ -n $RDP_FLAGS ]]; then From 3c137bca6670e0604b919da7b641f375fffeef3a Mon Sep 17 00:00:00 2001 From: Tony Dursun Date: Tue, 1 Sep 2026 05:56:15 -0400 Subject: [PATCH 2/2] feat: add a separate full-desktop scale and detect the host scale RDP_SCALE now controls RemoteApp launches only. RDP_DESKTOP_SCALE controls the full-desktop session started by 'winapps windows' and defaults to the RDP_SCALE value, as requested in review. Its name refers to the launch type, not to FreeRDP's /scale-desktop option: both settings supply a /scale-desktop value for their own launch type, and each derives its own /scale-device. Both settings also accept 'auto', which is the new default. On Cinnamon under X11 that reads the primary monitor's applied logical scale from Muffin's DisplayConfig and converts it to a percentage. The scale is read as a decimal string and converted with integer arithmetic, so no locale-sensitive float parsing is involved. Detection is best effort and never blocks a launch. gdbus is guarded by command -v; it normally ships with Cinnamon, and without it automatic detection falls back to 100. Every failure falls back to 100 with the reason logged: tool missing, bus unreachable, unparseable reply, no primary monitor, or an out-of-range scale. An explicit number always wins and bypasses detection entirely, and two 'auto' values share a single detection result per invocation. Cinnamon quantises scale per display mode, so a nominal 175% setting can apply as 176%. The applied value is used rather than the number shown in the settings UI. Changing the display resolution is not the same as changing its scale, and only the scale is followed. Not implemented: per-monitor values, rescaling a window moved between monitors, monitor hotplug, and scale changes during a running session. Detection is verified on Cinnamon under X11; every other desktop falls back to 100. An invalid scale value now also reports to stderr, rather than only to the log and a desktop notification. Co-Authored-By: Claude Opus 5 Signed-off-by: Tony Dursun --- README.md | 26 +++- bin/winapps | 357 ++++++++++++++++++++++++++++++++++++++++++++-------- setup.sh | 352 ++++++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 632 insertions(+), 103 deletions(-) diff --git a/README.md b/README.md index 5ec8c33c..fb561b31 100644 --- a/README.md +++ b/README.md @@ -440,14 +440,28 @@ VM_NAME="RDPWindows" # - 'manual' WAFLAVOR="docker" -# [DISPLAY SCALING FACTOR] +# [DISPLAY SCALING FACTORS] # NOTES: -# - RDP_SCALE must be an integer. Values outside the supported range will be clamped. -# - WinApps automatically approximates the device scale to 100, 140 or 180. -# DEFAULT VALUE: '100' +# - RDP_SCALE controls RemoteApp launches. +# - RDP_DESKTOP_SCALE controls the full-desktop session started by 'winapps windows'. Its name +# refers to the launch type; both settings provide FreeRDP's '/scale-desktop' value. +# - Each setting accepts 'auto' or an integer. Other values stop the launch with an error; +# integers outside the supported range are clamped. +# - 'auto' detects the primary monitor's applied logical scale on Cinnamon under X11. It uses +# 'gdbus', normally installed with Cinnamon; unsupported desktops or detection failures fall back to 100. +# - An explicit integer always wins and is never replaced by automatic detection. +# - The resolved value sizes the Windows UI. WinApps derives the separate device scale automatically +# and approximates only that value to 100, 140 or 180. +# - Display resolution and display scale are different; automatic detection follows only the scale. +# - Per-monitor scaling and scale changes during a running session are not supported. +# DEFAULT VALUES: +# - RDP_SCALE: 'auto' +# - RDP_DESKTOP_SCALE: the resolved RDP_SCALE value # VALID RANGE: # - '100' to '500' -RDP_SCALE="100" +RDP_SCALE="auto" +# Keep this assignment after RDP_SCALE so its default inherits the value above. +RDP_DESKTOP_SCALE="${RDP_SCALE}" # [MOUNTING REMOVABLE PATHS FOR FILES] # NOTES: @@ -569,7 +583,7 @@ HIDEF="on" - If using a pre-existing Windows RDP server on your LAN, you must use `RDP_IP` to specify the location of the Windows server. You may also wish to configure a static IP address for this server. - If running a Windows VM using `libvirt` with NAT enabled, leave `RDP_IP` commented out and WinApps will auto-detect the local IP address for the VM. - For domain users, you can uncomment and change `RDP_DOMAIN`. -- On high-resolution (UHD) displays, you can set `RDP_SCALE` to an integer from 100 to 500. WinApps passes that exact desktop scale to FreeRDP and approximates the device scale to the nearest supported value (100, 140 or 180). +- `RDP_SCALE` controls RemoteApp launches, while `RDP_DESKTOP_SCALE` controls the full-desktop session started by `winapps windows`. Each accepts `auto` or an integer from 100 to 500; explicit numbers are honoured exactly and are never overridden. For each launch, WinApps passes the resolved value as FreeRDP's desktop scale, which sizes the Windows UI, and derives the corresponding device scale separately. Only the device value is approximated, to 100, 140 or 180. On Cinnamon under X11, `auto` uses the primary monitor's applied logical scale, which can differ slightly from the percentage shown in display settings because Cinnamon quantises scale for the active display mode. Changing the display resolution is not the same as changing its scale; automatic detection follows only the scale, so a display running below its native resolution at scale 1 resolves to 100. Detection uses the `gdbus` tool from `libglib2.0-bin` or its equivalent, which is normally installed with the Cinnamon desktop. Without it, on other desktops, or when detection fails, WinApps falls back to 100 without affecting other features. Per-monitor values, rescaling a window when it moves between monitors, monitor hotplug and scale changes during a running session are not implemented. Older configurations may omit either key: a missing `RDP_SCALE` behaves as `auto`, and a missing `RDP_DESKTOP_SCALE` inherits the resolved `RDP_SCALE` value. - To add additional flags to the FreeRDP call (e.g. `/prevent-session-lock 120`), uncomment and use the `RDP_FLAGS` configuration option. - For multi-monitor setups, you can try adding `/multimon` to `RDP_FLAGS`. A FreeRDP bug may result in a black screen however, in which case you should revert this change. - To enable non-English input and seamless language switching, you can try adding `/kbd:unicode` to `RDP_FLAGS`. This ensures client inputs are sent as Unicode sequences. diff --git a/bin/winapps b/bin/winapps index 4dd329a7..72fbea3d 100755 --- a/bin/winapps +++ b/bin/winapps @@ -50,8 +50,11 @@ WAFLAVOR="docker" RDP_FLAGS="" FREERDP_COMMAND="" REMOVABLE_MEDIA="" -RDP_SCALE=100 +RDP_SCALE="auto" RDP_DEVICE_SCALE=100 +# RDP_DESKTOP_SCALE controls full-desktop sessions, not FreeRDP's /scale-desktop option. +RDP_DESKTOP_SCALE="inherit" +RDP_DESKTOP_DEVICE_SCALE=100 AUTOPAUSE="off" AUTOPAUSE_TIME="300" DEBUG="true" @@ -216,6 +219,8 @@ function waEarlyDispatch() { function waThrowExit() { # Declare variables. local ERR_CODE="$1" + local INVALID_SCALE_KEY="${2:-RDP_SCALE}" + local INVALID_SCALE_VALUE="${3-$RDP_SCALE}" # Throw error. case "$ERR_CODE" in @@ -284,8 +289,9 @@ Please run: notify-send --expire-time=8000 --icon="dialog-error" --app-name="WinApps" --urgency="low" "WinApps" "Invalid WinApps flavor.\nPlease ensure 'docker', 'podman' or 'libvirt' are specified as the flavor in the WinApps configuration file." ;; "$EC_INVALID_SCALE") - dprint "ERROR: RDP_SCALE MUST BE AN INTEGER. EXITING." - notify-send --expire-time=8000 --icon="dialog-error" --app-name="WinApps" --urgency="low" "WinApps" "Invalid RDP_SCALE value '${RDP_SCALE}'.\nPlease specify an integer value." + dprint "ERROR: ${INVALID_SCALE_KEY} MUST BE 'auto' OR AN INTEGER. EXITING." + printf "ERROR: %s must be 'auto' or an integer; received '%s'.\n" "$INVALID_SCALE_KEY" "$INVALID_SCALE_VALUE" >&2 + notify-send --expire-time=8000 --icon="dialog-error" --app-name="WinApps" --urgency="low" "WinApps" "Invalid ${INVALID_SCALE_KEY} value '${INVALID_SCALE_VALUE}'.\nPlease specify 'auto' or an integer value." ;; esac @@ -333,61 +339,314 @@ function waHelp() { echo " cleanrdp --> Remove orphaned FreeRDP process tracking files." } +# Name: 'waDetectHostScale' +# Role: Detect the host's logical display scale. +function waDetectHostScale() { + local DISPLAY_STATE="" + local LOGICAL_MONITORS="" + local LOGICAL_MONITOR_PATTERN='\([[:space:]]*-?[0-9]+[[:space:]]*,[[:space:]]*-?[0-9]+[[:space:]]*,[[:space:]]*([^,[:space:]]*)[[:space:]]*,[[:space:]]*uint32[[:space:]]+[0-9]+[[:space:]]*,[[:space:]]*(true|false)[[:space:]]*,[[:space:]]*\[' + local REMAINDER="" + local MATCHED_ENTRY="" + local PRIMARY_SCALE="" + local SCALE_WHOLE="" + local SCALE_FRACTION="" + local SCALE_FRACTION_PADDED="" + local SCALE_HUNDREDTHS="" + local SCALE_ROUND_DIGIT="" + local SCALE_PERCENTAGE=0 + local FOUND_LOGICAL_MONITOR=0 + local FOUND_PRIMARY_MONITOR=0 + + if ! command -v gdbus >/dev/null 2>&1; then + return 1 + fi + + if ! DISPLAY_STATE="$(gdbus call --session --timeout 5 \ + --dest org.cinnamon.Muffin.DisplayConfig \ + --object-path /org/cinnamon/Muffin/DisplayConfig \ + --method org.cinnamon.Muffin.DisplayConfig.GetCurrentState 2>/dev/null)"; then + return 2 + fi + + if [ -z "$DISPLAY_STATE" ]; then + return 3 + fi + + # Extract the third top-level tuple field while respecting nested GVariant containers. + if ! LOGICAL_MONITORS="$(printf '%s\n' "$DISPLAY_STATE" | LC_ALL=C awk ' + { + value = $0 + if ((substr(value, 1, 1) != "(") || (substr(value, length(value), 1) != ")")) + exit 1 + + parentheses = 0 + brackets = 0 + braces = 0 + quoted = 0 + escaped = 0 + field = 1 + start = 2 + + for (position = 2; position < length(value); position++) { + character = substr(value, position, 1) + + if (quoted) { + if (escaped) + escaped = 0 + else if (character == "\\") + escaped = 1 + else if (character == "\047") + quoted = 0 + continue + } + + if (character == "\047") { + quoted = 1 + continue + } + + if (character == "(") + parentheses++ + else if (character == ")") + parentheses-- + else if (character == "[") + brackets++ + else if (character == "]") + brackets-- + else if (character == "{") + braces++ + else if (character == "}") + braces-- + else if ((character == ",") && (parentheses == 0) && (brackets == 0) && (braces == 0)) { + if (field == 3) { + logical_monitors = substr(value, start, position - start) + sub(/^[[:space:]]+/, "", logical_monitors) + sub(/[[:space:]]+$/, "", logical_monitors) + print logical_monitors + exit 0 + } + field++ + start = position + 1 + } + + if ((parentheses < 0) || (brackets < 0) || (braces < 0)) + exit 1 + } + + exit 1 + } + ' 2>/dev/null)"; then + return 3 + fi + + if [[ ! "$LOGICAL_MONITORS" =~ ^\[.*\]$ ]]; then + return 3 + fi + + # Select the primary logical monitor independently of scale conversion. + REMAINDER="${LOGICAL_MONITORS:1:${#LOGICAL_MONITORS}-2}" + if [[ "$REMAINDER" =~ ^[[:space:]]*$ ]]; then + return 4 + fi + + while [[ "$REMAINDER" =~ $LOGICAL_MONITOR_PATTERN ]]; do + FOUND_LOGICAL_MONITOR=1 + MATCHED_ENTRY="${BASH_REMATCH[0]}" + if [ "${BASH_REMATCH[2]}" = "true" ]; then + FOUND_PRIMARY_MONITOR=1 + PRIMARY_SCALE="${BASH_REMATCH[1]}" + break + fi + REMAINDER="${REMAINDER#*"$MATCHED_ENTRY"}" + done + + if [ "$FOUND_LOGICAL_MONITOR" -eq 0 ]; then + return 3 + fi + if [ "$FOUND_PRIMARY_MONITOR" -eq 0 ]; then + return 4 + fi + + # Convert the GVariant decimal string without locale-sensitive floating-point parsing. + if [[ ! "$PRIMARY_SCALE" =~ ^([0-9]+)([.]([0-9]+))?$ ]]; then + return 5 + fi + + SCALE_WHOLE="${BASH_REMATCH[1]}" + SCALE_FRACTION="${BASH_REMATCH[3]:-}" + while [[ "$SCALE_WHOLE" == 0* ]] && [ "$SCALE_WHOLE" != "0" ]; do + SCALE_WHOLE="${SCALE_WHOLE#0}" + done + if (( ${#SCALE_WHOLE} > 1 )); then + return 5 + fi + + SCALE_FRACTION_PADDED="${SCALE_FRACTION}000" + SCALE_HUNDREDTHS="${SCALE_FRACTION_PADDED:0:2}" + SCALE_ROUND_DIGIT="${SCALE_FRACTION_PADDED:2:1}" + SCALE_PERCENTAGE=$(( 10#$SCALE_WHOLE * 100 + 10#$SCALE_HUNDREDTHS )) + if (( 10#$SCALE_ROUND_DIGIT >= 5 )); then + SCALE_PERCENTAGE=$(( SCALE_PERCENTAGE + 1 )) + fi + + if (( SCALE_PERCENTAGE < 100 || SCALE_PERCENTAGE > 500 )); then + return 5 + fi + + printf '%s\n' "$SCALE_PERCENTAGE" + return 0 +} + # Name: 'waFixScale' -# Role: Validate the desktop scale and derive FreeRDP's nearest supported device scale. +# Role: Resolve RemoteApp and full-desktop scales and derive their device scales. function waFixScale() { - # Define variables. - local OLD_SCALE="$RDP_SCALE" + # Capture both raw settings before either is resolved. + local RAW_RDP_SCALE="$RDP_SCALE" + local RAW_RDP_DESKTOP_SCALE="$RDP_DESKTOP_SCALE" + local INVALID_SCALE_KEY="" + local INVALID_SCALE_VALUE="" + + # Validate both raw settings independently. + if [[ "$RAW_RDP_SCALE" != "auto" && ! "$RAW_RDP_SCALE" =~ ^[+-]?[0-9]+$ ]]; then + INVALID_SCALE_KEY="RDP_SCALE" + INVALID_SCALE_VALUE="$RAW_RDP_SCALE" + elif [[ "$RAW_RDP_DESKTOP_SCALE" != "auto" && "$RAW_RDP_DESKTOP_SCALE" != "inherit" && ! "$RAW_RDP_DESKTOP_SCALE" =~ ^[+-]?[0-9]+$ ]]; then + INVALID_SCALE_KEY="RDP_DESKTOP_SCALE" + INVALID_SCALE_VALUE="$RAW_RDP_DESKTOP_SCALE" + fi + + if [ -n "$INVALID_SCALE_KEY" ]; then + waThrowExit "$EC_INVALID_SCALE" "$INVALID_SCALE_KEY" "$INVALID_SCALE_VALUE" + fi + + # Resolve each scale and derive the nearest supported device scale. + local SCALE_KEY="" + local DEVICE_SCALE_KEY="" + local RAW_SCALE="" + local SCALE_VALUE="" + local SCALE_SOURCE="" + local DETECTED_SCALE="" + local DETECTION_STATUS=0 + local DETECTION_REASON="" + local DETECTION_ATTEMPTED=0 + local CACHED_DETECTED_SCALE="" + local CACHED_DETECTION_STATUS=0 + local DBUS_ENV_STATE="" + local OLD_SCALE="" + local SCALE_DIGITS="" + local SCALE_SIGN="" + local RESOLVED_SCALE=0 + local DEVICE_SCALE=0 local VALID_SCALE_1=100 local VALID_SCALE_2=140 local VALID_SCALE_3=180 + local DIFF_1=0 + local DIFF_2=0 + local DIFF_3=0 + + for SCALE_KEY in RDP_SCALE RDP_DESKTOP_SCALE; do + if [ "$SCALE_KEY" = "RDP_SCALE" ]; then + RAW_SCALE="$RAW_RDP_SCALE" + DEVICE_SCALE_KEY="RDP_DEVICE_SCALE" + else + RAW_SCALE="$RAW_RDP_DESKTOP_SCALE" + DEVICE_SCALE_KEY="RDP_DESKTOP_DEVICE_SCALE" + fi - # Reject values that cannot be passed to FreeRDP as integers. - if ! [[ "$RDP_SCALE" =~ ^[+-]?[0-9]+$ ]]; then - waThrowExit "$EC_INVALID_SCALE" - fi + if [ "$SCALE_KEY" = "RDP_DESKTOP_SCALE" ] && [ "$RAW_SCALE" = "inherit" ]; then + RDP_DESKTOP_SCALE="$RDP_SCALE" + printf -v RDP_DESKTOP_DEVICE_SCALE '%s' "$RDP_DEVICE_SCALE" + dprint "NOTICE: RDP_DESKTOP_SCALE inherited resolved RDP_SCALE value '${RDP_DESKTOP_SCALE}'." + continue + fi - # Strip leading zeros so the length-based overflow guard uses the normalised magnitude. - local SCALE_DIGITS="${RDP_SCALE#[+-]}" - local SCALE_SIGN="${RDP_SCALE%"$SCALE_DIGITS"}" - while [[ "$SCALE_DIGITS" == 0* ]] && [ "$SCALE_DIGITS" != "0" ]; do - SCALE_DIGITS="${SCALE_DIGITS#0}" - done + SCALE_VALUE="$RAW_SCALE" + SCALE_SOURCE="configuration" + if [ "$RAW_SCALE" = "auto" ]; then + DETECTED_SCALE="" + DETECTION_STATUS=0 + if [ "$DETECTION_ATTEMPTED" -eq 0 ]; then + DBUS_ENV_STATE="not set" + if [ -n "${DBUS_SESSION_BUS_ADDRESS:-}" ]; then + DBUS_ENV_STATE="set" + fi + dprint "NOTICE: DBUS_SESSION_BUS_ADDRESS is ${DBUS_ENV_STATE}; attempting host-scale detection through the session bus." - # Clamp the desktop scale to FreeRDP's supported range. - if [ "$SCALE_SIGN" = "-" ]; then - RDP_SCALE=100 - dprint "WARNING: RDP_SCALE value '${OLD_SCALE}' is below 100. Clamping desktop scale to '${RDP_SCALE}'." - elif (( ${#SCALE_DIGITS} > 3 )); then - RDP_SCALE=500 - dprint "WARNING: RDP_SCALE value '${OLD_SCALE}' is above 500. Clamping desktop scale to '${RDP_SCALE}'." - else - RDP_SCALE=$(( 10#$SCALE_DIGITS )) - if (( RDP_SCALE < 100 )); then - RDP_SCALE=100 - dprint "WARNING: RDP_SCALE value '${OLD_SCALE}' is below 100. Clamping desktop scale to '${RDP_SCALE}'." - elif (( RDP_SCALE > 500 )); then - RDP_SCALE=500 - dprint "WARNING: RDP_SCALE value '${OLD_SCALE}' is above 500. Clamping desktop scale to '${RDP_SCALE}'." + DETECTED_SCALE="$(waDetectHostScale)" || DETECTION_STATUS=$? + if [ "$DETECTION_STATUS" -eq 0 ] && [[ ! "$DETECTED_SCALE" =~ ^[+-]?[0-9]+$ ]]; then + DETECTION_STATUS=5 + fi + DETECTION_ATTEMPTED=1 + CACHED_DETECTED_SCALE="$DETECTED_SCALE" + CACHED_DETECTION_STATUS=$DETECTION_STATUS + else + DETECTED_SCALE="$CACHED_DETECTED_SCALE" + DETECTION_STATUS=$CACHED_DETECTION_STATUS + dprint "NOTICE: Reusing the host-scale detection result for ${SCALE_KEY}." + fi + + if [ "$DETECTION_STATUS" -eq 0 ]; then + SCALE_VALUE="$DETECTED_SCALE" + SCALE_SOURCE="detection" + else + case "$DETECTION_STATUS" in + 1) DETECTION_REASON="gdbus is unavailable" ;; + 2) DETECTION_REASON="the Cinnamon DisplayConfig call failed or timed out" ;; + 3) DETECTION_REASON="the Cinnamon DisplayConfig reply could not be parsed" ;; + 4) DETECTION_REASON="Cinnamon DisplayConfig reported no primary logical monitor" ;; + 5) DETECTION_REASON="the primary logical-monitor scale was invalid" ;; + *) DETECTION_REASON="host-scale detection returned unexpected status ${DETECTION_STATUS}" ;; + esac + SCALE_VALUE=100 + SCALE_SOURCE="fallback" + dprint "WARNING: ${SCALE_KEY} automatic detection failed because ${DETECTION_REASON}; falling back to 100." + fi fi - fi - # Find the supported device scale nearest to the desktop scale. - local DIFF_1=$(( RDP_SCALE > VALID_SCALE_1 ? RDP_SCALE - VALID_SCALE_1 : VALID_SCALE_1 - RDP_SCALE )) - local DIFF_2=$(( RDP_SCALE > VALID_SCALE_2 ? RDP_SCALE - VALID_SCALE_2 : VALID_SCALE_2 - RDP_SCALE )) - local DIFF_3=$(( RDP_SCALE > VALID_SCALE_3 ? RDP_SCALE - VALID_SCALE_3 : VALID_SCALE_3 - RDP_SCALE )) + OLD_SCALE="$SCALE_VALUE" + SCALE_DIGITS="${SCALE_VALUE#[+-]}" + SCALE_SIGN="${SCALE_VALUE%"$SCALE_DIGITS"}" + while [[ "$SCALE_DIGITS" == 0* ]] && [ "$SCALE_DIGITS" != "0" ]; do + SCALE_DIGITS="${SCALE_DIGITS#0}" + done - if (( DIFF_1 <= DIFF_2 && DIFF_1 <= DIFF_3 )); then - RDP_DEVICE_SCALE="$VALID_SCALE_1" - elif (( DIFF_2 <= DIFF_1 && DIFF_2 <= DIFF_3 )); then - RDP_DEVICE_SCALE="$VALID_SCALE_2" - else - RDP_DEVICE_SCALE="$VALID_SCALE_3" - fi + if [ "$SCALE_SIGN" = "-" ]; then + RESOLVED_SCALE=100 + dprint "WARNING: ${SCALE_KEY} value '${OLD_SCALE}' is below 100. Clamping desktop scale to '${RESOLVED_SCALE}'." + elif (( ${#SCALE_DIGITS} > 3 )); then + RESOLVED_SCALE=500 + dprint "WARNING: ${SCALE_KEY} value '${OLD_SCALE}' is above 500. Clamping desktop scale to '${RESOLVED_SCALE}'." + else + RESOLVED_SCALE=$(( 10#$SCALE_DIGITS )) + if (( RESOLVED_SCALE < 100 )); then + RESOLVED_SCALE=100 + dprint "WARNING: ${SCALE_KEY} value '${OLD_SCALE}' is below 100. Clamping desktop scale to '${RESOLVED_SCALE}'." + elif (( RESOLVED_SCALE > 500 )); then + RESOLVED_SCALE=500 + dprint "WARNING: ${SCALE_KEY} value '${OLD_SCALE}' is above 500. Clamping desktop scale to '${RESOLVED_SCALE}'." + fi + fi - if [ "$RDP_DEVICE_SCALE" != "$RDP_SCALE" ]; then - dprint "NOTICE: Approximating only the RDP device scale from '${RDP_SCALE}' to '${RDP_DEVICE_SCALE}'; desktop scale remains '${RDP_SCALE}'." - fi + DIFF_1=$(( RESOLVED_SCALE > VALID_SCALE_1 ? RESOLVED_SCALE - VALID_SCALE_1 : VALID_SCALE_1 - RESOLVED_SCALE )) + DIFF_2=$(( RESOLVED_SCALE > VALID_SCALE_2 ? RESOLVED_SCALE - VALID_SCALE_2 : VALID_SCALE_2 - RESOLVED_SCALE )) + DIFF_3=$(( RESOLVED_SCALE > VALID_SCALE_3 ? RESOLVED_SCALE - VALID_SCALE_3 : VALID_SCALE_3 - RESOLVED_SCALE )) + + if (( DIFF_1 <= DIFF_2 && DIFF_1 <= DIFF_3 )); then + DEVICE_SCALE="$VALID_SCALE_1" + elif (( DIFF_2 <= DIFF_1 && DIFF_2 <= DIFF_3 )); then + DEVICE_SCALE="$VALID_SCALE_2" + else + DEVICE_SCALE="$VALID_SCALE_3" + fi + + printf -v "$SCALE_KEY" '%s' "$RESOLVED_SCALE" + printf -v "$DEVICE_SCALE_KEY" '%s' "$DEVICE_SCALE" + dprint "NOTICE: ${SCALE_KEY} resolved from ${SCALE_SOURCE} to '${RESOLVED_SCALE}'." + + if [ "$DEVICE_SCALE" != "$RESOLVED_SCALE" ]; then + dprint "NOTICE: Approximating the device scale for ${SCALE_KEY} from '${RESOLVED_SCALE}' to '${DEVICE_SCALE}'; desktop scale remains '${RESOLVED_SCALE}'." + fi + done return 0 } @@ -405,7 +664,7 @@ function waLoadConfig() { RDP_PORT="${RDP_PORT:-3389}" - # Update $RDP_SCALE. + # Resolve RemoteApp and full-desktop scale settings. waFixScale # Update when $REMOVABLE_MEDIA is null waFixRemovableMedia @@ -842,8 +1101,8 @@ function waRunCommand() { /d:"$RDP_DOMAIN" \ /u:"$RDP_USER" \ ${RDP_PASSWORD_ARG:+"$RDP_PASSWORD_ARG"} \ - /scale-desktop:"$RDP_SCALE" \ - /scale-device:"$RDP_DEVICE_SCALE" \ + /scale-desktop:"$RDP_DESKTOP_SCALE" \ + /scale-device:"$RDP_DESKTOP_DEVICE_SCALE" \ +auto-reconnect \ +dynamic-resolution \ /wm-class:"Microsoft Windows" \ diff --git a/setup.sh b/setup.sh index b85a79df..23abe032 100755 --- a/setup.sh +++ b/setup.sh @@ -32,7 +32,7 @@ readonly EC_BAD_PORT="13" # Windows is unreachable via RDP_PORT. readonly EC_RDP_FAIL="14" # FreeRDP failed to establish a connection with Windows. readonly EC_APPQUERY_FAIL="15" # Failed to query Windows for installed applications. readonly EC_INVALID_FLAVOR="16" # Backend specified is not 'libvirt', 'docker' or 'podman'. -readonly EC_INVALID_SCALE="17" # RDP_SCALE is not an integer. +readonly EC_INVALID_SCALE="17" # A display scale setting is invalid. # PATHS # 'BIN' @@ -93,8 +93,11 @@ RDP_DOMAIN="" # Imported variable. RDP_IP="" # Imported variable. VM_NAME="RDPWindows" # Name of the Windows VM (FOR 'libvirt' ONLY). WAFLAVOR="docker" # Imported variable. -RDP_SCALE=100 # Imported variable. +RDP_SCALE="auto" # Imported variable for RemoteApp sessions. RDP_DEVICE_SCALE=100 # Derived from RDP_SCALE. +# RDP_DESKTOP_SCALE controls full-desktop sessions, not FreeRDP's /scale-desktop option. +RDP_DESKTOP_SCALE="inherit" +RDP_DESKTOP_DEVICE_SCALE=100 RDP_FLAGS="" # Imported variable. DEBUG="true" # Imported variable. FREERDP_COMMAND="" # Imported variable. @@ -502,62 +505,315 @@ function waCheckExistingInstall() { } +# Name: 'waDetectHostScale' +# Role: Detect the host's logical display scale. +function waDetectHostScale() { + local DISPLAY_STATE="" + local LOGICAL_MONITORS="" + local LOGICAL_MONITOR_PATTERN='\([[:space:]]*-?[0-9]+[[:space:]]*,[[:space:]]*-?[0-9]+[[:space:]]*,[[:space:]]*([^,[:space:]]*)[[:space:]]*,[[:space:]]*uint32[[:space:]]+[0-9]+[[:space:]]*,[[:space:]]*(true|false)[[:space:]]*,[[:space:]]*\[' + local REMAINDER="" + local MATCHED_ENTRY="" + local PRIMARY_SCALE="" + local SCALE_WHOLE="" + local SCALE_FRACTION="" + local SCALE_FRACTION_PADDED="" + local SCALE_HUNDREDTHS="" + local SCALE_ROUND_DIGIT="" + local SCALE_PERCENTAGE=0 + local FOUND_LOGICAL_MONITOR=0 + local FOUND_PRIMARY_MONITOR=0 + + if ! command -v gdbus >/dev/null 2>&1; then + return 1 + fi + + if ! DISPLAY_STATE="$(gdbus call --session --timeout 5 \ + --dest org.cinnamon.Muffin.DisplayConfig \ + --object-path /org/cinnamon/Muffin/DisplayConfig \ + --method org.cinnamon.Muffin.DisplayConfig.GetCurrentState 2>/dev/null)"; then + return 2 + fi + + if [ -z "$DISPLAY_STATE" ]; then + return 3 + fi + + # Extract the third top-level tuple field while respecting nested GVariant containers. + if ! LOGICAL_MONITORS="$(printf '%s\n' "$DISPLAY_STATE" | LC_ALL=C awk ' + { + value = $0 + if ((substr(value, 1, 1) != "(") || (substr(value, length(value), 1) != ")")) + exit 1 + + parentheses = 0 + brackets = 0 + braces = 0 + quoted = 0 + escaped = 0 + field = 1 + start = 2 + + for (position = 2; position < length(value); position++) { + character = substr(value, position, 1) + + if (quoted) { + if (escaped) + escaped = 0 + else if (character == "\\") + escaped = 1 + else if (character == "\047") + quoted = 0 + continue + } + + if (character == "\047") { + quoted = 1 + continue + } + + if (character == "(") + parentheses++ + else if (character == ")") + parentheses-- + else if (character == "[") + brackets++ + else if (character == "]") + brackets-- + else if (character == "{") + braces++ + else if (character == "}") + braces-- + else if ((character == ",") && (parentheses == 0) && (brackets == 0) && (braces == 0)) { + if (field == 3) { + logical_monitors = substr(value, start, position - start) + sub(/^[[:space:]]+/, "", logical_monitors) + sub(/[[:space:]]+$/, "", logical_monitors) + print logical_monitors + exit 0 + } + field++ + start = position + 1 + } + + if ((parentheses < 0) || (brackets < 0) || (braces < 0)) + exit 1 + } + + exit 1 + } + ' 2>/dev/null)"; then + return 3 + fi + + if [[ ! "$LOGICAL_MONITORS" =~ ^\[.*\]$ ]]; then + return 3 + fi + + # Select the primary logical monitor independently of scale conversion. + REMAINDER="${LOGICAL_MONITORS:1:${#LOGICAL_MONITORS}-2}" + if [[ "$REMAINDER" =~ ^[[:space:]]*$ ]]; then + return 4 + fi + + while [[ "$REMAINDER" =~ $LOGICAL_MONITOR_PATTERN ]]; do + FOUND_LOGICAL_MONITOR=1 + MATCHED_ENTRY="${BASH_REMATCH[0]}" + if [ "${BASH_REMATCH[2]}" = "true" ]; then + FOUND_PRIMARY_MONITOR=1 + PRIMARY_SCALE="${BASH_REMATCH[1]}" + break + fi + REMAINDER="${REMAINDER#*"$MATCHED_ENTRY"}" + done + + if [ "$FOUND_LOGICAL_MONITOR" -eq 0 ]; then + return 3 + fi + if [ "$FOUND_PRIMARY_MONITOR" -eq 0 ]; then + return 4 + fi + + # Convert the GVariant decimal string without locale-sensitive floating-point parsing. + if [[ ! "$PRIMARY_SCALE" =~ ^([0-9]+)([.]([0-9]+))?$ ]]; then + return 5 + fi + + SCALE_WHOLE="${BASH_REMATCH[1]}" + SCALE_FRACTION="${BASH_REMATCH[3]:-}" + while [[ "$SCALE_WHOLE" == 0* ]] && [ "$SCALE_WHOLE" != "0" ]; do + SCALE_WHOLE="${SCALE_WHOLE#0}" + done + if (( ${#SCALE_WHOLE} > 1 )); then + return 5 + fi + + SCALE_FRACTION_PADDED="${SCALE_FRACTION}000" + SCALE_HUNDREDTHS="${SCALE_FRACTION_PADDED:0:2}" + SCALE_ROUND_DIGIT="${SCALE_FRACTION_PADDED:2:1}" + SCALE_PERCENTAGE=$(( 10#$SCALE_WHOLE * 100 + 10#$SCALE_HUNDREDTHS )) + if (( 10#$SCALE_ROUND_DIGIT >= 5 )); then + SCALE_PERCENTAGE=$(( SCALE_PERCENTAGE + 1 )) + fi + + if (( SCALE_PERCENTAGE < 100 || SCALE_PERCENTAGE > 500 )); then + return 5 + fi + + printf '%s\n' "$SCALE_PERCENTAGE" + return 0 +} + # Name: 'waFixScale' -# Role: Validate the desktop scale and derive FreeRDP's nearest supported device scale. +# Role: Resolve RemoteApp and full-desktop scales and derive their device scales. function waFixScale() { - # Define variables. - local OLD_SCALE="$RDP_SCALE" + # Capture both raw settings before either is resolved. + local RAW_RDP_SCALE="$RDP_SCALE" + local RAW_RDP_DESKTOP_SCALE="$RDP_DESKTOP_SCALE" + local INVALID_SCALE_KEY="" + local INVALID_SCALE_VALUE="" + + # Validate both raw settings independently. + if [[ "$RAW_RDP_SCALE" != "auto" && ! "$RAW_RDP_SCALE" =~ ^[+-]?[0-9]+$ ]]; then + INVALID_SCALE_KEY="RDP_SCALE" + INVALID_SCALE_VALUE="$RAW_RDP_SCALE" + elif [[ "$RAW_RDP_DESKTOP_SCALE" != "auto" && "$RAW_RDP_DESKTOP_SCALE" != "inherit" && ! "$RAW_RDP_DESKTOP_SCALE" =~ ^[+-]?[0-9]+$ ]]; then + INVALID_SCALE_KEY="RDP_DESKTOP_SCALE" + INVALID_SCALE_VALUE="$RAW_RDP_DESKTOP_SCALE" + fi + + if [ -n "$INVALID_SCALE_KEY" ]; then + echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}${INVALID_SCALE_KEY} must be 'auto' or an integer; received '${INVALID_SCALE_VALUE}'.${CLEAR_TEXT}" + return "$EC_INVALID_SCALE" + fi + + # Resolve each scale and derive the nearest supported device scale. + local SCALE_KEY="" + local DEVICE_SCALE_KEY="" + local RAW_SCALE="" + local SCALE_VALUE="" + local SCALE_SOURCE="" + local DETECTED_SCALE="" + local DETECTION_STATUS=0 + local DETECTION_REASON="" + local DETECTION_ATTEMPTED=0 + local CACHED_DETECTED_SCALE="" + local CACHED_DETECTION_STATUS=0 + local DBUS_ENV_STATE="" + local OLD_SCALE="" + local SCALE_DIGITS="" + local SCALE_SIGN="" + local RESOLVED_SCALE=0 + local DEVICE_SCALE=0 local VALID_SCALE_1=100 local VALID_SCALE_2=140 local VALID_SCALE_3=180 + local DIFF_1=0 + local DIFF_2=0 + local DIFF_3=0 + + for SCALE_KEY in RDP_SCALE RDP_DESKTOP_SCALE; do + if [ "$SCALE_KEY" = "RDP_SCALE" ]; then + RAW_SCALE="$RAW_RDP_SCALE" + DEVICE_SCALE_KEY="RDP_DEVICE_SCALE" + else + RAW_SCALE="$RAW_RDP_DESKTOP_SCALE" + DEVICE_SCALE_KEY="RDP_DESKTOP_DEVICE_SCALE" + fi - # Reject values that cannot be passed to FreeRDP as integers. - if ! [[ "$RDP_SCALE" =~ ^[+-]?[0-9]+$ ]]; then - echo -e "${ERROR_TEXT}ERROR:${CLEAR_TEXT} ${BOLD_TEXT}RDP_SCALE must be an integer; received '${RDP_SCALE}'.${CLEAR_TEXT}" - return "$EC_INVALID_SCALE" - fi + if [ "$SCALE_KEY" = "RDP_DESKTOP_SCALE" ] && [ "$RAW_SCALE" = "inherit" ]; then + RDP_DESKTOP_SCALE="$RDP_SCALE" + printf -v RDP_DESKTOP_DEVICE_SCALE '%s' "$RDP_DEVICE_SCALE" + echo -e "${INFO_TEXT}RDP_DESKTOP_SCALE inherited resolved RDP_SCALE value '${RDP_DESKTOP_SCALE}'.${CLEAR_TEXT}" + continue + fi - # Strip leading zeros so the length-based overflow guard uses the normalised magnitude. - local SCALE_DIGITS="${RDP_SCALE#[+-]}" - local SCALE_SIGN="${RDP_SCALE%"$SCALE_DIGITS"}" - while [[ "$SCALE_DIGITS" == 0* ]] && [ "$SCALE_DIGITS" != "0" ]; do - SCALE_DIGITS="${SCALE_DIGITS#0}" - done + SCALE_VALUE="$RAW_SCALE" + SCALE_SOURCE="configuration" + if [ "$RAW_SCALE" = "auto" ]; then + DETECTED_SCALE="" + DETECTION_STATUS=0 + if [ "$DETECTION_ATTEMPTED" -eq 0 ]; then + DBUS_ENV_STATE="not set" + if [ -n "${DBUS_SESSION_BUS_ADDRESS:-}" ]; then + DBUS_ENV_STATE="set" + fi + echo -e "${INFO_TEXT}DBUS_SESSION_BUS_ADDRESS is ${DBUS_ENV_STATE}; attempting host-scale detection through the session bus.${CLEAR_TEXT}" - # Clamp the desktop scale to FreeRDP's supported range. - if [ "$SCALE_SIGN" = "-" ]; then - RDP_SCALE=100 - echo -e "${WARNING_TEXT}[WARNING]${CLEAR_TEXT} RDP_SCALE value '${OLD_SCALE}' is below 100. Clamping desktop scale to '${RDP_SCALE}'." - elif (( ${#SCALE_DIGITS} > 3 )); then - RDP_SCALE=500 - echo -e "${WARNING_TEXT}[WARNING]${CLEAR_TEXT} RDP_SCALE value '${OLD_SCALE}' is above 500. Clamping desktop scale to '${RDP_SCALE}'." - else - RDP_SCALE=$(( 10#$SCALE_DIGITS )) - if (( RDP_SCALE < 100 )); then - RDP_SCALE=100 - echo -e "${WARNING_TEXT}[WARNING]${CLEAR_TEXT} RDP_SCALE value '${OLD_SCALE}' is below 100. Clamping desktop scale to '${RDP_SCALE}'." - elif (( RDP_SCALE > 500 )); then - RDP_SCALE=500 - echo -e "${WARNING_TEXT}[WARNING]${CLEAR_TEXT} RDP_SCALE value '${OLD_SCALE}' is above 500. Clamping desktop scale to '${RDP_SCALE}'." + DETECTED_SCALE="$(waDetectHostScale)" || DETECTION_STATUS=$? + if [ "$DETECTION_STATUS" -eq 0 ] && [[ ! "$DETECTED_SCALE" =~ ^[+-]?[0-9]+$ ]]; then + DETECTION_STATUS=5 + fi + DETECTION_ATTEMPTED=1 + CACHED_DETECTED_SCALE="$DETECTED_SCALE" + CACHED_DETECTION_STATUS=$DETECTION_STATUS + else + DETECTED_SCALE="$CACHED_DETECTED_SCALE" + DETECTION_STATUS=$CACHED_DETECTION_STATUS + echo -e "${INFO_TEXT}Reusing the host-scale detection result for ${SCALE_KEY}.${CLEAR_TEXT}" + fi + + if [ "$DETECTION_STATUS" -eq 0 ]; then + SCALE_VALUE="$DETECTED_SCALE" + SCALE_SOURCE="detection" + else + case "$DETECTION_STATUS" in + 1) DETECTION_REASON="gdbus is unavailable" ;; + 2) DETECTION_REASON="the Cinnamon DisplayConfig call failed or timed out" ;; + 3) DETECTION_REASON="the Cinnamon DisplayConfig reply could not be parsed" ;; + 4) DETECTION_REASON="Cinnamon DisplayConfig reported no primary logical monitor" ;; + 5) DETECTION_REASON="the primary logical-monitor scale was invalid" ;; + *) DETECTION_REASON="host-scale detection returned unexpected status ${DETECTION_STATUS}" ;; + esac + SCALE_VALUE=100 + SCALE_SOURCE="fallback" + echo -e "${WARNING_TEXT}[WARNING]${CLEAR_TEXT} ${SCALE_KEY} automatic detection failed because ${DETECTION_REASON}; falling back to 100." + fi fi - fi - # Find the supported device scale nearest to the desktop scale. - local DIFF_1=$(( RDP_SCALE > VALID_SCALE_1 ? RDP_SCALE - VALID_SCALE_1 : VALID_SCALE_1 - RDP_SCALE )) - local DIFF_2=$(( RDP_SCALE > VALID_SCALE_2 ? RDP_SCALE - VALID_SCALE_2 : VALID_SCALE_2 - RDP_SCALE )) - local DIFF_3=$(( RDP_SCALE > VALID_SCALE_3 ? RDP_SCALE - VALID_SCALE_3 : VALID_SCALE_3 - RDP_SCALE )) + OLD_SCALE="$SCALE_VALUE" + SCALE_DIGITS="${SCALE_VALUE#[+-]}" + SCALE_SIGN="${SCALE_VALUE%"$SCALE_DIGITS"}" + while [[ "$SCALE_DIGITS" == 0* ]] && [ "$SCALE_DIGITS" != "0" ]; do + SCALE_DIGITS="${SCALE_DIGITS#0}" + done - if (( DIFF_1 <= DIFF_2 && DIFF_1 <= DIFF_3 )); then - RDP_DEVICE_SCALE="$VALID_SCALE_1" - elif (( DIFF_2 <= DIFF_1 && DIFF_2 <= DIFF_3 )); then - RDP_DEVICE_SCALE="$VALID_SCALE_2" - else - RDP_DEVICE_SCALE="$VALID_SCALE_3" - fi + if [ "$SCALE_SIGN" = "-" ]; then + RESOLVED_SCALE=100 + echo -e "${WARNING_TEXT}[WARNING]${CLEAR_TEXT} ${SCALE_KEY} value '${OLD_SCALE}' is below 100. Clamping desktop scale to '${RESOLVED_SCALE}'." + elif (( ${#SCALE_DIGITS} > 3 )); then + RESOLVED_SCALE=500 + echo -e "${WARNING_TEXT}[WARNING]${CLEAR_TEXT} ${SCALE_KEY} value '${OLD_SCALE}' is above 500. Clamping desktop scale to '${RESOLVED_SCALE}'." + else + RESOLVED_SCALE=$(( 10#$SCALE_DIGITS )) + if (( RESOLVED_SCALE < 100 )); then + RESOLVED_SCALE=100 + echo -e "${WARNING_TEXT}[WARNING]${CLEAR_TEXT} ${SCALE_KEY} value '${OLD_SCALE}' is below 100. Clamping desktop scale to '${RESOLVED_SCALE}'." + elif (( RESOLVED_SCALE > 500 )); then + RESOLVED_SCALE=500 + echo -e "${WARNING_TEXT}[WARNING]${CLEAR_TEXT} ${SCALE_KEY} value '${OLD_SCALE}' is above 500. Clamping desktop scale to '${RESOLVED_SCALE}'." + fi + fi - if [ "$RDP_DEVICE_SCALE" != "$RDP_SCALE" ]; then - echo -e "${INFO_TEXT}Approximating only the RDP device scale from '${RDP_SCALE}' to '${RDP_DEVICE_SCALE}'; desktop scale remains '${RDP_SCALE}'.${CLEAR_TEXT}" - fi + DIFF_1=$(( RESOLVED_SCALE > VALID_SCALE_1 ? RESOLVED_SCALE - VALID_SCALE_1 : VALID_SCALE_1 - RESOLVED_SCALE )) + DIFF_2=$(( RESOLVED_SCALE > VALID_SCALE_2 ? RESOLVED_SCALE - VALID_SCALE_2 : VALID_SCALE_2 - RESOLVED_SCALE )) + DIFF_3=$(( RESOLVED_SCALE > VALID_SCALE_3 ? RESOLVED_SCALE - VALID_SCALE_3 : VALID_SCALE_3 - RESOLVED_SCALE )) + + if (( DIFF_1 <= DIFF_2 && DIFF_1 <= DIFF_3 )); then + DEVICE_SCALE="$VALID_SCALE_1" + elif (( DIFF_2 <= DIFF_1 && DIFF_2 <= DIFF_3 )); then + DEVICE_SCALE="$VALID_SCALE_2" + else + DEVICE_SCALE="$VALID_SCALE_3" + fi + + printf -v "$SCALE_KEY" '%s' "$RESOLVED_SCALE" + printf -v "$DEVICE_SCALE_KEY" '%s' "$DEVICE_SCALE" + echo -e "${INFO_TEXT}${SCALE_KEY} resolved from ${SCALE_SOURCE} to '${RESOLVED_SCALE}'.${CLEAR_TEXT}" + + if [ "$DEVICE_SCALE" != "$RESOLVED_SCALE" ]; then + echo -e "${INFO_TEXT}Approximating the device scale for ${SCALE_KEY} from '${RESOLVED_SCALE}' to '${DEVICE_SCALE}'; desktop scale remains '${RESOLVED_SCALE}'.${CLEAR_TEXT}" + fi + done return 0 } @@ -1754,7 +2010,7 @@ function waInstall() { # Check for missing dependencies. waCheckInstallDependencies - # Update $RDP_SCALE. + # Resolve RemoteApp and full-desktop scale settings. waFixScale || return "$?" # Append additional FreeRDP flags if required. @@ -1951,7 +2207,7 @@ function waAddApps() { # Check for missing dependencies. waCheckInstallDependencies - # Update $RDP_SCALE. + # Resolve RemoteApp and full-desktop scale settings. waFixScale || return "$?" # Append additional FreeRDP flags if required.