Switch Windows virtual desktops with a mouse gesture. Hold the wheel button, drag left or right, and Windows moves to the neighbouring desktop.
No Administrator privileges, no runtime to install, no driver, no service. One 270 KB executable that works with any mouse of any brand.
There is a product page at https://mouse-desktop.nivelepsilon.com, with a recording of the gesture, the checksums to verify a download against, and, if a managed machine refuses to run it, the request your IT department will want.
Windows already changes virtual desktop with Ctrl+Win+Left and Ctrl+Win+Right, but
that means taking a hand off the mouse. Vendor software such as Logitech Options or Razer
Synapse can remap a mouse button to that shortcut, and it is genuinely pleasant to use.
The problem is that every one of those tools needs an Administrator account to install. On a managed corporate laptop there is no such account, so the comfortable way of working disappears along with it. This utility gives it back, using only what a standard user is allowed to do.
Several free tools already do something similar. They were evaluated first, and why none of them fitted is written up in docs/prior-art.md.
- Hold the wheel button and drag horizontally to move one desktop per 60 pixels.
- Keep dragging in the same direction to walk across several desktops in one motion.
- Press and release the wheel button without dragging and the click is passed on intact, so middle click keeps closing browser tabs.
- Drag vertically and nothing happens, so the gesture stays out of the way.
The trigger button, the distance, the direction and the rest are configurable.
- Windows 10 version 21H2 or later, or Windows 11.
- A mouse with a wheel button, or any of the other configurable triggers.
- Nothing else. The executable links only against DLLs that ship with Windows.
Everything below works from an ordinary user account. Nothing here asks for elevation, and if a User Account Control prompt ever appears, something is wrong: stop and read the troubleshooting table.
On a managed machine, do this before anything else. It takes a minute and it is the cheapest way to find out whether the rest is worth attempting.
The probe has to be an unsigned executable that has never been seen before, because that
is what this utility is and it is the property application control actually judges. Do not
copy a system tool such as notepad.exe and run the copy: that is a flagged technique in
its own right, blocked by a separate Attack Surface Reduction rule
(c0033c00-d16d-4114-a5a0-dc9b3a7d2ceb, "Block use of copied or impersonated system
tools"), so it fails on machines that would have run this utility perfectly well. The C#
compiler that ships in the box with the .NET Framework builds throwaway probes instead,
with nothing to install.
$dir = "$env:LOCALAPPDATA\Programs\mouse-desktop"
New-Item -ItemType Directory -Force -Path $dir | Out-Null
$csc = "$env:WINDIR\Microsoft.NET\Framework64\v4.0.30319\csc.exe"
if (-not (Test-Path $csc)) { $csc = "$env:WINDIR\Microsoft.NET\Framework\v4.0.30319\csc.exe" }
Set-Content "$dir\probe.cs" 'class P{static void Main(){System.Console.WriteLine("probe ok");}}'
foreach ($i in 1..3) {
& $csc /nologo /target:exe /out:"$dir\probe$i.exe" "$dir\probe.cs"
& "$dir\probe$i.exe"
}Read the outcome as a reliable no and an unreliable yes.
Three probe ok lines mean the machine probably runs unsigned programs from your profile,
so carry on. They are not a proof. The rule that does most of this blocking asks the
Defender cloud about a file it has never seen before, and when that lookup times out the
file goes through anyway. On the machine these instructions were tested against, one probe
in fifteen slipped past that way while the release executable was blocked every single
time. It is also why the snippet compiles three separate binaries instead of running one
of them three times: each compilation produces a different file, whereas a second run of
the same one only re-reads a verdict that has already been cached. What actually settles
the question is step 4, when you start the utility itself.
An "Access denied" on any of the three rounds is unambiguous in the other direction: something is blocking unsigned executables, and this utility will be blocked the same way. A message about your organisation's policy points at an application control policy. A bare denial with no dialog, where reading the file fails as well as running it, points at Attack Surface Reduction. Either way, read the troubleshooting table below before going any further.
Delete probe.cs and the probe1.exe to probe3.exe files when you are done.
Pick one:
-
With winget, which ships with Windows 10 and 11 as App Installer, if your organisation has not switched it off:
winget install --id fpinero.mouse-desktop
By default it installs into your user profile and puts a
mouse-desktopcommand on the path. -
With Scoop, if you already use it. Scoop exists to install software without an Administrator account, which is the same constraint this utility was written for:
scoop bucket add fpinero https://github.com/fpinero/scoop-bucket scoop install mouse-desktop
Both package managers do the copying that step 3 does, so skip to step 4. Neither of them starts the utility or adds the start-up entry, though. Start it once with
mouse-desktop(after winget, in a new terminal, because the path only changes for windows opened afterwards), then right click the tray icon and tick "Start with Windows". Leave the configuration file where it is created by default: the folder a package manager installs into belongs to it and can be replaced on the next update. -
From a release. Download
mouse-desktop-x64.exefrom the releases page. Use thearm64build only on an Arm laptop such as a Snapdragon X machine; the x64 build also runs there under emulation. -
Build it yourself, on a machine where you can install Rust. This is the option that lets you know exactly what you are running:
cargo build --releaseThe result is
target\release\mouse-desktop.exe, about 270 KB. Copy that one file to the target machine, by memory stick, OneDrive, or however files normally reach it. No build tools are needed on the machine that runs it.
A package manager changes how the file arrives, not whether a managed machine lets it run. Attack Surface Reduction and application control judge the executable itself, so a file that step 1 predicts will be blocked is blocked just the same when winget or Scoop put it there. Whichever way you got it, the first start may show the SmartScreen panel described in step 3.
The script does the whole job:
.\scripts\install.ps1 -Source .\mouse-desktop.exeIt copies the executable to %LOCALAPPDATA%\Programs\mouse-desktop, adds a start-up entry
under HKEY_CURRENT_USER, and starts it. Add -NoAutostart or -NoStart to skip either
part.
If PowerShell refuses to run the script because of the execution policy, either allow it for the current session only:
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypassor skip the script entirely and do it by hand, which is the same three actions:
$dir = "$env:LOCALAPPDATA\Programs\mouse-desktop"
New-Item -ItemType Directory -Force $dir | Out-Null
Copy-Item .\mouse-desktop.exe "$dir\mouse-desktop.exe" -Force
New-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run' `
-Name 'mouse-desktop' -Value "`"$dir\mouse-desktop.exe`"" -PropertyType String -Force
Start-Process "$dir\mouse-desktop.exe"The first time the file runs, Windows SmartScreen may show a blue "Windows protected your PC" panel because the executable is not signed. Choose "More info", then "Run anyway".
- An icon of two blue arrows appears in the notification area. On Windows 11 it may be hidden behind the chevron next to the clock; drag it onto the taskbar to keep it in sight.
- Create a second virtual desktop with
Ctrl+Win+D. - Hold the wheel button and drag about 60 pixels to the left. The desktop should change.
- Middle click a browser tab. It should close, which confirms that a click without a drag is still passed through.
- Right click the tray icon and confirm "Start with Windows" is ticked.
- Sign out and back in. The icon should come back on its own.
Put mouse-desktop.exe anywhere, including a memory stick, and run it. To keep the
configuration beside the executable rather than in your profile, create an empty
config.toml in the same folder. The utility fills it in on the next start and reads it
from there, so nothing is left behind on the machine.
Run install.ps1 again with the new executable. It stops the running copy first, so there
is nothing else to do. Your configuration file is not touched.
If a package manager installed it, let it do the update instead. Choose "Exit" from the
tray menu first, or run Stop-Process -Name mouse-desktop, because Windows will not
replace an executable that is running:
winget upgrade --id fpinero.mouse-desktop
scoop update mouse-desktopThen start it again and check in the tray menu that "Start with Windows" is still ticked. The tick is only shown when the start-up entry points at the very copy that is running, so if the update moved the file, it shows unticked and one click puts it right.
Neither package manager follows the releases page by itself. A version reaches winget once
its manifest has been merged into microsoft/winget-pkgs, which includes a review by a
volunteer moderator, and reaches Scoop once the bucket has been updated for it. Until then
both answer that there is nothing to upgrade, so they can trail a new release by days or
weeks.
A new release is a new file, and a managed machine judges it from scratch. A laptop that runs the current version without complaint can still block the next one for a while, as described under "Blocked by Attack Surface Reduction" below.
.\scripts\uninstall.ps1This stops the utility, removes the start-up entry and deletes the folder. Add
-RemoveConfig to delete the configuration too.
If a package manager installed it, untick "Start with Windows" in the tray menu and choose "Exit" before you remove it:
winget uninstall --id fpinero.mouse-desktop
scoop uninstall mouse-desktopNeither of them knows about the start-up entry, which lives under HKEY_CURRENT_USER, so
they leave it behind pointing at a file that no longer exists. They leave the configuration
in %APPDATA%\mouse-desktop too; delete that folder by hand if you want it gone.
| What you see | What it means | What to do |
|---|---|---|
| "This app has been blocked by your system administrator" | AppLocker or App Control is blocking user-writable folders | Ask IT to allow the file by hash, or to name a folder you may run programs from, and install there |
| SmartScreen panel on first run | The executable is unsigned | "More info", then "Run anyway". Building it yourself avoids the question |
| The script will not run | PowerShell execution policy | Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass, or use the manual commands above |
| No icon in the notification area | It may simply be hidden | Click the chevron next to the clock and drag the icon onto the taskbar |
| The gesture works everywhere except one window | That window belongs to an elevated process | Expected, see the known limitations |
| Nothing happens at all | Another copy may already be running | Check with Get-Process mouse-desktop, and start it with --verbose to get a log next to the configuration file |
| A bare "Access denied", with no dialog, and even reading the file fails | A Defender Attack Surface Reduction rule is blocking it on reputation | See "Blocked by Attack Surface Reduction" below |
Attack Surface Reduction is not AppLocker and it does not announce itself. Rule
01443614-cd74-433a-b99e-2ecdc07bfc25, "Block executable files from running unless they
meet a prevalence, age, or trusted list criterion", blocks executables that are unsigned
and too new to have a reputation, which is exactly what a fresh build of this utility is.
It is easy to misread, because it gives a bare "Access denied" with no dialog and it denies
plain reads as well as execution, so even Get-FileHash fails and the symptom looks like a
file permission problem. It is not. The rule follows the file and not the folder, so
another directory, a portable copy and a renamed file all behave the same, while a signed
and prevalent executable in that very folder runs normally.
Get-MpPreference will not show it either: a standard user cannot read the policy merged
from Intune, and the rule list comes back empty. The event log is the reliable check.
Get-WinEvent -LogName 'Microsoft-Windows-Windows Defender/Operational' -MaxEvents 50 |
Where-Object Id -eq 1121Event 1121 is a block, 1122 is audit only. The message carries the rule id and the path.
There is no way around this from a user account, and looking for one is the wrong move on a managed machine. What does work, cheapest first: wait, because age is one of the three criteria and a fresh build fails it; submit the file to Microsoft through the Defender Security Intelligence submission portal, which is the intended route for a reputation block and needs no local privileges; ask IT for an Attack Surface Reduction exclusion by hash; or sign the release with an Authenticode certificate, which is the only fix that also travels to other managed machines.
Waiting works, but not on the timetable the documentation suggests. Microsoft describes the age criterion in terms of hours. On the machine these instructions were tested against, the release binary was refused every single time for the next forty four hours, ninety blocks in the event log, and then the very same file, byte for byte, ran without a complaint two weeks later. Nobody tried in between, so the day it turned is unknown. Budget ten days rather than two, and do not read a block on the second day as a permanent verdict.
The clock is not yours to start. Age is counted by the Defender cloud from the first time it saw that hash anywhere in its telemetry, not from the moment the file reached your Downloads folder, so deleting it and downloading it again advances nothing, and neither does renaming it or moving it. The flip side is convenient: once a file has cleared it stays cleared wherever it goes, so copying it into the install folder afterwards is safe. Every new release is a different hash and starts again from zero.
The two remedies are hard to tell apart afterwards. In the case above the file was submitted through the portal on the first day, so there is no way to know from the outside whether it finally passed on age or on the review. Submitting early costs nothing and at least guarantees the cloud has seen the file at all.
The file lives at %APPDATA%\mouse-desktop\config.toml and is created with comments on
the first run. Open it from the tray menu, edit it, then choose "Reload configuration".
| Setting | Default | Meaning |
|---|---|---|
trigger |
"middle" |
Button to hold: middle, right, x1 or x2 |
threshold_px |
60 |
Horizontal distance that moves one desktop |
repeat_cooldown_ms |
250 |
Shortest delay between two switches |
max_vertical_ratio |
1.0 |
How far from horizontal a drag may stray and still count |
invert |
false |
Swap the direction of travel |
replay_click_when_no_gesture |
true |
Pass the click on when there was no drag |
enabled |
true |
Start with gesture recognition switched on |
x1 and x2 are the two side buttons found on most mice, usually mapped to back and
forward. Choosing one of them as the trigger makes the gesture feel closest to a vendor
button remap.
Elevated windows. While the window in front belongs to a process running as Administrator, such as an elevated terminal, Task Manager started as administrator, or the UAC prompt itself, the gesture does nothing. Windows deliberately hides input from lower-privileged processes through User Interface Privilege Isolation, and there is no way around it that does not require the Administrator account this utility exists to avoid.
No wrap around. On the first or the last desktop, dragging further does nothing. That
is how Ctrl+Win+Arrow behaves too.
Unsigned executable. The binary carries no code signing certificate, so Windows SmartScreen warns the first time it runs, and a managed laptop with AppLocker or App Control policies may refuse to run it from a user-writable folder at all. If that happens, the options are to have IT allow it by hash or to place it in a folder the policy permits. Building it yourself from this repository is the surest way to know what you are running.
Corporate policy. Nothing here bypasses a policy. It only avoids needing rights a standard user does not have.
A global low level mouse hook, SetWindowsHookEx(WH_MOUSE_LL, ...), watches for the
trigger button being held. When the pointer has travelled far enough horizontally, the
utility synthesises Ctrl+Win+Left or Ctrl+Win+Right with SendInput. Neither call
needs any privileges.
Switching could also be done through IVirtualDesktopManagerInternal, a COM interface
that changes desktops directly. This project does not use it: Microsoft changes its GUIDs
with nearly every major Windows update, which silently breaks callers several times a
year. Synthesising the documented shortcut is slower by a few milliseconds and survives
Windows updates.
The trigger button is hidden from other applications while it is held, so a drag cannot also open a context menu or start an autoscroll. If the button is released without a drag, a real click is injected at the pointer, which is what keeps ordinary clicking with that button working.
cargo build --release
cargo test # gesture, configuration and icon logic
cargo clippy --all-targets -- -D warnings
cargo test -- --ignored --nocapture # end to end, needs an interactive sessionThe end to end test starts the real executable, synthesises a wheel drag, and checks
through the documented IVirtualDesktopManager interface that Windows actually changed
desktop. It creates two scratch desktops for the measurement and closes both afterwards.
It takes over the mouse pointer for about five seconds, so do not run it while you are
using the machine.
Stop any installed copy before running it. The utility refuses to start twice, so the copy the test launches would put up a dialog instead of installing its hook. The test checks for this and says so rather than reporting a broken gesture.
Manual check, worth doing after any change to the hook:
- Create three virtual desktops with
Ctrl+Win+D. - Start the utility and confirm the icon in the notification area.
- Hold the wheel button and drag right over the desktop, over Explorer and over a browser. Each should move one desktop forward.
- Keep dragging without releasing. It should keep moving forward.
- Middle click a browser tab. It should close, which proves the click was passed on.
- Hold the wheel button and drag vertically. Nothing should happen.
MIT. See LICENSE.