feat(engineering): add desktop-manager skill for Windows desktop automation

Adds a new Windows-only skill that enables full desktop control from Claude
Code via PowerShell and Win32 API — no third-party tools or admin rights needed.

- window_manager.py: list, snap (12 positions), tile (2col/3col/grid4), move, show/focus
- process_manager.py: list (windowed/all), launch by name or path, kill by name or PID
- desktop_snapshot.py: save/restore named layouts to ~/.desktop-snapshots/ with title fallback
- references/powershell-window-api.md: Win32 function reference, multi-monitor, DPI, virtual desktops

All scripts: stdlib-only, argparse CLI, --json flag, exit codes 0/1/2
Validation: skill_validator 100/100, security audit PASS (0 CRITICAL/HIGH), 180-line SKILL.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Farah Peach 2026-05-19 07:45:39 +08:00
parent 29b4b141d0
commit 37b21bf8f1
5 changed files with 1337 additions and 0 deletions

View file

@ -0,0 +1,243 @@
---
name: "desktop-manager"
description: "Windows desktop automation skill for Claude Code. Manages windows, processes, and layouts via PowerShell and Win32 API. Use when: user wants to tile or snap windows, arrange apps side by side, launch or kill processes, list what's open, save a desktop layout, or restore a named workspace. Trigger phrases: 'tile my windows', 'snap VS Code to the left', 'launch notepad', 'kill Chrome', 'list open windows', 'save my coding layout', 'restore my layout', 'arrange windows 2-column', 'what's running', 'close all instances of X'."
license: MIT
metadata:
version: 1.0.0
author: VSD Communications
category: engineering
updated: 2026-05-19
platform: windows
---
# Desktop Manager
> Tile. Snap. Launch. Restore. Full Windows desktop control from Claude Code.
PowerShell + Win32 API automation for Windows 10/11. No third-party tools. No admin rights required for standard window operations.
---
## Slash Commands
| Command | What it does |
|---------|-------------|
| `/desktop:windows` | List all open windows with HWND, PID, and geometry |
| `/desktop:tile` | Tile foreground windows in 2-column, 3-column, or 2×2 grid |
| `/desktop:snap` | Snap a specific window to a screen position |
| `/desktop:processes` | List, launch, or kill processes |
| `/desktop:snapshot` | Save or restore a named window layout |
---
## When This Skill Activates
Recognize these patterns:
- "Tile my windows" / "arrange side by side" / "split screen"
- "Snap [app] to the left / right / corner"
- "Launch [app]" / "open [app]" / "start [app]"
- "Kill [process]" / "close all [app] windows"
- "What's running?" / "list open windows"
- "Save my layout as [name]" / "restore [name] layout"
- Any request involving window positions, screen arrangements, or process control
---
## Workflow
### `/desktop:windows` — List & Inspect Windows
1. Run the window lister to get current state:
```bash
python scripts/window_manager.py list
```
2. Output shows: HWND (handle), PID, W×H dimensions, process name, window title.
3. Use HWND values from this output for `snap`, `move`, and `show` commands.
4. To get JSON for further processing:
```bash
python scripts/window_manager.py list --json
```
### `/desktop:tile` — Tile Windows
Identify which layout the user wants, then apply it. Tiling acts on the first N visible
windows in the order returned by `list`.
```bash
# Side-by-side (first 2 windows)
python scripts/window_manager.py tile --layout 2col
# Three columns (first 3 windows)
python scripts/window_manager.py tile --layout 3col
# 2×2 grid (first 4 windows)
python scripts/window_manager.py tile --layout grid4
```
If the user wants a specific app in a specific position, use `snap` instead.
### `/desktop:snap` — Snap a Single Window
1. Run `list` to get the HWND for the target window.
2. Apply snap position:
```bash
python scripts/window_manager.py snap --hwnd <HWND> --pos left-half
python scripts/window_manager.py snap --hwnd <HWND> --pos top-right
```
3. For exact pixel control:
```bash
python scripts/window_manager.py move --hwnd <HWND> --x 0 --y 0 --w 960 --h 1080
```
4. To change window state:
```bash
python scripts/window_manager.py show --hwnd <HWND> --state maximize
python scripts/window_manager.py show --hwnd <HWND> --state minimize
python scripts/window_manager.py show --hwnd <HWND> --state focus
```
**Available snap positions:** `left-half`, `right-half`, `top-half`, `bot-half`,
`top-left`, `top-right`, `bot-left`, `bot-right`, `col-1-of-3`, `col-2-of-3`,
`col-3-of-3`, `maximize`
### `/desktop:processes` — Process Management
**List windowed processes (default):**
```bash
python scripts/process_manager.py list
python scripts/process_manager.py list --filter chrome
python scripts/process_manager.py list --all # include background processes
```
**Launch an application:**
```bash
python scripts/process_manager.py launch --app notepad
python scripts/process_manager.py launch --app code --args "--new-window"
python scripts/process_manager.py launch --path "C:/Program Files/app/app.exe"
```
**Terminate a process:**
```bash
python scripts/process_manager.py kill --name chrome # kills all chrome.exe
python scripts/process_manager.py kill --pid 12345
```
### `/desktop:snapshot` — Save & Restore Layouts
Save the current window layout before switching contexts:
```bash
python scripts/desktop_snapshot.py save --name coding
python scripts/desktop_snapshot.py save --name meetings
```
Restore when returning to a context:
```bash
python scripts/desktop_snapshot.py restore --name coding
```
List and manage snapshots:
```bash
python scripts/desktop_snapshot.py list
python scripts/desktop_snapshot.py delete --name old-layout
```
Snapshots are stored as JSON in `~/.desktop-snapshots/`. Restore matches windows
by title (exact, then prefix), then by process name as fallback.
---
## Tooling
### `scripts/window_manager.py`
Controls window positions, sizes, states, and layout presets via Win32 API.
| Subcommand | Purpose |
|-----------|---------|
| `list` | Enumerate visible windows with HWND and geometry |
| `move` | Set exact x, y, width, height for a window |
| `snap` | Snap to a named position (left-half, top-right, etc.) |
| `tile` | Tile first N windows: 2col, 3col, grid4 |
| `show` | Minimize, maximize, restore, or focus a window |
All subcommands support `--json` for structured output.
### `scripts/process_manager.py`
Lists, launches, and terminates Windows processes.
| Subcommand | Purpose |
|-----------|---------|
| `list` | Windowed processes by default; `--all` for background; `--filter NAME` to search |
| `launch` | Start an app by name (`--app`) or full path (`--path`); optional `--args` |
| `kill` | Stop by `--name` (all matching) or `--pid` |
Process names are sanitized to prevent PowerShell injection.
### `scripts/desktop_snapshot.py`
Saves and restores full window layout snapshots as named JSON files in `~/.desktop-snapshots/`.
| Subcommand | Purpose |
|-----------|---------|
| `save --name X` | Capture current layout |
| `restore --name X` | Re-apply saved positions (matches by title, then process name) |
| `list` | Show all saved snapshots with creation date and window count |
| `delete --name X` | Remove a snapshot |
---
## Snap Position Quick Reference
| Position | Description |
|----------|-------------|
| `left-half` | Left 50% of screen |
| `right-half` | Right 50% of screen |
| `top-half` / `bot-half` | Top or bottom 50% |
| `top-left` / `top-right` | Top quadrant |
| `bot-left` / `bot-right` | Bottom quadrant |
| `col-1-of-3` / `col-2-of-3` / `col-3-of-3` | Three equal columns |
| `maximize` | Full working area |
Screen dimensions are read from `System.Windows.Forms.Screen.PrimaryScreen.WorkingArea`
(excludes taskbar). All measurements are in pixels.
---
## Proactive Triggers
Flag these without being asked:
- **Multiple windows open** when user mentions switching context → suggest saving a snapshot first.
- **User references "left / right side"** for windows → `snap` is the right tool, not `tile`.
- **User wants to "reopen my setup"** → check if a matching snapshot exists with `list`.
- **User asks "what PID is X"** → `process_manager.py list --filter X` gives both PID and HWND.
- **Tile produces wrong results** → remind user that `tile` uses window order from `list`; run `list` first to confirm ordering.
---
## Requirements
- Windows 10 or Windows 11
- PowerShell 5.1+ (default on both; no install needed)
- Python 3.9+ (standard library only — no pip install required)
- No admin rights required for window positioning and process listing
---
## Additional Resources
### Reference Files
- **`references/powershell-window-api.md`** — Win32 API functions used, `Add-Type` patterns,
and how to extend scripts with additional Win32 calls (full-screen enumeration, multi-monitor,
DPI awareness, virtual desktops)
---
## Related Skills
- **browser-automation** — Automate browser-specific interactions beyond window positioning.
- **env-secrets-manager** — Manage environment variables set before launching apps.
- **git-worktree-manager** — Pairs well: open each worktree in a snapped window.
- **docker-development** — Launch and monitor Docker Desktop alongside dev windows.

