AppMover sits quietly in the system tray and lists your open windows. Pick one, pick a monitor from the submenu, and it's moved there — centered in the usable work area (taskbar excluded), with its maximized/restored state preserved. No dragging across screens, no admin rights, no bloat.
| ⌨️ Global hotkeys | Win+Shift+Right / Win+Shift+Left move the focused window to the next/previous monitor without opening the tray menu. Configurable — see Configuration. |
| 🧠 "Last used monitor" memory | The submenu entry an app was last moved to is shown checked, per app (matched by executable name), so you can see at a glance where something usually goes. Purely informational — AppMover never moves a window on its own. |
| 🚀 Start with Windows | A checkbox in the tray menu toggles autostart via the per-user registry Run key. No admin rights needed. |
| 👻 Cloaked/UWP filtering | Windows DWM has hidden from the screen (common for suspended/background UWP frame windows on Windows 10/11) are not listed, even though Win32 still reports them as WS_VISIBLE. |
| 🖥️ Virtual desktop awareness | Windows that aren't on the currently active virtual desktop are filtered out, via the public IVirtualDesktopManager COM interface. |
| 🔍 Per-monitor DPI awareness | AppMover declares Per-Monitor-V2 DPI awareness, so positions/sizes are computed in real physical pixels on mixed-DPI multi-monitor setups, with a size-scaling fallback for windows that aren't DPI-aware themselves. |
| ⚡ Event-driven refresh | The window list updates via SetWinEventHook/WM_DISPLAYCHANGE rather than blind polling; a much longer poll (configurable, default 10s) remains only as a safety net. |
| 🔔 Update check | At startup and every 24h, AppMover checks GitHub Releases for a newer version; if one exists, a "Update available: vX.Y.Z" entry appears in the tray menu and opens the release page when clicked. Only runs against tagged builds (see Configuration to disable it). |
Grab the latest release from the Releases page:
AppMover-Setup.exe(recommended) — an Inno Setup installer that installs per-user to%LocalAppData%\Programs\AppMover— no admin rights, no UAC prompt — with a Start Menu entry, an optional desktop shortcut, and a proper uninstaller. It deliberately doesn't offer its own "start with Windows" option: use the one already in AppMover's tray menu instead, so there's only one place that toggles autostart.AppMover-portable.exe— the plain binary, run from anywhere, nothing installed.
Every v* tag produces both artifacts automatically
(.github/workflows/release.yml).
- Windows (the app uses the Win32 API directly and only builds with
GOOS=windows) - Go 1.27+ to build from source
The project has no cgo dependencies, so it cross-compiles cleanly from any platform, e.g. from macOS or Linux:
./build.sh # or: GOOS=windows GOARCH=amd64 go build -o dist/appmover.exe ./cmd/appmoverRun dist/appmover.exe on a Windows machine (or VM) to test it — a native
macOS or Linux binary cannot execute a Windows PE binary.
To build and run on Windows directly:
go build -o appmover.exe ./cmd/appmover
.\appmover.exeAppMover-Setup.exe is built with Inno Setup
(installer/appmover.iss). To build it locally you need Windows and
Inno Setup's ISCC.exe — it's not
something that can be produced by cross-compiling from macOS/Linux:
go build -ldflags="-H=windowsgui" -o dist\appmover.exe .\cmd\appmover
iscc installer\appmover.issNote
We looked at Velopack (a newer installer/auto-update
framework) as an alternative, but its vpk CLI requires the full .NET SDK
regardless of the target app's language — a disproportionate toolchain
addition for a project that otherwise builds with nothing but go build.
Inno Setup is a single small compiler binary, has no runtime dependency,
and is the long-established standard for this kind of small Windows utility.
On first run AppMover creates a config file at
%AppData%\AppMover\config.json with these defaults:
{
"refreshIntervalMs": 10000,
"moveNextMonitorHotkey": "win+shift+right",
"movePrevMonitorHotkey": "win+shift+left",
"maxWindowSlots": 20,
"excludedProcessNames": [],
"excludedTitles": [],
"disableUpdateCheck": false
}| Key | Description |
|---|---|
refreshIntervalMs |
The safety-net poll interval; the menu also refreshes immediately on real window/display changes regardless of this value. |
moveNextMonitorHotkey / movePrevMonitorHotkey |
modifier+...+key, where modifiers are any of win/ctrl/alt/shift and the key is an arrow (left/right/up/down) or a single letter/digit. |
maxWindowSlots |
How many window entries the tray menu pre-allocates; beyond that, a trailing "+N more windows not shown" entry appears instead of silently truncating the list. |
excludedProcessNames / excludedTitles |
Case-insensitive substring filters, e.g. "excludedProcessNames": ["chrome.exe"]. |
disableUpdateCheck |
Set to true to stop AppMover from calling the GitHub releases API to check for updates (see Update check). |
Edit the file and restart AppMover to apply changes. Per-app "last used
monitor" memory is stored separately at %AppData%\AppMover\state.json.
cmd/appmover— application entry point; sets process DPI awareness.rsrc_windows_amd64.syso(generated, checked in) embedsicon.icoas the exe's own Windows resource, so the file icon shown by Explorer, the taskbar, shortcuts, and the installer/uninstaller entry all match the tray icon. Regenerate it after changing the icon with:go run github.com/tc-hib/go-winres@latest simply --icon internal/tray/icon.ico --arch amd64 --manifest none --file-description "AppMover" --product-name "AppMover" --out cmd/appmover/rsrcinternal/tray— system tray icon, menu, refresh, and wiring for hotkeys/autostart/stateinternal/win32— thin wrappers around the Win32 APIs (window/monitor enumeration, positioning, DPI, cloaking) built ongithub.com/gonutz/w32/v2internal/winlist— filters raw window handles down to real, user-visible application windowsinternal/movewindow— the move/resize logic that places a window on a target monitorinternal/layout— pure geometry math (centering/DPI scaling), no Win32 dependency, builds and tests on any platforminternal/msgloop— a dedicated hidden window + Win32 message loop (separate from systray's own, which is private to that library) for global hotkeys,WM_DISPLAYCHANGE, andSetWinEventHook-driven refreshinternal/vdesktop— hand-written COM wrapper aroundIVirtualDesktopManagerfor virtual-desktop filteringinternal/autostart— toggles the per-user Run registry keyinternal/config,internal/state,internal/applog— JSON settings/memory persistence and the rotating debug log, pure Go, no Win32 dependencyinternal/update,internal/version— checks GitHub Releases for a newer version;version.Versionis set at build time via-ldflags -X(see.github/workflows/release.yml) and stays"dev"otherwise, which disables the checkinstaller/appmover.iss— Inno Setup script forAppMover-Setup.exe(see Download)
Packages with no Win32 dependency (internal/layout, internal/config,
internal/state, internal/applog, internal/update) have real unit
tests that run on any platform:
go test ./internal/layout/... ./internal/config/... ./internal/state/... ./internal/applog/... ./internal/update/...Everything else is Windows-only by nature (raw syscalls, COM, a native message loop) and can only be verified by compiling for the target and exercising it on real Windows:
GOOS=windows GOARCH=amd64 go vet ./...
GOOS=windows GOARCH=amd64 go build ./...CI (.github/workflows/ci.yml) runs both of the above on every push/PR;
.github/workflows/release.yml builds and publishes appmover.exe on
v* tags.
At runtime AppMover writes a debug log to
%LocalAppData%\AppMover\logs\appmover.log, rotating it (one .log.old
backup) once it passes 5MB. This deliberately isn't next to the
executable: AppMover builds with -H=windowsgui (no console, so stderr
would be invisible anyway), and the install directory may not be writable
by a regular user once properly installed. If even that location can't be
opened, it falls back to %Temp%\AppMover\appmover.log.
GPL-3.0 — see LICENSE.
This project depends on:
fyne.io/systray— Apache-2.0github.com/gonutz/w32/v2— MITgithub.com/godbus/dbus/v5,golang.org/x/sys— BSD-3-Clause
All of these are permissive licenses compatible with distribution under GPL-3.0.