Skip to content

About

Move windows between monitors without dragging — flick one across, or trade two screens whole · UI in 한국어 العربية ไทย हिन्दी | 多屏之间搬窗口不用拖:划一下送走一扇,或整块屏对调

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

WindowShuttle

Move windows between monitors without dragging — one at a time, or a whole screen at once

⬇ Download for Windows · 简体中文

License: MIT 365 Open Source Plan #033

A map of the connected monitors drawn at their desk-relative sizes and positions, each card listing its windows as chips; the primary screen is badged and the screen holding the cursor is highlighted

Getting a window onto another monitor should not be a drag across the desk. Hold Ctrl and the right button, flick toward the screen you want, and the window under your cursor is already there. Need the whole screen instead? One hotkey trades every window between the screen you're on and the primary. WindowShuttle lives in the tray and works out of the box.

  • Flick, don't aim. Ctrl + right button, throw the pointer at a screen, let go. You were already pointing at it, so there is no screen number to translate first. Misflicked? Ctrl + middle-click puts it back.
  • One window or the whole screen, same tool. Seven actions on one scale: send this one somewhere, trade two screens outright, or gather everything home after a monitor comes off. All of them from the keyboard, the mouse, the tray menu or the CLI.
  • Geometry is recomputed, not copied. Mixed DPI is the main case, not an edge case: sizes are rescaled for the destination, maximized stays maximized, minimized restores on the new screen, and a snapped window keeps its half or its quarter.
  • The map is the control surface. The panel at the top draws your monitors at the size and position they actually have on your desk — drag a window from one card to another and it moves. The map redraws after every move, so it always shows where the windows are now; hover a truncated chip to read its full title.
  • Nothing to commit to. One portable exe, no installer, no account. It stays offline unless you ask it to check for updates — that one box is the only thing that ever goes online. Windows only.

Does it fit your setup

Area Support
Windows 10 or 11
Architecture x64. Only x64 builds are published — there is no native ARM64 one
.NET Not needed for the standalone build; the smaller one wants the .NET 10 desktop runtime
Monitors Two or more. The screen-to-screen actions need at least two; on a single screen they answer Only one screen (or nothing to do) and move nothing
Mixed DPI / resolutions Supported, and the point of the thing: geometry is rescaled for the target screen instead of copied pixel-for-pixel
Admin-owned windows Need WindowShuttle itself to run elevated — see Known limitations

Download

⬇ Get the latest release — Windows x64.

  • WindowShuttle-<version>-win-x64.zip — standalone; nothing else required. Unzip and run. Take this if you're unsure.
  • WindowShuttle-<version>-win-x64-needs-dotnet10.zip — the same app without the bundled runtime; requires the .NET 10 desktop runtime already installed on the machine. Much the smaller download; the release page prints both sizes.
  • WindowShuttle.exe — that same build as the zip above, with no zip around it: click, run, or drop it over your existing copy to update. Windows offers the runtime download if it is missing.

All three unpack to (or already are) the same single-file WindowShuttle.exe — no installer, no account. The only difference is startup cost: the standalone exe unpacks itself to a temp folder on every launch, which makes it slower to start than the runtime-installed build — most visibly on a one-shot CLI call like list. That gap doesn't touch the resident tray app or hotkey response — it's paid once at launch either way, not on every swap.

The exe isn't code-signed, so Windows SmartScreen warns on first run: click More info → Run anyway.

When something goes wrong. If an action ever dies mid-step, the app recovers, keeps running, and says so on a card — the details are appended to %APPDATA%\WindowShuttle\crash.log. The same log catches side failures (a notification or map refresh that failed) without reversing a move that already completed. The file only exists if something actually went wrong; an empty search there is itself the answer.

Removing it. There is no installer, so there is nothing to uninstall — with one exception: if you ever ticked As administrator, WindowShuttle registered a scheduled task, and deleting the exe would leave that task behind, pointing every logon at a file that no longer exists. Untick Start with Windows first — that clears both the task and the registry entry — then delete the exe. Settings live in %APPDATA%\WindowShuttle and can go with it.