View file

@ -0,0 +1,294 @@
# PowerShell Window API Reference
Reference for the Win32 API functions used in desktop-manager scripts and how to extend
them for advanced use cases.
---
## Core Pattern: `Add-Type` for Win32
PowerShell has no native window management cmdlets. All window control goes through
`Add-Type` to compile and expose C# P/Invoke signatures at runtime:
```powershell
Add-Type @"
using System;
using System.Runtime.InteropServices;
public class WinAPI {
[DllImport("user32.dll")]
public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[StructLayout(LayoutKind.Sequential)]
public struct RECT { public int left, top, right, bottom; }
}
"@ -ErrorAction SilentlyContinue # SilentlyContinue skips error if type already loaded
```
Use `-ErrorAction SilentlyContinue` on every `Add-Type` call — if the type was already
compiled in the same PS session it will throw; this suppresses that error harmlessly.
---
## Win32 Functions Used
### GetWindowRect
```csharp
[DllImport("user32.dll")]
public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
```
Returns the bounding rectangle of the window **including borders and title bar** in
screen coordinates. For borderless/maximized windows, `left` and `top` may be negative
(Windows 10+ extends invisible borders off-screen for shadow rendering).
**Usage:**
```powershell
$rect = New-Object WinAPI+RECT
[WinAPI]::GetWindowRect($hwnd, [ref]$rect)
$width = $rect.right - $rect.left
$height = $rect.bottom - $rect.top
```
---
### SetWindowPos
```csharp
[DllImport("user32.dll")]
public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter,
int X, int Y, int cx, int cy, uint uFlags);
```
Moves and/or resizes a window. The scripts use `uFlags = 0x14`:
- `0x0004` — `SWP_NOZORDER`: keep current Z-order (don't bring to front)
- `0x0010` — `SWP_NOACTIVATE`: don't steal focus
**Common flag combinations:**
| uFlags | Effect |
|--------|--------|
| `0x0014` | Move/resize without changing Z-order or focus (recommended) |
| `0x0001` | `SWP_NOSIZE` — move only, keep current size |
| `0x0002` | `SWP_NOMOVE` — resize only, keep current position |
| `0x0040` | `SWP_SHOWWINDOW` — make visible if hidden |
**hWndInsertAfter special values:**
| Value | Meaning |
|-------|---------|
| `[IntPtr]::Zero` | No Z-order change |
| `[IntPtr](-1)` | `HWND_TOPMOST` — always on top |
| `[IntPtr](-2)` | `HWND_NOTOPMOST` — remove always-on-top |
---
### ShowWindow
```csharp
[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
```
**nCmdShow values:**
| Value | Constant | Effect |
|-------|----------|--------|
| `0` | `SW_HIDE` | Hide window |
| `1` | `SW_SHOWNORMAL` | Show/restore normal |
| `2` | `SW_MINIMIZE` | Minimize to taskbar |
| `3` | `SW_MAXIMIZE` | Maximize |
| `5` | `SW_SHOW` | Show at current size/position |
| `6` | `SW_MINIMIZE` (alt) | Minimize without activating |
| `9` | `SW_RESTORE` | Restore from min/max |
---
### SetForegroundWindow / GetForegroundWindow
```csharp
[DllImport("user32.dll")]
public static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern IntPtr GetForegroundWindow();
```
`SetForegroundWindow` brings a window to the front and gives it keyboard focus.
Windows may silently deny this if the calling process is not the foreground process —
use `AttachThreadInput` if reliable focusing is required (see advanced section).
**Get the currently focused window:**
```powershell
$hwnd = [WinAPI]::GetForegroundWindow()
```
---
### GetWindowThreadProcessId
```csharp
[DllImport("user32.dll")]
public static extern int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);
```
Maps HWND → PID. Useful when you have an HWND from enumeration and need the process:
```powershell
$pid = 0
[WinAPI]::GetWindowThreadProcessId($hwnd, [ref]$pid)
```
---
## Getting Windows: Three Approaches
### 1. `Get-Process` (used in scripts — simplest)
```powershell
Get-Process | Where-Object {
$_.MainWindowHandle -ne [IntPtr]::Zero -and $_.MainWindowTitle -ne ''
}
```
Returns **one window per process** (the `MainWindow`). Covers 99% of desktop management
needs. Does not enumerate secondary windows (popup dialogs, tool windows).
### 2. `EnumWindows` (all top-level windows)
```powershell
Add-Type @"
using System;
using System.Runtime.InteropServices;
using System.Collections.Generic;
public class WinEnum {
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
[DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc p, IntPtr l);
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);
[DllImport("user32.dll")] public static extern int GetWindowText(IntPtr h,
System.Text.StringBuilder s, int n);
public static List<long> GetWindows() {
var list = new List<long>();
EnumWindows((h, l) => { if (IsWindowVisible(h)) list.Add(h.ToInt64()); return true; }, IntPtr.Zero);
return list;
}
}
"@ -ErrorAction SilentlyContinue
$handles = [WinEnum]::GetWindows()
```
Use this when you need child windows, tool windows, or multiple windows per process.
### 3. `FindWindow` (by class or title)
```csharp
[DllImport("user32.dll", CharSet=CharSet.Unicode)]
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
```
Finds a specific window directly without enumeration:
```powershell
# Find Notepad by class name
$hwnd = [WinAPI]::FindWindow("Notepad", $null)
# Find by exact title
$hwnd = [WinAPI]::FindWindow($null, "Untitled - Notepad")
```
---
## Multi-Monitor Support
Get all monitors (not just primary):
```powershell
Add-Type -AssemblyName System.Windows.Forms
$screens = [System.Windows.Forms.Screen]::AllScreens
foreach ($s in $screens) {
$wa = $s.WorkingArea
Write-Host "$($s.DeviceName): $($wa.Width)x$($wa.Height) at ($($wa.X),$($wa.Y))"
}
```
To position a window on a secondary monitor, use its `WorkingArea.X` as the X offset:
```powershell
$monitor2 = [System.Windows.Forms.Screen]::AllScreens | Where-Object { -not $_.Primary } | Select-Object -First 1
$x = $monitor2.WorkingArea.X # e.g. 1920 for a monitor to the right
$y = $monitor2.WorkingArea.Y
```
---
## DPI Awareness
On high-DPI displays (125%, 150%, 200% scaling), `GetWindowRect` returns physical pixels
but the coordinates passed to `SetWindowPos` must also be physical pixels — so they match.
No conversion is needed unless mixing with `System.Windows.Forms` which returns logical pixels.
To get the screen DPI:
```powershell
Add-Type -AssemblyName System.Drawing
$g = [System.Drawing.Graphics]::FromHwnd([IntPtr]::Zero)
$dpi = $g.DpiX # typically 96 (100%), 120 (125%), 144 (150%), 192 (200%)
$g.Dispose()
```
---
## Virtual Desktops (Windows 10/11)
No native PowerShell cmdlets exist. Options:
**Option 1: VirtualDesktop PowerShell module (third-party)**
```powershell
Install-Module -Name VirtualDesktop -Scope CurrentUser
Get-DesktopList
Switch-Desktop -Desktop 1
```
**Option 2: COM interface (no install, complex)**
```powershell
$shell = New-Object -ComObject "Shell.Application"
# Limited API — Switch-to only, not create/query
```
**Option 3: Keyboard shortcuts via SendKeys**
```powershell
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.SendKeys]::SendWait("^#{RIGHT}") # Win+Ctrl+Right
```
For production virtual desktop management, the `VirtualDesktop` module is the most
reliable option. Requires one-time install per machine.
---
## Common Issues
### Window positions are wrong / off by a few pixels
Windows 10/11 adds an invisible 8px border to windows for shadow rendering. `GetWindowRect`
includes this border, so `left` may be `-8` for a maximized window. This is expected — the
scripts account for it by using the values as-is, which places windows correctly.
### SetWindowPos has no effect on maximized windows
Call `ShowWindow(hwnd, 9)` (SW_RESTORE) first to un-maximize the window before repositioning.
### Focus stealing prevention
Windows prevents background processes from calling `SetForegroundWindow` in some cases.
If reliable focus is needed, use `AttachThreadInput` to attach to the foreground thread first:
```csharp
[DllImport("user32.dll")]
public static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);
[DllImport("kernel32.dll")]
public static extern uint GetCurrentThreadId();
[DllImport("user32.dll")]
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid);
```
### Admin processes
`SetWindowPos` will fail silently on windows owned by processes running as Administrator
if the calling process is not elevated. Common example: Task Manager. This is a Windows
security restriction — no workaround without matching elevation.

View file

@ -0,0 +1,298 @@
#!/usr/bin/env python3
"""
desktop-manager: Desktop Snapshot
Save and restore named window layout snapshots. Captures the HWND, title,
process name, and geometry of every visible window, keyed by snapshot name.
Snapshots are stored as JSON files in ~/.desktop-snapshots/ and restored by
matching saved window titles to currently open windows.
Usage:
python scripts/desktop_snapshot.py save --name coding
python scripts/desktop_snapshot.py restore --name coding
python scripts/desktop_snapshot.py list
python scripts/desktop_snapshot.py list --json
python scripts/desktop_snapshot.py delete --name coding
"""
import argparse
import json
import re
import subprocess
import sys
from datetime import datetime
from pathlib import Path
SNAPSHOT_DIR = Path.home() / ".desktop-snapshots"
# Win32 type definition reused from window_manager pattern
WIN32_TYPE = r"""
Add-Type @"
using System;
using System.Runtime.InteropServices;
public class WinAPI {
[DllImport("user32.dll")]
public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[DllImport("user32.dll")]
public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter,
int X, int Y, int cx, int cy, uint uFlags);
[StructLayout(LayoutKind.Sequential)]
public struct RECT { public int left, top, right, bottom; }
}
"@ -ErrorAction SilentlyContinue
"""
SWP_FLAGS = "0x14" # SWP_NOZORDER | SWP_NOACTIVATE
def run_ps(script: str) -> str:
result = subprocess.run(
["powershell", "-NoProfile", "-NonInteractive", "-Command", script],
capture_output=True,
text=True,
)
if result.returncode not in (0, 1) and result.stderr.strip():
raise RuntimeError(result.stderr.strip())
return result.stdout.strip()
def _sanitize_name(name: str) -> str:
if not re.match(r"^[\w\-]+$", name):
raise ValueError(
f"Snapshot name '{name}': use alphanumeric, hyphen, or underscore only"
)
return name
def _snapshot_path(name: str) -> Path:
return SNAPSHOT_DIR / f"{name}.json"
def capture_windows() -> list[dict]:
"""Return current visible window geometries via PowerShell."""
ps = (
WIN32_TYPE
+ r"""
$wins = Get-Process | Where-Object {
$_.MainWindowHandle -ne [IntPtr]::Zero -and $_.MainWindowTitle -ne ''
} | ForEach-Object {
$hwnd = $_.MainWindowHandle
$rect = New-Object WinAPI+RECT
[void][WinAPI]::GetWindowRect($hwnd, [ref]$rect)
[PSCustomObject]@{
hwnd = $hwnd.ToInt64()
title = $_.MainWindowTitle
name = $_.ProcessName
pid = $_.Id
x = $rect.left
y = $rect.top
w = $rect.right - $rect.left
h = $rect.bottom - $rect.top
}
}
if ($wins) { $wins | ConvertTo-Json -Compress } else { "[]" }
"""
)
out = run_ps(ps)
if not out or out == "[]":
return []
data = json.loads(out)
return data if isinstance(data, list) else [data]
def set_window_pos(hwnd: int, x: int, y: int, w: int, h: int) -> None:
ps = WIN32_TYPE + (
f"[WinAPI]::SetWindowPos([IntPtr]{hwnd}, [IntPtr]::Zero,"
f" {x}, {y}, {w}, {h}, {SWP_FLAGS})"
)
run_ps(ps)
# ── Snapshot operations ──────────────────────────────────────────────────────
def save_snapshot(name: str) -> dict:
"""Capture current window layout and write to ~/.desktop-snapshots/{name}.json."""
SNAPSHOT_DIR.mkdir(parents=True, exist_ok=True)
windows = capture_windows()
if not windows:
raise RuntimeError("No visible windows found to snapshot.")
payload = {
"name": name,
"created": datetime.now().isoformat(timespec="seconds"),
"count": len(windows),
"windows": windows,
}
_snapshot_path(name).write_text(json.dumps(payload, indent=2))
return payload
def restore_snapshot(name: str) -> dict:
"""
Restore a saved layout. Matches saved windows to current windows by title
(exact match first, then substring). Unmatched entries are skipped.
"""
path = _snapshot_path(name)
if not path.exists():
raise FileNotFoundError(
f"No snapshot named '{name}'. Run 'list' to see available snapshots."
)
saved = json.loads(path.read_text())
current = capture_windows()
# Build lookup: title -> hwnd for current windows
exact: dict[str, int] = {w["title"]: w["hwnd"] for w in current}
partial: list[dict] = current # for fallback substring match
restored, skipped = [], []
for sw in saved["windows"]:
hwnd = exact.get(sw["title"])
if hwnd is None:
# Fallback: find first current window whose title contains saved title prefix
match = next(
(
c
for c in partial
if sw["title"][:30] in c["title"] or c["name"] == sw["name"]
),
None,
)
hwnd = match["hwnd"] if match else None
if hwnd:
set_window_pos(hwnd, sw["x"], sw["y"], sw["w"], sw["h"])
restored.append({"title": sw["title"], "hwnd": hwnd})
else:
skipped.append(sw["title"])
return {
"snapshot": name,
"restored": len(restored),
"skipped": len(skipped),
"details": {"restored": restored, "skipped": skipped},
}
def list_snapshots() -> list[dict]:
"""Return metadata for all saved snapshots."""
SNAPSHOT_DIR.mkdir(parents=True, exist_ok=True)
snapshots = []
for p in sorted(SNAPSHOT_DIR.glob("*.json")):
try:
data = json.loads(p.read_text())
snapshots.append(
{
"name": data.get("name", p.stem),
"created": data.get("created", "unknown"),
"count": data.get("count", 0),
}
)
except (json.JSONDecodeError, KeyError):
snapshots.append({"name": p.stem, "created": "?", "count": "?"})
return snapshots
def delete_snapshot(name: str) -> dict:
path = _snapshot_path(name)
if not path.exists():
raise FileNotFoundError(f"No snapshot named '{name}'.")
path.unlink()
return {"deleted": name}
# ── CLI ─────────────────────────────────────────────────────────────────────
def main() -> None:
parser = argparse.ArgumentParser(
description="Desktop snapshot — save and restore window layout presets"
)
sub = parser.add_subparsers(dest="cmd")
p_save = sub.add_parser("save", help="Save current window layout")
p_save.add_argument(
"--name", required=True, help="Snapshot name (alphanumeric/hyphen)"
)
p_save.add_argument("--json", action="store_true")
p_restore = sub.add_parser("restore", help="Restore a saved layout")
p_restore.add_argument("--name", required=True)
p_restore.add_argument("--json", action="store_true")
p_list = sub.add_parser("list", help="List saved snapshots")
p_list.add_argument("--json", action="store_true")
p_delete = sub.add_parser("delete", help="Delete a saved snapshot")
p_delete.add_argument("--name", required=True)
p_delete.add_argument("--json", action="store_true")
args = parser.parse_args()
if not args.cmd:
parser.print_help()
sys.exit(2)
use_json = getattr(args, "json", False)
name = _sanitize_name(getattr(args, "name", "")) if hasattr(args, "name") else ""
try:
if args.cmd == "save":
result = save_snapshot(name)
if use_json:
print(json.dumps(result, indent=2))
else:
print(
f"Saved '{name}': {result['count']} windows → {_snapshot_path(name)}"
)
elif args.cmd == "restore":
result = restore_snapshot(name)
if use_json:
print(json.dumps(result, indent=2))
else:
print(
f"Restored '{name}': {result['restored']} windows placed"
f", {result['skipped']} skipped"
)
if result["details"]["skipped"]:
for t in result["details"]["skipped"]:
print(f" skipped: {t[:70]}")
elif args.cmd == "list":
snaps = list_snapshots()
if use_json:
print(json.dumps(snaps, indent=2))
else:
if not snaps:
print(f"No snapshots found in {SNAPSHOT_DIR}")
else:
print(f"{'Name':<25} {'Created':<22} Windows")
print("─" * 55)
for s in snaps:
print(f"{s['name']:<25} {s['created']:<22} {s['count']}")
elif args.cmd == "delete":
result = delete_snapshot(name)
if use_json:
print(json.dumps(result))
else:
print(f"Deleted snapshot: {name}")
except ValueError as exc:
print(f"Input error: {exc}", file=sys.stderr)
sys.exit(2)
except FileNotFoundError as exc:
if use_json:
print(json.dumps({"error": str(exc)}))
else:
print(f"Not found: {exc}", file=sys.stderr)
sys.exit(1)
except Exception as exc:
if use_json:
print(json.dumps({"error": str(exc)}))
else:
print(f"Error: {exc}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,200 @@
#!/usr/bin/env python3
"""
desktop-manager: Process Manager
List running processes, launch applications, and terminate processes on Windows
via PowerShell. Filters to windowed applications by default.
Usage:
python scripts/process_manager.py list
python scripts/process_manager.py list --all
python scripts/process_manager.py list --filter chrome --json
python scripts/process_manager.py launch --app notepad
python scripts/process_manager.py launch --path "C:/Windows/System32/notepad.exe"
python scripts/process_manager.py launch --app code --args "--new-window C:/project"
python scripts/process_manager.py kill --name notepad
python scripts/process_manager.py kill --pid 1234
python scripts/process_manager.py kill --name notepad --json
"""
import argparse
import json
import re
import subprocess
import sys
def run_ps(script: str) -> str:
"""Run a PowerShell script and return stdout. Raises RuntimeError on failure."""
result = subprocess.run(
["powershell", "-NoProfile", "-NonInteractive", "-Command", script],
capture_output=True,
text=True,
)
if result.returncode not in (0, 1) and result.stderr.strip():
raise RuntimeError(result.stderr.strip())
return result.stdout.strip()
def _sanitize_name(name: str) -> str:
"""Allow only safe characters in process names to prevent PS injection."""
if not re.match(r"^[\w.\-]+$", name):
raise ValueError(
f"Invalid process name '{name}': use alphanumeric, dot, or hyphen only"
)
return name
def list_processes(filter_name: str = "", all_procs: bool = False) -> list[dict]:
"""
Return running processes. Without --all, returns only windowed processes.
Optionally filter by name substring.
"""
where = ""
if not all_procs:
where = "| Where-Object { $_.MainWindowTitle -ne '' }"
if filter_name:
safe = _sanitize_name(filter_name)
where += f" | Where-Object {{ $_.ProcessName -like '*{safe}*' }}"
ps = f"""
$procs = Get-Process {where} | Select-Object -Property `
Id, ProcessName, MainWindowTitle,
@{{n='CpuSec'; e={{[math]::Round($_.CPU, 1)}}}},
@{{n='MemMB'; e={{[math]::Round($_.WorkingSet64 / 1MB, 1)}}}}
if ($procs) {{ $procs | ConvertTo-Json -Compress }} else {{ "[]" }}
"""
out = run_ps(ps)
if not out or out == "[]":
return []
data = json.loads(out)
return data if isinstance(data, list) else [data]
def launch_app(app: str = "", path: str = "", args: str = "") -> dict:
"""
Launch an application by name (resolved via PATH / Windows shell) or full path.
Optional args string is passed as argument list.
"""
if path:
target = path.replace("'", "\\'")
cmd = f"Start-Process '{target}'"
else:
safe = _sanitize_name(app)
cmd = f"Start-Process '{safe}'"
if args:
safe_args = args.replace("'", "\\'")
cmd += f" -ArgumentList '{safe_args}'"
run_ps(cmd)
return {"launched": path or app, "args": args}
def kill_process(name: str = "", pid: int = 0) -> dict:
"""
Terminate one or more processes by name or PID.
Name supports wildcard (e.g. 'chrome' kills all chrome.exe instances).
"""
if pid:
ps = f"Stop-Process -Id {int(pid)} -Force -ErrorAction Stop"
run_ps(ps)
return {"killed": {"pid": pid}}
else:
safe = _sanitize_name(name)
ps = f"Stop-Process -Name '{safe}' -Force -ErrorAction Stop"
run_ps(ps)
return {"killed": {"name": name}}
# ── CLI ─────────────────────────────────────────────────────────────────────
def main() -> None:
parser = argparse.ArgumentParser(
description="Process manager — list, launch, and kill Windows processes"
)
sub = parser.add_subparsers(dest="cmd")
p_list = sub.add_parser("list", help="List running processes")
p_list.add_argument(
"--filter", default="", metavar="NAME", help="Filter by process name substring"
)
p_list.add_argument(
"--all", action="store_true", help="Include background processes (no window)"
)
p_list.add_argument("--json", action="store_true")
p_launch = sub.add_parser("launch", help="Launch an application")
grp = p_launch.add_mutually_exclusive_group(required=True)
grp.add_argument(
"--app", help="App name (must be in PATH or a known Windows command)"
)
grp.add_argument("--path", help="Full path to executable")
p_launch.add_argument(
"--args", default="", help="Arguments to pass to the application"
)
p_launch.add_argument("--json", action="store_true")
p_kill = sub.add_parser("kill", help="Terminate a process")
grp2 = p_kill.add_mutually_exclusive_group(required=True)
grp2.add_argument("--name", help="Process name (all matching instances)")
grp2.add_argument("--pid", type=int, help="Process ID")
p_kill.add_argument("--json", action="store_true")
args = parser.parse_args()
if not args.cmd:
parser.print_help()
sys.exit(2)
use_json = getattr(args, "json", False)
try:
if args.cmd == "list":
procs = list_processes(args.filter, getattr(args, "all", False))
if use_json:
print(json.dumps(procs, indent=2))
else:
print(
f"{'PID':<8} {'CPU(s)':<9} {'Mem(MB)':<10} {'Name':<24} Window Title"
)
print("─" * 95)
for p in procs:
title = (p.get("MainWindowTitle") or "")[:42]
print(
f"{p.get('Id', ''):<8} {p.get('CpuSec', ''):<9}"
f" {p.get('MemMB', ''):<10} {p.get('ProcessName', '')[:23]:<24} {title}"
)
if not procs:
print("No matching processes found.")
elif args.cmd == "launch":
result = launch_app(args.app or "", args.path or "", args.args)
if use_json:
print(json.dumps(result))
else:
msg = f"Launched: {result['launched']}"
if result["args"]:
msg += f" {result['args']}"
print(msg)
elif args.cmd == "kill":
result = kill_process(args.name or "", args.pid or 0)
if use_json:
print(json.dumps(result))
else:
target = args.name or str(args.pid)
print(f"Killed: {target}")
except ValueError as exc:
print(f"Input error: {exc}", file=sys.stderr)
sys.exit(2)
except Exception as exc:
if use_json:
print(json.dumps({"error": str(exc)}))
else:
print(f"Error: {exc}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,302 @@
#!/usr/bin/env python3
"""
desktop-manager: Window Manager
List, move, resize, snap, and tile windows on Windows 10/11 using PowerShell
and Win32 API. Requires PowerShell 5.1+ (default on Windows 10/11).
Usage:
python scripts/window_manager.py list
python scripts/window_manager.py list --json
python scripts/window_manager.py snap --hwnd 1234567 --pos left-half
python scripts/window_manager.py tile --layout 2col
python scripts/window_manager.py tile --layout 3col
python scripts/window_manager.py tile --layout grid4
python scripts/window_manager.py move --hwnd 1234567 --x 0 --y 0 --w 960 --h 1080
python scripts/window_manager.py show --hwnd 1234567 --state maximize
python scripts/window_manager.py show --hwnd 1234567 --state focus
"""
import argparse
import json
import subprocess
import sys
# ── Win32 type definition (loaded once per PS invocation) ───────────────────
WIN32_TYPE = r"""
Add-Type @"
using System;
using System.Runtime.InteropServices;
public class WinAPI {
[DllImport("user32.dll")]
public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[DllImport("user32.dll")]
public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter,
int X, int Y, int cx, int cy, uint uFlags);
[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")]
public static extern bool SetForegroundWindow(IntPtr hWnd);
[StructLayout(LayoutKind.Sequential)]
public struct RECT { public int left, top, right, bottom; }
}
"@ -ErrorAction SilentlyContinue
"""
# uFlags: SWP_NOZORDER (0x4) | SWP_NOACTIVATE (0x10) = 0x14
SWP_FLAGS = "0x14"
def run_ps(script: str) -> str:
"""Run PowerShell script and return stdout. Raises RuntimeError on failure."""
result = subprocess.run(
["powershell", "-NoProfile", "-NonInteractive", "-Command", script],
capture_output=True,
text=True,
)
if result.returncode != 0 and result.stderr.strip():
raise RuntimeError(result.stderr.strip())
return result.stdout.strip()
def get_screen_size() -> tuple[int, int]:
"""Return (width, height) of primary monitor working area."""
ps = """
Add-Type -AssemblyName System.Windows.Forms
$s = [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea
Write-Output "$($s.Width) $($s.Height)"
"""
out = run_ps(ps)
w, h = out.split()
return int(w), int(h)
def list_windows() -> list[dict]:
"""Return visible windows (main window per process) with position and size."""
ps = (
WIN32_TYPE
+ r"""
$wins = Get-Process | Where-Object {
$_.MainWindowHandle -ne [IntPtr]::Zero -and $_.MainWindowTitle -ne ''
} | ForEach-Object {
$hwnd = $_.MainWindowHandle
$rect = New-Object WinAPI+RECT
[void][WinAPI]::GetWindowRect($hwnd, [ref]$rect)
[PSCustomObject]@{
hwnd = $hwnd.ToInt64()
title = $_.MainWindowTitle
pid = $_.Id
name = $_.ProcessName
x = $rect.left
y = $rect.top
w = $rect.right - $rect.left
h = $rect.bottom - $rect.top
}
}
if ($wins) { $wins | ConvertTo-Json -Compress } else { "[]" }
"""
)
out = run_ps(ps)
if not out or out == "[]":
return []
data = json.loads(out)
return data if isinstance(data, list) else [data]
def set_window_pos(hwnd: int, x: int, y: int, w: int, h: int) -> None:
"""Move and resize a window without changing its Z-order."""
ps = (
WIN32_TYPE
+ f"""
[WinAPI]::SetWindowPos([IntPtr]{hwnd}, [IntPtr]::Zero, {x}, {y}, {w}, {h}, {SWP_FLAGS})
"""
)
run_ps(ps)
def show_window(hwnd: int, state: int) -> None:
"""Change window visibility state. SW_MINIMIZE=2, SW_MAXIMIZE=3, SW_RESTORE=9."""
ps = WIN32_TYPE + f"[WinAPI]::ShowWindow([IntPtr]{hwnd}, {state})"
run_ps(ps)
def focus_window(hwnd: int) -> None:
"""Bring window to foreground."""
ps = WIN32_TYPE + f"[WinAPI]::SetForegroundWindow([IntPtr]{hwnd})"
run_ps(ps)
# ── Layout presets (returns x, y, w, h given screen w, h) ──────────────────
SNAP_POSITIONS: dict[str, callable] = {
"left-half": lambda sw, sh: (0, 0, sw // 2, sh),
"right-half": lambda sw, sh: (sw // 2, 0, sw // 2, sh),
"top-half": lambda sw, sh: (0, 0, sw, sh // 2),
"bot-half": lambda sw, sh: (0, sh // 2, sw, sh // 2),
"top-left": lambda sw, sh: (0, 0, sw // 2, sh // 2),
"top-right": lambda sw, sh: (sw // 2, 0, sw // 2, sh // 2),
"bot-left": lambda sw, sh: (0, sh // 2, sw // 2, sh // 2),
"bot-right": lambda sw, sh: (sw // 2, sh // 2, sw // 2, sh // 2),
"col-1-of-3": lambda sw, sh: (0, 0, sw // 3, sh),
"col-2-of-3": lambda sw, sh: (sw // 3, 0, sw // 3, sh),
"col-3-of-3": lambda sw, sh: (sw * 2 // 3, 0, sw // 3, sh),
"maximize": lambda sw, sh: (0, 0, sw, sh),
}
def tile_windows(layout: str) -> dict:
"""Tile the first N visible windows using a named layout."""
sw, sh = get_screen_size()
wins = [w for w in list_windows() if w["w"] > 100 and w["h"] > 100]
if layout == "2col":
slots = [
SNAP_POSITIONS["left-half"](sw, sh),
SNAP_POSITIONS["right-half"](sw, sh),
]
targets = wins[:2]
elif layout == "3col":
slots = [SNAP_POSITIONS[f"col-{i}-of-3"](sw, sh) for i in [1, 2, 3]]
targets = wins[:3]
elif layout == "grid4":
slots = [
SNAP_POSITIONS["top-left"](sw, sh),
SNAP_POSITIONS["top-right"](sw, sh),
SNAP_POSITIONS["bot-left"](sw, sh),
SNAP_POSITIONS["bot-right"](sw, sh),
]
targets = wins[:4]
else:
raise ValueError(f"Unknown layout '{layout}'. Use: 2col, 3col, grid4")
placed = []
for win, (x, y, w, h) in zip(targets, slots):
set_window_pos(win["hwnd"], x, y, w, h)
placed.append(
{"hwnd": win["hwnd"], "title": win["title"], "x": x, "y": y, "w": w, "h": h}
)
return {"layout": layout, "tiled": len(placed), "windows": placed}
# ── CLI ─────────────────────────────────────────────────────────────────────
def main() -> None:
parser = argparse.ArgumentParser(
description="Window manager — list, snap, tile, and control Windows windows"
)
sub = parser.add_subparsers(dest="cmd")
p_list = sub.add_parser("list", help="List visible windows")
p_list.add_argument("--json", action="store_true", help="JSON output")
p_move = sub.add_parser("move", help="Move and resize a window by HWND")
p_move.add_argument(
"--hwnd", type=int, required=True, help="Window handle (from list)"
)
p_move.add_argument("--x", type=int, required=True)
p_move.add_argument("--y", type=int, required=True)
p_move.add_argument("--w", type=int, required=True)
p_move.add_argument("--h", type=int, required=True)
p_move.add_argument("--json", action="store_true")
p_snap = sub.add_parser("snap", help="Snap a window to a preset screen position")
p_snap.add_argument("--hwnd", type=int, required=True)
p_snap.add_argument("--pos", choices=list(SNAP_POSITIONS.keys()), required=True)
p_snap.add_argument("--json", action="store_true")
p_tile = sub.add_parser("tile", help="Tile windows in a layout")
p_tile.add_argument("--layout", choices=["2col", "3col", "grid4"], required=True)
p_tile.add_argument("--json", action="store_true")
p_show = sub.add_parser("show", help="Change window state or bring to focus")
p_show.add_argument("--hwnd", type=int, required=True)
p_show.add_argument(
"--state", choices=["minimize", "maximize", "restore", "focus"], required=True
)
p_show.add_argument("--json", action="store_true")
args = parser.parse_args()
if not args.cmd:
parser.print_help()
sys.exit(2)
use_json = getattr(args, "json", False)
try:
if args.cmd == "list":
wins = list_windows()
if use_json:
print(json.dumps(wins, indent=2))
else:
print(f"{'HWND':<14} {'PID':<8} {'W×H':<14} {'Name':<20} Title")
print("─" * 90)
for w in wins:
size = f"{w['w']}×{w['h']}"
print(
f"{w['hwnd']:<14} {w['pid']:<8} {size:<14}"
f" {w['name'][:19]:<20} {w['title'][:45]}"
)
elif args.cmd == "move":
set_window_pos(args.hwnd, args.x, args.y, args.w, args.h)
result = {
"hwnd": args.hwnd,
"x": args.x,
"y": args.y,
"w": args.w,
"h": args.h,
}
if use_json:
print(json.dumps(result))
else:
print(f"Moved {args.hwnd} → ({args.x},{args.y}) {args.w}×{args.h}")
elif args.cmd == "snap":
sw, sh = get_screen_size()
x, y, w, h = SNAP_POSITIONS[args.pos](sw, sh)
set_window_pos(args.hwnd, x, y, w, h)
result = {
"hwnd": args.hwnd,
"pos": args.pos,
"x": x,
"y": y,
"w": w,
"h": h,
}
if use_json:
print(json.dumps(result))
else:
print(f"Snapped {args.hwnd} to {args.pos}: ({x},{y}) {w}×{h}")
elif args.cmd == "tile":
result = tile_windows(args.layout)
if use_json:
print(json.dumps(result, indent=2))
else:
print(f"Tiled {result['tiled']} windows [{args.layout}]:")
for w in result["windows"]:
print(
f" {w['hwnd']}: ({w['x']},{w['y']}) {w['w']}×{w['h']} {w['title'][:50]}"
)
elif args.cmd == "show":
state_map = {"minimize": 2, "maximize": 3, "restore": 9}
if args.state == "focus":
focus_window(args.hwnd)
else:
show_window(args.hwnd, state_map[args.state])
result = {"hwnd": args.hwnd, "state": args.state}
if use_json:
print(json.dumps(result))
else:
print(f"Window {args.hwnd}: {args.state}")
except Exception as exc:
if use_json:
print(json.dumps({"error": str(exc)}))
else:
print(f"Error: {exc}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()