Arch Omarchy Hyprland 使用 WinApps 连接局域网 Windows 应用
在 Linux 桌面中启动局域网 Windows Professional 上的应用
背景
我现在的主力桌面是 Arch Linux + Omarchy + Hyprland,但是局域网里面还有一台 Windows Professional 设备,有些应用还是只能在 Windows 里面跑。
我的目标不是用 Wine 在 Linux 本地运行 Windows 程序,而是希望在 Linux 桌面上像启动本地应用一样打开 Windows 机器里的程序窗口。
这次用到的是这个项目:
WinApps 的核心思路是:
Linux 桌面入口 / .desktop
↓
WinApps 脚本
↓
FreeRDP
↓
局域网 Windows Professional 的 RDP / RemoteApp
↓
Windows 应用窗口显示在 Linux 桌面上
所以它本质上是远程桌面/RemoteApp 方案,不是 Wine,也不是把 exe 复制到 Linux 上执行。
当前环境
这篇文章记录的是我当前设备上实际跑通的配置。其他人想 1:1 复现,环境差异越小越好。
Linux 设备
发行版: Arch Linux rolling
内核: Linux omarchy 7.0.9-arch2-1
桌面环境: Omarchy
窗口管理器: Hyprland
Hyprland 版本: 0.55.2
会话类型: Wayland
XDG_SESSION_TYPE=wayland
XDG_CURRENT_DESKTOP=Hyprland
DESKTOP_SESSION=omarchy
WAYLAND_DISPLAY=wayland-1
DISPLAY=:0
Hyprland 版本信息:
Hyprland 0.55.2
commit: 39d7e209c79d451efab1b21151d5938289da838d
Windows 设备
系统版本: Windows Professional
网络位置: 同一局域网
局域网 IP: 192.168.6.105
RDP 端口: 3389
注意:Windows Home 版不适合这个方案,因为它默认不提供完整的 Remote Desktop Host 能力。Windows Professional 是符合要求的。
WinApps / FreeRDP
WinApps 安装方式: user install
WinApps backend: manual
FreeRDP: 3.26.0
FreeRDP 路径: /home/linuxbrew/.linuxbrew/bin/xfreerdp
netcat: GNU netcat 0.7.1
netcat 路径: /home/linuxbrew/.linuxbrew/bin/nc
dialog: 1.3-20260107
我这里因为 sudo pacman 需要交互密码,所以最后使用 Linuxbrew 安装缺失依赖。
依赖安装
WinApps README 里面给 Arch 的依赖安装方式大概是:
sudo pacman -Syu --needed curl dialog freerdp git iproute2 libnotify openbsd-netcat
我当前机器上系统已经有:
curl 8.20.0-7
git 2.54.0-1
iproute2 7.0.0-1
libnotify 0.8.8-1
缺的是:
dialog
freerdp
openbsd-netcat / netcat
因为不想在这次操作里处理 sudo 交互,所以我用了 Homebrew:
brew install freerdp netcat dialog
安装后验证:
xfreerdp --version
# This is FreeRDP version 3.26.0
nc -h
# GNU netcat 0.7.1
dialog --version
# Version: 1.3-20260107
Windows 侧准备
Windows 机器需要先做这些事:
- 确认系统是 Windows Professional。
- 打开「远程桌面」。
- 防火墙允许 Remote Desktop / TCP 3389。
- 登录账号必须有真实密码,不能只用 Windows Hello PIN。
- 运行 WinApps 的 Windows 侧注册表和脚本。
我从 WinApps 仓库里准备了这些文件:
RDPApps.reg
install.bat
TimeSync.ps1
NetProfileCleanup.ps1
放在 Linux 本地的目录是:
~/Downloads/winapps-windows-setup/
然后把这个目录复制到 Windows 机器上,在 Windows 里右键:
install.bat → 以管理员身份运行
运行完成后重启 Windows。
注意:manual / 局域网已有 Windows 机器的方式不需要 Container.reg,那个是 Docker / Podman 后端用的。
Linux 侧安装 WinApps
我把项目克隆到了:
~/src/winapps
也就是:
git clone https://github.com/winapps-org/winapps.git ~/src/winapps
配置目录:
mkdir -p ~/.config/winapps
我没有把密码直接写在 winapps.conf 里,而是使用 RDP_ASKPASS 从单独文件读取密码。
密码文件:
~/.config/winapps/rdp-password
权限:
chmod 600 ~/.config/winapps/rdp-password
最终可用的 WinApps 配置
配置文件路径:
~/.config/winapps/winapps.conf
我的最终配置如下,用户名和密码请换成自己的:
RDP_USER="你的 Windows 用户名"
RDP_PASS=""
RDP_ASKPASS="bash -c 'cat ~/.config/winapps/rdp-password'"
RDP_DOMAIN=""
RDP_IP="192.168.6.105"
RDP_PORT="3389"
WAFLAVOR="manual"
RDP_SCALE="100"
RDP_FLAGS="/cert:tofu /sound /microphone +home-drive /kbd:remap:0x1d=0x38,remap:0x38=0x1d"
RDP_FLAGS_NON_WINDOWS=""
RDP_FLAGS_WINDOWS=""
DEBUG="true"
AUTOPAUSE="off"
AUTOPAUSE_TIME="300"
FREERDP_COMMAND="/home/linuxbrew/.linuxbrew/bin/xfreerdp"
PORT_TIMEOUT="5"
RDP_TIMEOUT="30"
APP_SCAN_TIMEOUT="60"
BOOT_TIMEOUT="120"
HIDEF="on"
REMOVABLE_MEDIA="/run/media"
配置文件权限:
chmod 600 ~/.config/winapps/winapps.conf
这里几个关键点:
WAFLAVOR="manual":表示使用已有的局域网 Windows RDP server。RDP_IP="192.168.6.105":这是我的 Windows 机器 IP。FREERDP_COMMAND强制指定xfreerdp,原因后面会说。+home-drive:把 Linux 的 home 目录映射到 Windows 里的\\tsclient\home。/kbd:remap:0x1d=0x38,remap:0x38=0x1d:用于处理我在 Omarchy 里交换左 Ctrl / 左 Alt 的问题。
安装和启动
先测试 RDP 端口:
nc -z -w 5 192.168.6.105 3389
如果没有报错,说明端口能通。
测试完整 RDP:
xfreerdp /u:"你的 Windows 用户名" /v:192.168.6.105:3389 /cert:tofu
然后运行 WinApps 安装:
cd ~/src/winapps
bash ./setup.sh --user
安装成功后可以启动完整 Windows 桌面:
winapps windows
启动资源管理器:
winapps explorer
手动启动任意 exe:
winapps manual "C:\Windows\System32\notepad.exe"
问题 1:EXISTING 'USER' WINAPPS INSTALLATION
现象
执行:
winapps-install-user
出现:
ERROR: EXISTING 'USER' WINAPPS INSTALLATION.
A previous WinApps installation was detected for the current user.
Please remove the existing WinApps installation using winapps-setup --user --uninstall.
原因
WinApps 安装器会检查这些路径:
~/.local/bin/winapps
~/.local/bin/winapps-src
我一开始为了方便,提前创建了:
~/.local/bin/winapps
~/.local/bin/winapps-setup
这两个 symlink 触发了安装器的已有安装检测。
解决办法
删除提前创建的 symlink 和失败安装留下的源码目录:
rm ~/.local/bin/winapps
rm ~/.local/bin/winapps-setup
rm -rf ~/.local/bin/winapps-src
然后重新安装:
cd ~/src/winapps
bash ./setup.sh --user
问题 2:RDP 端口明明开着,但安装器报 NETWORK CONFIGURATION ERROR
现象
独立测试端口是通的:
nc -z -w 5 192.168.6.105 3389
# OK
但 WinApps 安装器里报:
ERROR: NETWORK CONFIGURATION ERROR.
Failed to establish a connection with Windows at '192.168.6.105:3389'.
原因
WinApps 里面的检查逻辑是:
timeout "$PORT_TIMEOUT" nc -z "$RDP_IP" "$RDP_PORT"
我这里用的是 Homebrew 安装的 GNU netcat:
GNU netcat 0.7.1
这个版本在执行:
nc -z 192.168.6.105 3389
时可能会一直等待远端关闭连接,导致外层 timeout 判定失败。
但是加上 -w 就正常:
nc -z -w 5 192.168.6.105 3389
解决办法
我做了一个 WinApps 专用 nc 兼容包装器:
mkdir -p ~/.local/lib/winapps-compat
cat > ~/.local/lib/winapps-compat/nc <<'EOS'
#!/usr/bin/env bash
real_nc="/home/linuxbrew/.linuxbrew/bin/nc"
if "$; then
shift
if "${1:-}" == "-w" && "$; then
shift 2
elif "${1:-}" == -w* && "$; then
shift
fi
host="${1:-}"
port="${2:-}"
if -n "$host" && -n "$port" ; then
exec bash -c 'exec 3<>"/dev/tcp/$1/$2"' _ "$host" "$port"
fi
fi
exec "$real_nc" "$@"
EOS
chmod +x ~/.local/lib/winapps-compat/nc
然后把已安装 WinApps 副本里的端口检查从:
timeout 10 nc -z "$RDP_IP" "$RDP_PORT"
改成:
timeout 10 "$HOME/.local/lib/winapps-compat/nc" -z "$RDP_IP" "$RDP_PORT"
对应文件:
~/.local/bin/winapps-src/bin/winapps
~/.local/bin/winapps-src/setup.sh
注意:这个是针对我当前 Homebrew GNU netcat 的兼容修复。如果你直接用 Arch 的 openbsd-netcat,可能不需要这个 patch。
问题 3:Backspace 在 Windows 窗口里不生效
现象
Windows 窗口可以打开,但是部分键盘输入不正常,例如 Backspace 没反应。
原因
我一开始在 RDP_FLAGS 里加过:
/kbd:unicode
这个参数对中文/非英文输入可能有帮助,但在我的 FreeRDP + Hyprland + WinApps 组合里,它会影响 Backspace 这类控制键。
解决办法
从 RDP_FLAGS 里移除:
/kbd:unicode
也就是不要写成:
RDP_FLAGS="/cert:tofu /sound /microphone +home-drive /kbd:unicode"
而是先保持:
RDP_FLAGS="/cert:tofu /sound /microphone +home-drive"
修改后杀掉旧的 RDP 会话:
winapps killrdp
再重新启动:
winapps windows
问题 4:Omarchy 里交换了 Alt/Ctrl,但 Windows 窗口里不生效
现象
我的 Omarchy / Hyprland 配置里有:
kb_layout = us
kb_options = compose:caps,ctrl:swap_lalt_lctl
也就是本机把左 Alt 和左 Ctrl 交换了。
但是在 WinApps 打开的 Windows 窗口里,这个交换没有按预期生效。
原因
Hyprland 的 kb_options 是本地 XKB 层的映射,而 FreeRDP 会把键盘事件转换成 RDP scancode / Windows 虚拟键再传给远端 Windows。
所以本机的 XKB modifier 交换不一定会完整传递到 Windows。
FreeRDP 里左 Ctrl / 左 Alt 对应的 scancode 是:
0x1d -> Left Ctrl
0x38 -> Left Alt
解决办法
在 FreeRDP 层再做一次 remap:
/kbd:remap:0x1d=0x38,remap:0x38=0x1d
最终写进 RDP_FLAGS:
RDP_FLAGS="/cert:tofu /sound /microphone +home-drive /kbd:remap:0x1d=0x38,remap:0x38=0x1d"
注意这里要使用短 scancode:
0x1d
0x38
不要写成长 scancode:
0x001d
0x0038
后面这个会导致 FreeRDP 参数解析失败。
问题 5:只弹出 Using default removeable media path: /run/media,但窗口不出现
现象
启动:
winapps windows
没有打开窗口,只看到一条通知:
Using default removeable media path: /run/media
原因
这条通知本身不是错误,它只是 WinApps 告诉你默认使用了:
/run/media
真正的问题在后面。
我当时有两个叠加问题。
第一个是我把 FREERDP_COMMAND 清空了:
FREERDP_COMMAND=""
WinApps 在 Wayland 下会自动优先选择:
sdl-freerdp
但是在我的环境里,sdl-freerdp 启动后马上退出,没有正常显示窗口。
第二个是键盘 remap 写成了 FreeRDP 不接受的形式:
/kbd:remap:0x001d=0x0038,remap:0x0038=0x001d
FreeRDP 报错:
Command line parsing failed at 'kbd'
解决办法
强制使用已验证可用的 xfreerdp:
FREERDP_COMMAND="/home/linuxbrew/.linuxbrew/bin/xfreerdp"
并把 remap 改成短 scancode:
RDP_FLAGS="/cert:tofu /sound /microphone +home-drive /kbd:remap:0x1d=0x38,remap:0x38=0x1d"
为了不再看到默认 removable media 的提示,也可以显式写上:
REMOVABLE_MEDIA="/run/media"
附录:Windows 侧脚本、SCP 传输命令和本机辅助脚本
前面主流程已经能跑通。为了方便别人完全复现,这里把本次实际用到但前文没有完整展开的脚本和配置集中贴出来。所有密码、用户名都保留为占位符或示例,不要把自己的真实密码写进博客或仓库。
用 SCP 把 Windows 侧配置包传到 Windows
我的 Linux 主机局域网 IP 是:
192.168.6.184
Windows 主机局域网 IP 是:
192.168.6.105
方式 1:在 Windows PowerShell 里从 Linux 拉取
这个方式更符合“把 Linux 上的目录下载到 Windows 中”。前提是 Linux 机器开启了 SSH 服务,并且 Windows 自带的 OpenSSH Client 可用。
在 Windows PowerShell 中执行:
scp -r gavin@192.168.6.184:/home/gavin/Downloads/winapps-windows-setup "$env:USERPROFILE\Downloads"
拉取完成后,在 Windows 里进入:
cd "$env:USERPROFILE\Downloads\winapps-windows-setup"
然后右键 install.bat,选择“以管理员身份运行”。
方式 2:在 Linux 里推送到 Windows
这个方式要求 Windows 启用 OpenSSH Server,并允许 SSH 登录。
scp -r ~/Downloads/winapps-windows-setup "WindowsUser@192.168.6.105:C:/Users/WindowsUser/Downloads/"
把 WindowsUser 换成你的 Windows 用户名。如果使用微软账号登录 Windows,OpenSSH 用户名可能和显示名不完全一致,需要以 Windows 实际 SSH 登录用户名为准。
Windows 侧配置包内容
本次目录是:
~/Downloads/winapps-windows-setup/
包含:
README.txt
install.bat
RDPApps.reg
NetProfileCleanup.ps1
TimeSync.ps1
~/Downloads/winapps-windows-setup/README.txt
WinApps Windows-side setup for an existing Windows Professional LAN machine
1. Enable Remote Desktop in Windows Settings.
2. Ensure Windows Firewall allows Remote Desktop TCP 3389.
3. Use a real Windows account password; RDP does not accept Windows Hello PIN-only login.
4. Copy this folder to the Windows machine.
5. Right-click install.bat and choose "Run as administrator".
6. Reboot Windows after the script completes.
7. On Arch, run: winapps-configure-local
8. Then run: winapps-test-rdp
9. If the full RDP session works, run: winapps-install-user
Do not use Container.reg for this LAN/manual setup.
~/Downloads/winapps-windows-setup/install.bat
@echo off
title WinApps Setup Wizard
:: Check for administrative privileges
fltmc >nul 2>&1 || (
echo [INFO] Script not running as administrator. Attempting to relaunch with elevation...
powershell -Command "Start-Process '%~f0' -Verb runAs"
exit /b
)
echo ============================================
echo WinApps Setup Wizard
echo ============================================
echo.
echo [INFO] Starting setup...
:: Apply RDP and system configuration tweaks
echo [INFO] Importing "RDPApps.reg"...
if exist "%~dp0RDPApps.reg" (
reg import "%~dp0RDPApps.reg" >nul 2>&1
if %ERRORLEVEL% equ 0 (
echo [SUCCESS] Imported "RDPApps.reg".
) else (
echo [ERROR] Failed to import "RDPApps.reg".
)
) else (
echo [ERROR] "RDPApps.reg" not found. Skipping...
)
:: Allow Remote Desktop connections through the firewall
echo [INFO] Allowing Remote Desktop connections through the firewall...
powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass ^
-Command "if (Get-Command Enable-NetFirewallRule -ErrorAction SilentlyContinue) { try { Enable-NetFirewallRule -DisplayGroup 'Remote Desktop' -ErrorAction Stop; exit 0 } catch { exit 1 } } else { exit 2 }" >nul 2>&1
if %ERRORLEVEL% equ 0 (
echo [SUCCESS] Firewall changes applied successfully.
) else (
:: Fallback to using 'netsh' to make the firewall modification
netsh advfirewall firewall set rule group="remote desktop" new enable=Yes >nul 2>&1
if %ERRORLEVEL% equ 0 (
echo [SUCCESS] Firewall changes applied successfully.
) else (
echo [ERROR] Failed to apply firewall changes.
echo Please manually enable Remote Desktop via 'Settings --> System --> Remote Desktop'.
)
)
:: Configure the system clock to use UTC instead of local time
if exist "%~dp0Container.reg" (
echo [INFO] Importing "Container.reg"...
reg import "%~dp0Container.reg" >nul 2>&1
if %ERRORLEVEL% equ 0 (
echo [SUCCESS] Imported "Container.reg".
) else (
echo [ERROR] Failed to import "Container.reg".
)
) else (
echo [WARNING] "Container.reg" not found. Skipping...
)
:: Create a startup task to clean up stale network profiles
echo [INFO] Creating network profile cleanup task...
:: Initialise values required to create the startup task
set "scriptpath=%windir%\NetProfileCleanup.ps1"
set "taskname=WinApps_NetworkProfileCleanup"
set "command=powershell.exe -ExecutionPolicy Bypass -File ""%scriptpath%"""
:: Copy the script to the Windows directory
copy /Y "%~dp0NetProfileCleanup.ps1" "%scriptpath%" >nul
if %ERRORLEVEL% neq 0 (
echo [ERROR] Failed to copy "NetProfileCleanup.ps1" to "%windir%".
) else (
schtasks /create /tn "%taskname%" /tr "%command%" /sc onstart /ru "SYSTEM" /rl HIGHEST /f >nul 2>&1
if %ERRORLEVEL% equ 0 (
echo [SUCCESS] Created scheduled task "%taskname%".
) else (
echo [ERROR] Failed to create scheduled task "%taskname%".
)
)
REM Create time sync task to be run by the user at login
copy %~dp0\TimeSync.ps1 %windir%
set "taskname2=TimeSync"
set "command2=powershell.exe -WindowStyle Hidden -ExecutionPolicy Bypass -File \"%windir%\TimeSync.ps1\""
schtasks /query /tn "%taskname2%" >nul
if %ERRORLEVEL% equ 0 (
echo %DATE% %TIME% Task "%taskname2%" already exists, skipping creation.
) else (
schtasks /create /tn "%taskname2%" /tr "%command2%" /sc onlogon /rl HIGHEST /f
if %ERRORLEVEL% equ 0 (
echo %DATE% %TIME% Scheduled task "%taskname2%" created successfully.
) else (
echo %DATE% %TIME% Failed to create scheduled task %taskname2%.
)
)
~/Downloads/winapps-windows-setup/RDPApps.reg
Windows Registry Editor Version 5.00
; Enable Remote Desktop
; NOTE: The relevant firewall rule must be added separately with either:
; Enable-NetFirewallRule -DisplayGroup "Remote Desktop"
; or
; netsh advfirewall firewall set rule group="remote desktop" new enable=Yes
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Terminal Server]
"fDenyTSConnections"=dword:00000000
; Require Network Level Authentication (NLA) for Remote Desktop
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp]
"UserAuthentication"=dword:00000001
; Disable RemoteApp allowlist so all applications can be used in Remote Desktop sessions
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Terminal Server\TSAppAllowList]
"fDisabledAllowList"=dword:00000001
; Allow unlisted programs to be run in Remote Desktop sessions
[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services]
"fAllowUnlistedRemotePrograms"=dword:00000001
; Disable Windows 11 top-of-screen window snapping toolbar (snap bar)
[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced]
"EnableSnapBar"=dword:00000000
; Disable automatic administrator logon at startup
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon]
"AutoAdminLogon"="0"
; Always use the server's keyboard layout
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layout]
"IgnoreRemoteKeyboardLayout"=dword:00000001
; Disable "Do you want your PC to be discoverable" prompt after each host system reboot
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Network\NewNetworkWindowOff]
~/Downloads/winapps-windows-setup/NetProfileCleanup.ps1
# Get the current network profile name
$currentProfile = (Get-NetConnectionProfile).Name
# Get all profiles from the registry
$profilesKey = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\NetworkList\Profiles"
$profiles = Get-ChildItem -Path $profilesKey
foreach ($profile in $profiles) {
$profilePath = "$profilesKey\$($profile.PSChildName)"
$profileName = (Get-ItemProperty -Path $profilePath).ProfileName
# Remove profiles that don't match the current one
if ($profileName -ne $currentProfile) {
Remove-Item -Path $profilePath -Recurse
Write-Host "Deleted profile: $profileName"
}
}
# Change the current profile name to "WinApps"
$profiles = Get-ChildItem -Path $profilesKey
foreach ($profile in $profiles) {
$profilePath = "$profilesKey\$($profile.PSChildName)"
$profileName = (Get-ItemProperty -Path $profilePath).ProfileName
if ($profileName -eq $currentProfile) {
# Update the profile name
Set-ItemProperty -Path $profilePath -Name "ProfileName" -Value "WinApps"
Write-Host "Renamed profile to: WinApps"
}
}
~/Downloads/winapps-windows-setup/TimeSync.ps1
# Script to monitor if there is a sleep_marker created by WinApps (indicating the Linux host was suspended) in order to trigger a time sync as the time in the Windows VM will otherwise drift while Linux is suspended.
# Define the path to monitor. Make sure this matches the location for the sleep_marker in the Winapps script (need to match the APPDATA path).
$filePath = "\\tsclient\home\.local\share\winapps\sleep_marker"
$networkPath = "\\tsclient\home"
# Function to check and handle file
function Monitor-File {
while ($true) {
# Check if network location is available
try {
$null = Test-Path -Path $networkPath -ErrorAction Stop
# Check if file exists
if (Test-Path -Path $filePath) {
# Run time resync silently
w32tm /resync
# Remove the file
Remove-Item -Path $filePath -Force
}
}
catch {
# Network location not available, continue monitoring silently
}
# Wait 5 minutes before next check
Start-Sleep -Seconds 3000
}
}
# Start monitoring silently
Monitor-File
Linux 侧辅助配置和脚本
下面这些是本次为了避免手动输入、避免暴露密码、规避 GNU netcat 行为差异而创建的辅助脚本。它们不是 WinApps 官方必须步骤,但如果想复现我当前机器的效果,可以照着建。
~/.config/winapps/winapps.conf.template
##################################
# WINAPPS CONFIGURATION FILE #
# LAN Windows Pro / manual mode #
##################################
# Fill via: winapps-configure-local
RDP_USER="YOUR_WINDOWS_USERNAME"
RDP_PASS=""
RDP_ASKPASS="bash -c 'cat ~/.config/winapps/rdp-password'"
RDP_DOMAIN=""
RDP_IP="YOUR_WINDOWS_LAN_IP"
RDP_PORT="3389"
# Existing Windows machine on LAN.
WAFLAVOR="manual"
# Supported values: 100, 140, 180.
RDP_SCALE="100"
# /cert:tofu trusts first seen cert; +home-drive exposes Linux $HOME as \\tsclient\home.
# /kbd remap keeps left Ctrl/Alt behavior consistent with Omarchy ctrl:swap_lalt_lctl.
RDP_FLAGS="/cert:tofu /sound /microphone +home-drive /kbd:remap:0x1d=0x38,remap:0x38=0x1d"
RDP_FLAGS_NON_WINDOWS=""
RDP_FLAGS_WINDOWS=""
DEBUG="true"
AUTOPAUSE="off"
AUTOPAUSE_TIME="300"
# Homebrew installed FreeRDP 3.26.0 on this machine.
FREERDP_COMMAND="/home/linuxbrew/.linuxbrew/bin/xfreerdp"
PORT_TIMEOUT="5"
RDP_TIMEOUT="30"
APP_SCAN_TIMEOUT="60"
BOOT_TIMEOUT="120"
# If maximized RemoteApp windows are misaligned in Hyprland, change to "off".
HIDEF="on"
REMOVABLE_MEDIA="/run/media"
~/.local/bin/winapps-configure-local
#!/usr/bin/env bash
set -euo pipefail
CONFIG="$HOME/.config/winapps/winapps.conf"
PASSFILE="$HOME/.config/winapps/rdp-password"
TEMPLATE="$HOME/.config/winapps/winapps.conf.template"
mkdir -p "$HOME/.config/winapps"
if ! -f "$TEMPLATE" ; then
echo "Missing template: $TEMPLATE" >&2
exit 1
fi
if -f "$CONFIG" ; then
cp -p "$CONFIG" "$CONFIG.bak.$(date +%Y%m%d-%H%M%S)"
echo "Backed up existing config."
fi
read -r -p "Windows LAN IP: " ip
read -r -p "Windows username: " user
read -r -s -p "Windows password: " pass
echo
if | -z "$user" || -z "$pass" ; then
echo "IP, username and password are required." >&2
exit 2
fi
python3 - "$TEMPLATE" "$CONFIG" "$ip" "$user" <<'PY'
import pathlib, sys
src, dst, ip, user = map(str, sys.argv[1:])
text = pathlib.Path(src).read_text()
text = text.replace('YOUR_WINDOWS_LAN_IP', ip)
text = text.replace('YOUR_WINDOWS_USERNAME', user)
pathlib.Path(dst).write_text(text)
PY
printf '%s' "$pass" > "$PASSFILE"
chmod 600 "$CONFIG" "$PASSFILE"
echo "Wrote $CONFIG and password file with mode 600."
echo "Next: winapps-test-rdp"
~/.local/bin/winapps-test-rdp
#!/usr/bin/env bash
set -euo pipefail
CONFIG="$HOME/.config/winapps/winapps.conf"
if ! -f "$CONFIG" ; then
echo "Missing $CONFIG. Run: winapps-configure-local" >&2
exit 1
fi
# shellcheck disable=SC1090
source "$CONFIG"
FREERDP="${FREERDP_COMMAND:-xfreerdp}"
if ! -x "$FREERDP" && ! command -v "$FREERDP" >/dev/null 2>&1; then
echo "FreeRDP command not found: $FREERDP" >&2
exit 2
fi
if | -z "${RDP_USER:-}" ; then
echo "RDP_IP and RDP_USER must be set in $CONFIG" >&2
exit 3
fi
PORT="${RDP_PORT:-3389}"
echo "Checking TCP $RDP_IP:$PORT ..."
nc -z -w "${PORT_TIMEOUT:-5}" "$RDP_IP" "$PORT"
echo "Port open. Launching full RDP test. Close the Windows session window when done."
if -n "${RDP_ASKPASS:-}" ; then
export FREERDP_ASKPASS="$RDP_ASKPASS"
exec $FREERDP /u:"$RDP_USER" /d:"${RDP_DOMAIN:-}" /v:"$RDP_IP:$PORT" /cert:tofu /scale:"${RDP_SCALE:-100}"
else
exec $FREERDP /u:"$RDP_USER" /p:"$RDP_PASS" /d:"${RDP_DOMAIN:-}" /v:"$RDP_IP:$PORT" /cert:tofu /scale:"${RDP_SCALE:-100}"
fi
~/.local/bin/winapps-install-user
#!/usr/bin/env bash
set -euo pipefail
CONFIG="$HOME/.config/winapps/winapps.conf"
if ! -f "$CONFIG" ; then
echo "Missing $CONFIG. Run: winapps-configure-local" >&2
exit 1
fi
export PATH="$HOME/.local/lib/winapps-compat:$PATH"
cd "$HOME/src/winapps"
exec bash ./setup.sh --user
~/.local/bin/winapps-add-apps
#!/usr/bin/env bash
set -euo pipefail
export PATH="$HOME/.local/lib/winapps-compat:$PATH"
cd "$HOME/src/winapps"
exec bash ./setup.sh --user --add-apps
~/.local/lib/winapps-compat/nc
#!/usr/bin/env bash
# Compatibility wrapper for winapps-org/winapps port checks.
# Handles: nc -z host port [or nc -z -w N host port] with quick connect semantics.
real_nc="/home/linuxbrew/.linuxbrew/bin/nc"
if "$; then
shift
if "${1:-}" == "-w" && "$; then
shift 2
elif "${1:-}" == -w* && "$; then
shift
fi
host="${1:-}"
port="${2:-}"
if -n "$host" && -n "$port" ; then
exec bash -c 'exec 3<>"/dev/tcp/$1/$2"' _ "$host" "$port"
fi
fi
exec "$real_nc" "$@"
还需要确认的配置是否都已经展示?
本次复现涉及的关键配置已经都在文中展示:
- Windows 侧:
install.bat、RDPApps.reg、NetProfileCleanup.ps1、TimeSync.ps1。 - Linux 侧:
winapps.conf最终配置、winapps.conf.template模板、RDP 密码文件路径、WinApps 辅助脚本、GNU netcat 兼容包装器。 - Hyprland 侧:当前只依赖已有的
kb_options = compose:caps,ctrl:swap_lalt_lctl,没有额外新增 Hyprland window rule。
唯一不应该展示的是实际 Windows 登录密码;文章中只保留了密码文件路径和读取方式。
最终验证
最终启动:
winapps windows
Hyprland 里可以看到窗口:
Window -> Windows RDP Session [192.168.6.105]
class: Microsoft Windows
title: Windows RDP Session [192.168.6.105]
xwayland: 1
对应的 FreeRDP 进程:
/home/linuxbrew/.linuxbrew/bin/xfreerdp \
/cert:tofu \
/sound \
/microphone \
+home-drive \
/kbd:remap:0x1d=0x38,remap:0x38=0x1d \
/d: \
/u:你的 Windows 用户名 \
/scale:100 \
+auto-reconnect \
+dynamic-resolution \
/wm-class:"Microsoft Windows" \
/t:"Windows RDP Session [192.168.6.105]" \
/v:192.168.6.105:3389
此时:
- Windows 桌面窗口可以正常打开。
- Backspace 正常。
- 左 Ctrl / 左 Alt 的行为和我在 Omarchy 里的交换习惯一致。
- Linux home 目录能通过
\\tsclient\home映射到 Windows。
常用命令
启动完整 Windows 桌面:
winapps windows
启动资源管理器:
winapps explorer
手动启动记事本:
winapps manual "C:\Windows\System32\notepad.exe"
杀掉 FreeRDP 会话:
winapps killrdp
查看 WinApps 日志:
tail -200 ~/.local/share/winapps/winapps.log
查看 FreeRDP 进程:
pgrep -a -f freerdp
查看 Hyprland 里的窗口:
hyprctl clients
总结
WinApps 在 Arch + Omarchy + Hyprland 上是可以跑通的,但它不是完全开箱即用。
我这次踩到的坑主要来自三个地方:
- Wayland / Hyprland 下 FreeRDP 客户端选择问题。
- Homebrew GNU netcat 和 WinApps 端口检查逻辑不兼容。
- 键盘输入在 XKB、FreeRDP scancode、Windows 虚拟键之间的映射差异。
最终稳定方案是:
WAFLAVOR="manual"
FREERDP_COMMAND="/home/linuxbrew/.linuxbrew/bin/xfreerdp"
RDP_FLAGS="/cert:tofu /sound /microphone +home-drive /kbd:remap:0x1d=0x38,remap:0x38=0x1d"
REMOVABLE_MEDIA="/run/media"
如果你也是 Arch / Omarchy / Hyprland,并且 Windows 设备是局域网里的 Windows Professional,可以按这篇文章基本 1:1 复现。