The seven actions

Six of the seven are in the tray menu and the CLI; Send by direction is mouse-gesture only, because a menu click or a command has no direction. Identify screens is a separate tool in the tray menu and the CLI. Two actions ship with a keyboard shortcut and four with a mouse gesture:

Below, "the window" means whatever your hand is on: a mouse gesture acts on the window under the pointer (as of the moment you pressed), while the keyboard, the tray menu and the CLI all act on the current foreground window (see Known limitations for why the two differ).

Action Shortcut Mouse gesture What it does
Swap screens Ctrl+Alt+1 Alt + middle-click Swap every window between that screen and the primary; when it is already the primary, it swaps with the screen you last swapped with
Swap front windows (unbound) Alt + right-click Swap just the frontmost window of each of those two monitors
Send to primary (unbound) (unbound) Send that window to the primary monitor and raise it
Send to next monitor (unbound) (unbound) Send that window to the monitor on its right, wrapping to the leftmost, and raise it
Send by direction (not applicable) Ctrl + right, flick Flick toward a screen and the window under the cursor goes there. See Mouse gestures
Gather all (unbound) (unbound) Gather every window sitting on a secondary screen back to the primary monitor. A window stranded completely off-screen after a display is unplugged is a different case — that is the automatic rescue, below
Undo Ctrl+Alt+2 Ctrl + middle-click Restore the positions from before the last move

There is also one tool that moves nothing: Identify screens, the button next to Refresh above the map. Press it and each screen flashes its own number in the middle — that's how you check that the arrangement Windows believes in matches the one on your desk. It takes no shortcut or gesture slot (it isn't a window-moving action); the tray menu and the identify command trigger it too.

Why only two hotkeys are bound out of the box, on purpose: a global hotkey takes that combination away from every application on the desktop, so WindowShuttle claims as little as it can get away with — Swap and Undo, because being unable to undo a bad move is the worst first impression a window-moving tool can make. The rest are yours to bind. Every action's shortcut slot shows one of three states: bound (normal), unbound (gray placeholder text, not an error), or conflict (red border + tooltip, when another program already holds that combination). Send by direction is the exception — it carries no shortcut at all, so its slot shows a dash and refuses to record. To release a bound hotkey, click its key slot and press Delete or Backspace — it clears back to unbound, saved immediately, no editing settings.json by hand required. After a successful capture the slot keeps keyboard focus (so Tab keeps its place) but stops listening; click it again to record another combination. Why Ctrl+Alt+<letter> is avoided by default: docs/design-notes.md.

Mouse gestures

Your hand is already on the mouse, so four gestures ship bound. Every action has a gesture slot next to its shortcut slot in the main window — click it and perform the gesture to record, Delete to clear.

Modifier Right button — this one window Middle button — a pile of windows
Ctrl — move this one Send by direction Undo
Alt — swap both ways Swap front windows Swap screens
Shift — left clear (empty) (empty)

The best cell goes to send-by-direction, because it is the one you reach for all day: point at a window, flick toward the screen you want, done. Of the three modifiers, Ctrl + right takes the least away — Shift + right is Explorer's "copy as path" extended menu, Alt + right is AltSnap's default resize gesture, and Ctrl + right has no widely-honoured meaning to collide with.

Undo sits right beside it, on the same modifier. Move windows by flicking and you will misflick sooner or later; having to reach for the keyboard at that moment breaks the whole flow. So the Ctrl row reads "I am moving windows" — right sends it away, middle takes it back.

Two clues instead of four facts. The button says how much: right sits under your finger and acts on the one window beneath the cursor; middle takes a deliberate press and moves a pile at once — that holds for the whole middle column (undo usually takes back exactly the kind of bulk move the middle column makes), so every action whose misfire costs the most is behind the button you have to press on purpose. The modifier says what: Ctrl moves or takes back, Alt exchanges — one window and whole screen versions of the same idea.

The Shift row is left clear, both for you and for Explorer and your browser: Shift + right-click (that extended menu) and Shift + middle-click (open a tab in the foreground) are things people use every day, so the defaults stay off them. When you want gestures of your own, that row is already free.

