mirror of
https://github.com/alirezarezvani/claude-skills.git
synced 2026-10-09 03:17:54 +00:00
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:
parent
29b4b141d0
commit
37b21bf8f1
5 changed files with 1337 additions and 0 deletions
243
engineering/desktop-manager/SKILL.md
Normal file
243
engineering/desktop-manager/SKILL.md
Normal 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.
|
||||
294
engineering/desktop-manager/references/powershell-window-api.md
Normal file
294
engineering/desktop-manager/references/powershell-window-api.md
Normal 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.
|
||||
298
engineering/desktop-manager/scripts/desktop_snapshot.py
Normal file
298
engineering/desktop-manager/scripts/desktop_snapshot.py
Normal 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()
|
||||
200
engineering/desktop-manager/scripts/process_manager.py
Normal file
200
engineering/desktop-manager/scripts/process_manager.py
Normal 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()
|
||||
302
engineering/desktop-manager/scripts/window_manager.py
Normal file
302
engineering/desktop-manager/scripts/window_manager.py
Normal 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()
|
||||
Loading…
Add table
Reference in a new issue