Fast Action

A PowerToys-inspired Windows keyboard overlay. Press a global hotkey to open a QWERTY-aligned action grid. Keys and clicks run shell commands or open nested grids.
Requirements
- Windows 10 1809+ / Windows 11
- .NET 10 SDK
- Windows App SDK runtime (bundled via self-contained build)
Install
Once the first release has gone through the one-time WinGet setup described in Releases & distribution:
winget install Luttik.FastAction
Until then, or if you'd rather grab it directly, download the latest FastActionSetup-*.exe from Releases and run it — it's a normal machine-wide installer (via Inno Setup; requires admin) with a Start Menu shortcut, per-user startup registration (HKCU\...\Run), and a clean uninstaller. No separate .NET or Windows App SDK runtime install is required; the app is self-contained.
Run from source
You can also build and run the unpackaged app directly.
cd c:\workspace\fast-action
.\run.ps1
Or double-click / run run.cmd. Options:
.\run.ps1 # Debug build + launch
.\run.ps1 -Release # Release build + launch
.\run.ps1 -NoBuild # Launch last build only
Manual equivalent:
dotnet build src\FastAction\FastAction.csproj -c Debug -p:Platform=x64
dotnet run --project src\FastAction\FastAction.csproj -c Debug -p:Platform=x64
The app starts in the system tray. Default hotkey: Win+Shift+Space (registered via a low-level keyboard hook so Win combinations work reliably).
- Open overlay — show the root grid
- Settings — show the overlay with the layout/appearance pane open
- Open config folder —
%LOCALAPPDATA%\FastAction
- Reload config — re-read
config.yaml
- Start with Windows — toggle launching automatically at sign-in (checked by default)
- Exit
Config
On first launch the app copies an example config to:
%LOCALAPPDATA%\FastAction\config.yaml
Edits are watched and reloaded live.
runOnStartup (default true) is applied via a per-user HKCU\...\CurrentVersion\Run registry entry. The installer writes that entry so silent installs (including WinGet) register startup even before the first launch; the running app then keeps the same value pointed at the current exe and in sync with this setting — on every launch, when you flip Start with Windows in the tray menu, or when you edit the config directly.
Schema
hotkey:
modifiers: [Win, Shift] # Win, Ctrl, Alt, Shift
key: Space # Space, Tab, Enter, Esc, A–Z, 0–9, F1–F12
rootGridId: home
editOnRightClick: true # right-click a tile to edit or clear it
runOnStartup: true # launch automatically at Windows sign-in
layout:
startKey: "1" # top-left key of the overlay grid
columns: 4 # 1–10
rows: 4 # 1–4
appearance:
theme: system # system | light | dark
tileSize: 88 # 72 | 88 | 112
cornerRadius: 12 # 4 | 12 | 22
acrylic: true # Windows Terminal-style frosted glass
opacity: 80 # 20–100 (higher = more solid)
acrylicBlur: standard # standard | soft
grids:
- id: home
title: Home
items:
- key: Q # Must be one of Q–P / A–L / Z–M
name: Notepad
icon:
type: app # app | lucide | svg
path: notepad.exe
action:
type: shell # shell | grid | hotkey
command: notepad.exe
args: [] # optional
workingDirectory: # optional
- key: W
name: Dev tools
icon:
type: lucide
name: wrench # Assets/Icons/lucide/{name}.svg
color: orange # auto | white | slate | blue | green | amber | orange | pink | purple | red
action:
type: grid
gridId: devtools
- key: E
name: Explorer
icon:
type: lucide
name: folder
action:
type: hotkey
modifiers: [Win]
key: E
- key: R
name: Custom SVG
icon:
type: svg
path: icons/my.svg # relative to config folder, or absolute
action:
type: shell
command: powershell
args: [-NoProfile, -Command, "Write-Host hi"]
Keyboard layout
The overlay is a rectangular slice of the US QWERTY map. layout.startKey is the top-left key; columns and rows size the slice. The default (startKey: "1", 4×4) matches the original grid:
startKey: Q with 3×3 is the letter core (Q W E / A S D / Z X C). Keys outside the current slice stay in config.yaml and come back if you enlarge the grid later.
The overlay Grid and Appearance menus (and the gear on the overlay) write these values live. Appearance → Acrylic toggles Desktop Acrylic, sets opacity (20–100, like Windows Terminal), and chooses standard vs soft blur. Right-click a tile to edit that shortcut; Lucide color tints that tile’s stroke icon (icon.color). Empty slots stay blank. Esc closes the overlay. Backspace pops a nested grid.
Values with colons (protocol handlers like ms-settings:) must be quoted in YAML:
command: "ms-settings:"
| Type | Fields | Notes |
|---|
app | path | .exe / .lnk; icon extracted via Shell |
lucide | name, optional color | Full Lucide set under Assets/Icons/lucide/ (e.g. wrench, file). color is a palette id (auto, blue, orange, …); pick it in the shortcut editor. |
svg | path | Custom SVG on disk |
Browse names at lucide.dev/icons. To refresh the bundled set:
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\sync_lucide_icons.ps1
Regenerating the app icon / brand assets
AppIcon.ico, the tray icon, and the Windows Store-style logos (StoreLogo.png, Square44x44Logo.*, SplashScreen.*, etc.) are all generated from the source marks in src/FastAction/Assets/Brand/. After changing a mark-*.png, regenerate everything with:
pip install -r scripts/requirements.txt
python scripts/build_icons.py
Project layout
src/FastAction/ WinUI 3 unpackaged tray app
Overlay/ Acrylic keyboard grid overlay + menu/settings
Services/ Config, hotkey, actions, icons, theme
Models/ YAML models + QWERTY layout
Assets/config.example.yaml shipped first-run template
src/FastAction.Tests/ Layout/appearance unit tests (no WinUI)
docs/overlay-mock/ HTML mock used to maintain README screenshots
docs/screenshots/ PNG stills embedded in this README
.cursor/skills/readme-screenshots/ How to regenerate those stills
Releases & distribution
Releases are cut by pushing a v*.*.* tag. Two ways that happens:
Either way, the tag push triggers .github/workflows/release.yml, which:
- Publishes a self-contained Release build (
dotnet publish, currently win-x64; other Platforms in the csproj can be added to the build matrix later).
- Compiles it into
FastActionSetup--x64.exe with Inno Setup (installer/FastAction.iss) — a normal Windows installer with a Start Menu shortcut, per-user HKCU\...\Run startup registration, and a proper uninstaller.
- Publishes a GitHub Release with that installer attached and auto-generated release notes.
- Opens a PR against the WinGet Community Repository (via winget-releaser) so
winget install Luttik.FastAction picks up the new version automatically.
(The WinGet step lives in the same workflow rather than triggering off release: published, because GitHub Actions doesn't fire other workflows off events created by the default GITHUB_TOKEN.) The installer is currently unsigned, so Windows SmartScreen may warn on first run until the file builds up reputation; see "Code signing" below if that becomes a priority.
Status: v0.0.1 is out. First WinGet submission is open: microsoft/winget-pkgs#408285. Once that merges, winget install Luttik.FastAction works and later releases auto-update via release.yml.
WinGet setup (one-time, manual)
winget-releaser can only update a package that's already in winget-pkgs — it can't create the first submission. Status:
- Fork
microsoft/winget-pkgs — done: Luttik/winget-pkgs.
- Create a classic GitHub PAT with the
public_repo scope and store it as repo secret WINGET_TOKEN — done.
- Submit the first manifest — done / awaiting merge: PR #408285.
After that PR merges, every subsequent tagged release keeps WinGet in sync automatically via the winget job in release.yml.
Code signing
The installer ships unsigned today. Two low/no-cost options to reduce SmartScreen friction later:
- SignPath.io's free OSS program — free code signing for open-source projects, integrates with GitHub Actions.
- A low-cost OV certificate (e.g. Certum ~$25-30/yr) added as a GitHub secret and wired into the
release.yml build step.
Neither is required for WinGet distribution to work.
Microsoft Store (future work)
The project already ships Store-ready assets (Package.appxmanifest, StoreLogo.png, tile logos) with a placeholder identity, but nothing is wired up yet. Getting there requires steps only the account owner can do:
- Create a Microsoft Partner Center developer account (one-time fee + identity verification).
- Reserve the app name to get a real
Package/Identity/Name and Publisher — update Package.appxmanifest with those values.
- Register an Azure AD app associated with the Partner Center account to get submission-API credentials (tenant ID, client ID, client secret).
- Add a CD job that builds an MSIX/MSIX bundle (
-p:WindowsPackageType=MSIX) and submits it via the Microsoft Store submission API/CLI.
Once published, winget also picks up the Store listing automatically via its msstore source — no extra WinGet-side work needed for that path.
Development
CI runs on every push/PR to main (see .github/workflows/ci.yml): a Release build, dotnet format verification, ruff for the Python scripts, and PSScriptAnalyzer for the PowerShell scripts.
To check the same things locally before pushing:
# Build
dotnet build src\FastAction\FastAction.csproj -c Release -p:Platform=x64
# C# style/format check (add `dotnet format FastAction.sln` without --verify-no-changes to auto-fix)
dotnet format FastAction.sln --verify-no-changes
# Layout unit tests (no WinUI)
dotnet test src\FastAction.Tests\FastAction.Tests.csproj
# Python lint/format (scripts/build_icons.py)
pip install ruff
ruff check scripts/
ruff format --check scripts/
# PowerShell lint (run.ps1, scripts/*.ps1)
Install-Module -Name PSScriptAnalyzer -Scope CurrentUser
Get-ChildItem -Recurse -Filter *.ps1 -File |
Where-Object { $_.FullName -notmatch '\\(bin|obj)\\' } |
ForEach-Object { Invoke-ScriptAnalyzer -Path $_.FullName -Settings .\PSScriptAnalyzerSettings.psd1 }