Send to primary, Gather all and Send to next monitor ship without a gesture — not because they are weak, but because there is already a way: send-to-primary is a flick toward the primary; gather-all's main job (windows stranded off-screen after unplugging a display) is handled by the automatic rescue, with the manual one as a backstop; send-to-next is a flick to the right. All three are in the tray menu and the CLI, and recording a gesture for one is a single click. Binding one more cell costs another combination taken from other programs, and buys a path you already had.

Ctrl + right, flick

Hold Ctrl and the right button, flick toward the screen you want, let go. The window under the cursor goes there. No need to work out which number that screen is — you're already pointing at it. Misflicked? Ctrl + middle-click puts it back, without your hand leaving the mouse.

  • How far counts as a flick: 40 pixels. Anything shorter isn't a direction — the card tells you to flick somewhere, and the click is replayed as-is.
  • Direction is the angle of the whole stroke, split at 45°, diagonals counting as horizontal. It takes the farthest the stroke ever reached on each axis: within 45° of horizontal counts as left/right, steeper counts as up/down — an exact diagonal goes to left/right, the overwhelmingly common direction on a row of monitors. The whole stroke is measured, not the release point: a fast lateral flick always carries a downward tail, and judging by where you let go misreads a crisp sideways flick as vertical (caught in real use). Swinging out and back still counts — the distance you reached sideways doesn't vanish because you came back.
  • Flicking across a screen edge is fine: what moves is always the window you were pointing at when you pressed, no matter which screen you release on — flicking toward the screen on the right naturally ends up on it.
  • The edges wrap around: flick right on the rightmost screen and the window cycles to the leftmost one — the same convention as Send to next monitor wrapping at the end. It only wraps along the same axis: flick up on a row of monitors and there genuinely is no screen that way, so it still says so.
  • Change your mind mid-flick: click the left button and the whole thing is cancelled — no window moved, no menu opened.
  • A snapped window stays snapped. A window snapped to the left half lands on the left half of the screen you flick it to — quarters, top/bottom halves and vertical thirds likewise — instead of being proportionally scaled into a free-floating "roughly half" window. Detection is geometric (the window occupies a snap slot of its screen's work area); Windows' internal snap-group bookkeeping is out of reach for any third party, but the layout your eyes see survives the move.

Why not the bare right button. That is the usual shape for gesture software, and it does work here — verified by watching the messages a real Win32 window receives, not by counting menus. It isn't the default because the stakes are lopsided: what you save is one modifier key, what you bet is the system-wide context menu — and if the replay ever fails on a machine we can't test, the victim is every right-click you own. The capability is still there for anyone who wants it: record that slot as a bare right button (it is allowed for this action only). The measurement write-up: design notes.

The four that ship do take combinations away from other programs — Ctrl + middle-click is a browser's background tab, Alt + right-click is AltSnap — and that is a deliberate trade, not an accident: a bound gesture is intercepted before any application sees it, WindowShuttle always wins the combination it recorded. If you would rather give one back, click that slot and press Delete; it is one keystroke. And unlike a hotkey, whose registration fails visibly (the red state), a mouse gesture gets no conflict signal from Windows at all, so a silent collision is possible — the app says so once, in the settings strip at the bottom of the window, right after the mouse-glyph mark that identifies every gesture slot. Mechanism detail: design notes.

Left alone on purpose: bare buttons — none are used out of the box (see the section above); and the side buttons, which many mice do not have, so they stay out of the defaults and are yours to record. Treat the Windows key as unavailable — Windows reclaims that key before the click reaches any application, so a press with no readable modifier is dropped and no binding is stored; the one exception is Send by direction, whose slot takes a bare button by design. That is why the recording prompt names only Ctrl / Alt / Shift. The app does not pop a card explaining this — what needed saying is in the prompt, before you act. How it was measured: design notes.

Bind no gestures at all and the hook does not exist in the process — not "installed but idle". Clear all four slots with Delete and WindowShuttle leaves the mouse-input path entirely, leaving keyboard shortcuts, the tray menu and the CLI. Worth knowing: if you run AutoHotkey, Logitech Options+, PowerToys or similar, they sit on the same hook chain; whichever link gets slow, the symptom is the same — a sluggish pointer and clicks that occasionally stick — so disabling them one at a time is the way to find it. The hook's thread architecture and its 300 ms budget: design notes.

Settings

The bar along the bottom of the main window, plus one box in the title bar. Defaults in brackets:

  • Skip fullscreen apps (on) — a window that exactly fills a monitor with no title bar or thick border is left alone. This applies to every action.

  • Rescue off-screen windows when a monitor disconnects (off) — turn it on and, about two seconds after you unplug a display, any window left completely outside every remaining screen is pulled back to the primary.

    It is off by default because Windows already moves windows off a display you disconnect — the main case is covered. What is left is the residue: a minimized window restored to a remembered off-screen rect, an app that starts up at its own saved coordinates, a window still sitting where it was after a resolution drop. Paying for that residue with moving your windows when you pressed nothing should not be everyone's default. Turn it on if you hit it; Gather all does the same thing on demand any time.

    It only touches windows with zero overlap with any monitor, so anything you can still see stays put, and it deliberately does not take the undo slot: undo still steps back the last move you made.

  • Keep running in the tray when closed (on) — the close button hides the window instead of exiting. Turn it off and closing the window quits.

  • Check for updates (off; in the title bar, next to the GitHub link) — the only thing in this program that ever goes online. Tick it and each launch asks GitHub once whether a newer release exists; if there is one, the standalone zip is downloaded, its SHA-256 is checked against the one published next to it, and the card says where the new program landed. It does not replace the running copy — Windows locks the exe that is executing, and overwriting it leaves a half-installed program behind. There is a Check now button next to the box for asking without waiting for the next start.

  • Start with Windows (on) — a tray-resident tool that doesn't start with the machine loses half its point. The plain tier is a HKCU\...\Run entry passing --tray, so the boot launch goes straight to the tray without putting a window in your face; launching the exe yourself opens the main window.

  • As administrator (off; under Start with Windows) — swaps the registry entry for a Run-with-highest-privileges scheduled task: one UAC prompt when you tick it, then every boot starts silently elevated.

    This is the only complete fix for the two elevated-window limitations (their windows can't be moved, and gestures go dead while one holds the foreground — which in daily use looks like "my flick suddenly turned into a right-drag/copy"). Every bypass we measured, including handing the move to Windows' own Win+Shift keys, dies on UIPI. The first launch does not ask for this — it only turns on plain autostart. Elevation buys exactly those two things, and if you never hit them you never need it; the app speaks up when you do — the toast shown when a move is refused and the one shown when gestures go dead both offer to restart elevated, and Restart as administrator sits in the tray menu. Tick the box to make it permanent; unticking deletes the task (one more UAC). While resident elevated, the CLI from a non-elevated terminal keeps working (the message filter allows it).

CLI

WindowShuttle.exe <command> — output and errors are always in English, regardless of the UI language, so scripts can parse them reliably:

Command Effect
swap [--to <n>] Swap screens (optionally targeting monitor <n> instead of the primary; if the current window is already on screen <n> it reports "already on the target screen" and exits 1)
swap-top Swap front windows
to-primary Send to primary
to-next [--to <n>] Send to the monitor on the right, wrapping to the leftmost (or straight to monitor <n>)
gather Gather all
undo Undo
identify Flash each screen's number
list Print monitors and movable windows as JSON (every window carries position, the same numbering --to <n> takes). Movable means the same thing here as everywhere else in the app: windows that are not responding are left out, and so are fullscreen ones while Skip fullscreen apps is on — so a window absent from this list is a window no command would have moved either
--help Print the usage text and exit 0. A question, not a typo — an unknown verb exits 3

There is one screen numbering in this app: position, counted left to right (then top to bottom within a column), 1-based. It is what the map prints, what Identify screens flashes (the button next to Refresh), what --to <n> takes, and what list reports as position.

It is not the number in Windows display settings. That one has no relation to where a screen sits — put a second screen left of your primary and the primary is still number 1 while sitting in the middle — and it can be reshuffled by changing ports, docking, or reinstalling a GPU driver, which would silently repoint a binding at a different screen. The Windows number isn't lost: the map card's small print shows system N, and list still carries index and device for reconciling with the OS.

Sending a window straight to one particular screen is reachable from a script as to-next --to <n>; it shares this command rather than adding a redundant verb. Send by direction has no CLI verb of its own — "which way did you flick" has no command-line counterpart, and a --direction left would just be --to <n> under another name.

Exit codes: 0 done, 1 nothing to do, 2 some windows failed to move (see Known limitations), 3 error (bad arguments, a --to position that doesn't exist, etc.).

The CLI follows the same rule as the keyboard: it acts on the current foreground window. Run to-primary from a terminal and the terminal window is what moves — far more predictable for a script than "wherever the mouse happens to be parked".

If WindowShuttle is already resident in the tray, running a CLI command relays it to that instance and exits with its result — hotkeys, tray menu, and CLI all share the same undo stack. If nothing is resident, the command runs once, standalone, and exits without leaving anything in the tray. list is the exception: it always answers directly from the invoking process, read-only, no IPC.

Known limitations

  • Windows belonging to elevated (administrator) programs can't be moved. A non-elevated process can't reposition a higher-integrity window — Windows blocks it (UIPI), not a WindowShuttle bug. WindowShuttle counts how many windows this happened to and offers to restart itself elevated — click the notification card, confirm the prompt, and it relaunches with admin rights. This is a real, visible behavior: on the author's daily desktop, 6 of 27 open windows hit it on an average swap.

  • Gestures stop working entirely while an elevated program has focus. When the foreground window belongs to a program running as administrator, Windows stops delivering mouse events to a normal-privilege global hook like ours — regardless of which screen the cursor is on or which window you click. Every gesture goes silent together. Focus any ordinary window (one click elsewhere) and it recovers immediately; no restart needed. Measured: with an ordinary window focused, 6 of 6 gestures landed; with an elevated window focused, 0 of 5 landed on ordinary windows.

    WindowShuttle tells you about this itself, but only while you are actually using gestures: if you triggered a mouse gesture within the last three minutes and an elevated program then takes and holds the foreground, an actionable card appears; clicking it takes the Restart as administrator route. The condition is "you are using gestures", not "the foreground changed" — the alternative nagged people who never meant to move a window. Why, with the tested-and-rejected version: design notes.

    Keyboard shortcuts are completely unaffected (RegisterHotKey, a different path), which makes them the fastest diagnosis: gestures dead but shortcuts fine = something elevated has focus. The only way to remove the limitation is to run WindowShuttle elevated too — the tray menu has Restart as administrator. That is also the only way the elevated windows themselves become movable, since SetWindowPos is refused at normal privilege as well.

  • Once in a while a gesture comes out as a plain right-click. The modifier has to be held at the instant the button goes down — that is when the decision to swallow the click or let it through must be made. If Ctrl lands even a few milliseconds late, that press matches nothing, passes straight through to the app underneath, and you get a context menu instead of a window flying across. This is not a tolerance that can be widened: by the time the drag reveals what you meant, the button-down has already been delivered. Why even a "simultaneous" press usually loses — the polling-rate measurements: design notes.

    Two ways around it: hold Ctrl down across a run of flicks (only the first one can race), or bind the action to a bare side button (X1/X2) — no modifier, no race, and a press without a flick is replayed verbatim so the button's usual Back still works.

  • Which window moves depends on which hand you used. Mouse gestures act on the window under the cursor — the one you were pointing at when you pressed (and for a flick across a screen edge, still the one under the press point). Keyboard shortcuts, the tray menu and the CLI act on the focused window. Why this is one rule with two anchors, not an inconsistency: design notes.

    Neither path ever reaches past its target: if the window it points at can't be moved (WindowShuttle's own window, a skipped fullscreen or hung app), nothing happens and the toast says so — it will not quietly move whatever is behind it. Moving a window you never pointed at is worse than doing nothing.

  • Modal dialogs owned by another window are not moved. v1 only moves top-level windows (the ones you'd see in Alt+Tab); a dialog attached to a window that gets swapped stays behind on the old monitor.

  • Undo is a single step. WindowShuttle keeps exactly one snapshot: the positions from before the last move you made. Pressing undo twice returns you to where you were rather than stepping further back. The automatic off-screen rescue deliberately does not overwrite that snapshot, so unplugging a monitor never costs you the undo you were about to press.

  • Fullscreen applications are skipped by default (a window that exactly fills a monitor with no title bar or thick border — games, video players). Turn off "Skip fullscreen apps" at the bottom of the main window to include them.

  • Windows that aren't responding are skipped, and the CLI says so — the CLI result line reports a "skipped N not-responding window(s)" count, the same way it reports skipped fullscreen apps. The card stays quiet about a move that succeeded: the window moved, and that is the feedback.

  • Windows on another virtual desktop are invisible to WindowShuttle, on purpose. Windows marks a window as "cloaked" while its virtual desktop isn't the active one — the same flag used for suspended UWP apps — and WindowShuttle treats cloaked windows as not there, so they're excluded from every action, including list. This is deliberate, not a bug: gather reaching across desktops to yank a window onto your current screen would be more surprising than just leaving it where it is. Switch to that virtual desktop first if you want to move something on it.

Languages

18 languages — English, 简体中文, 繁體中文, 日本語, 한국어, Español, Français, Deutsch, Italiano, Português, Русский, العربية, हिन्दी, Bahasa Indonesia, Tiếng Việt, ไทย, Türkçe, Nederlands — following the system language by default. Switch it at the bottom of the main window — the change takes effect after a restart (WindowShuttle prompts you).

Build from source

Requires the .NET 10 SDK. Windows only.

dotnet build WindowShuttle.sln -c Debug
dotnet test  tests/WindowShuttle.Core.Tests
dotnet run   --project src/WindowShuttle.App

Two self-check switches are built into the exe. Both run before the single-instance check, so they never disturb an instance already sitting in your tray, and neither one touches your settings.json:

WindowShuttle.exe --smoke              # construct and lay out every window, then exit
WindowShuttle.exe --shots <directory>  # render every window to PNG: 2 themes x 5 languages (incl. RTL Arabic) x 7 real screen sizes

--smoke reports through a marker file at %TEMP%\windowshuttle-smoke.txt (OK, or the exception) rather than an exit code. Why each switch exists is documented in src/WindowShuttle.App/App.SelfCheck.cs.

Publish a single-file build (win-x64):

dotnet publish src/WindowShuttle.App -c Release -p:PublishProfile=win-x64              # standalone, no runtime required
dotnet publish src/WindowShuttle.App -c Release -p:PublishProfile=win-x64-needs-dotnet10  # small, needs the .NET 10 desktop runtime

Every push runs the tests, the --smoke self-check and the CLI exit codes. The other self-check switch, --shots, renders the whole screenshot matrix — it is far too slow for CI, so it stays a local step, listed in docs/manual-checks.md. Pushing a v* tag additionally publishes both builds, writes SHA256SUMS.txt and attaches a build-provenance attestation, so a downloaded asset can be checked with gh attestation verify <file> -R rockbenben/WindowShuttle rather than trusting a checksum that sits on the same page as the file it vouches for.

What no runner can reach — a real UAC prompt, physically unplugging a display, gestures while an elevated window holds the foreground, a key press that synthetic input reports differently from a real finger, and anything judged by eye — is in docs/manual-checks.md, grouped by why each one needs hands. Walk it before tagging.

About the 365 Open Source Plan

Project #033 of the 365 Open Source Plan — one person + AI, 300+ open-source projects in a year.

Submit your idea → · Discord · Telegram

About

Move windows between monitors without dragging — flick one across, or trade two screens whole · UI in 한국어 العربية ไทย हिन्दी | 多屏之间搬窗口不用拖:划一下送走一扇,或整块屏对调

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Contributors

Languages