diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml
new file mode 100644
index 0000000..c42281f
--- /dev/null
+++ b/.github/workflows/windows.yml
@@ -0,0 +1,70 @@
+name: Windows build
+
+on:
+ push:
+ branches: [main]
+ tags: ["v*"]
+ pull_request:
+ branches: [main]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ runs-on: windows-2022
+ steps:
+ - name: Check out source
+ uses: actions/checkout@v4
+
+ - name: Configure x64 Release
+ run: cmake -S . -B build/release -G "Visual Studio 17 2022" -A x64
+
+ - name: Build
+ run: cmake --build build/release --config Release --parallel
+
+ - name: Run self-test
+ shell: pwsh
+ run: powershell.exe -NoProfile -ExecutionPolicy Bypass -File ./scripts/ci-smoke.ps1 -Exe ./build/release/bin/Release/Peek.exe
+
+ - name: Prepare artifact
+ shell: pwsh
+ run: |
+ New-Item -ItemType Directory -Force -Path artifact | Out-Null
+ Copy-Item build/release/bin/Release/Peek.exe artifact/Peek.exe
+ $hash = (Get-FileHash artifact/Peek.exe -Algorithm SHA256).Hash
+ "$hash Peek.exe" | Set-Content artifact/SHA256SUMS.txt -Encoding ascii
+
+ - name: Upload build artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: Peek-windows-x64
+ path: artifact/*
+ if-no-files-found: error
+
+ publish:
+ if: startsWith(github.ref, 'refs/tags/v')
+ needs: build
+ runs-on: windows-2022
+ permissions:
+ contents: write
+ steps:
+ - name: Download verified build
+ uses: actions/download-artifact@v4
+ with:
+ name: Peek-windows-x64
+ path: artifact
+
+ - name: Create or update GitHub Release
+ shell: pwsh
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ gh release view "${{ github.ref_name }}" 2>$null
+ if ($LASTEXITCODE -eq 0) {
+ gh release upload "${{ github.ref_name }}" artifact/Peek.exe artifact/SHA256SUMS.txt --clobber
+ } else {
+ gh release create "${{ github.ref_name }}" artifact/Peek.exe artifact/SHA256SUMS.txt --generate-notes --title "${{ github.ref_name }}"
+ }
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
diff --git a/README.md b/README.md
index 8bef5f9..7b818df 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,8 @@
[](LICENSE)

+**[官方网站 / Official website](https://habadede.github.io/Peek/)**
+
[简体中文](#readme-zh) · [English](#readme-en)
**[下载最新版本](https://github.com/HaBaDEDE/Peek/releases/latest/download/Peek.exe) · [Download latest release](https://github.com/HaBaDEDE/Peek/releases/latest/download/Peek.exe)**
diff --git a/docs/.nojekyll b/docs/.nojekyll
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/docs/.nojekyll
@@ -0,0 +1 @@
+
diff --git a/docs/index.html b/docs/index.html
new file mode 100644
index 0000000..d461412
--- /dev/null
+++ b/docs/index.html
@@ -0,0 +1,140 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Peek — Tiny native Windows utility
+
+
+
+
+
+
+
+
+
+
+
NATIVE WINDOWS UTILITY
+
Three answers. One tiny Windows tool.
+
Find what flashed past, who locked a file, and which Windows UI is under the pointer.
+
+
Windows 10/11 · x64 · Portable · No account · No telemetry
+
+
+
+
+
+
+
+
+
REAL WORKFLOW
+
The answer is one hotkey away.
+
+
+
+
+
+
+
+
+
THREE FOCUSED TOOLS
+
Only what you need, when you need it.
+
+
+
+
+
01
+
Ghost
+
What just flashed past?
+
Keeps the latest short-lived process and top-level window events in memory, including lifetime, path, command line, and parent process when Windows makes them available.
+
Ctrl + Shift + G
+
+
+
+
+
+
+
02
+
Unlock
+
Who is using this file?
+
Drop a file or choose one. Peek uses Windows Restart Manager to identify likely owners and lets you reveal the executable or end the process after confirmation.
+
+
+
+
+
+
+
03
+
Focus
+
What Windows UI is under the pointer?
+
Highlight a window or control and read its HWND, process, class, title, rectangle, and available UI Automation properties.
+
Ctrl + Shift + F
+
+
+
+
+
+
+
+
SMALL BY DESIGN
+
Native, offline, and understandable.
+
+
+ Single EXE Run it directly. No installer or separate runtime.
+ No network No account, cloud sync, online database, or telemetry.
+ Native Win32 C++20 and Windows system APIs. No Electron or WebView.
+ Open source MIT licensed, with source and build instructions on GitHub.
+
+
+
+
+
+
+
Keep the answer one action away.
+
Download the portable Windows x64 release.
+
+ Download
+
+
+
+
+ Peek · MIT License
+ No tracking on this site.
+
+
+
+
diff --git a/docs/media/peek-ghost-demo.mp4 b/docs/media/peek-ghost-demo.mp4
new file mode 100644
index 0000000..e402a5c
Binary files /dev/null and b/docs/media/peek-ghost-demo.mp4 differ
diff --git a/docs/robots.txt b/docs/robots.txt
new file mode 100644
index 0000000..79476d9
--- /dev/null
+++ b/docs/robots.txt
@@ -0,0 +1,4 @@
+User-agent: *
+Allow: /
+
+Sitemap: https://habadede.github.io/Peek/sitemap.xml
diff --git a/docs/site.css b/docs/site.css
new file mode 100644
index 0000000..b8c7502
--- /dev/null
+++ b/docs/site.css
@@ -0,0 +1,131 @@
+:root {
+ color-scheme: light;
+ --ink: #10213a;
+ --muted: #5e6f87;
+ --blue: #2869e8;
+ --blue-dark: #174fbd;
+ --line: #dce4ef;
+ --panel: #f5f8fc;
+ --navy: #0d1b2f;
+}
+
+* { box-sizing: border-box; }
+html { scroll-behavior: smooth; }
+body {
+ margin: 0;
+ color: var(--ink);
+ background: #fff;
+ font-family: "Segoe UI Variable Text", "Segoe UI", system-ui, sans-serif;
+ font-size: 17px;
+ line-height: 1.65;
+}
+a { color: inherit; }
+img, video { display: block; max-width: 100%; }
+
+.topbar {
+ position: sticky;
+ z-index: 10;
+ top: 0;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ height: 68px;
+ padding: 0 max(24px, calc((100vw - 1180px) / 2));
+ border-bottom: 1px solid rgba(220, 228, 239, .85);
+ background: rgba(255, 255, 255, .92);
+ backdrop-filter: blur(14px);
+}
+.brand { display: flex; align-items: center; gap: 10px; text-decoration: none; font-size: 21px; font-weight: 650; }
+.brand img { border-radius: 9px; }
+nav { display: flex; align-items: center; gap: 27px; }
+nav a { color: #42536b; text-decoration: none; font-size: 15px; }
+nav a:hover { color: var(--blue); }
+.language { min-width: 62px; padding: 7px 12px; border: 1px solid var(--line); border-radius: 7px; color: var(--ink); background: #fff; font: inherit; font-size: 14px; cursor: pointer; }
+.language:hover { border-color: #aab8cc; }
+
+main { overflow: hidden; }
+.hero {
+ display: grid;
+ grid-template-columns: minmax(0, .9fr) minmax(420px, 1.1fr);
+ gap: 74px;
+ align-items: center;
+ max-width: 1180px;
+ min-height: 690px;
+ margin: 0 auto;
+ padding: 80px 24px 92px;
+}
+.eyebrow, .section-label { margin: 0 0 14px; color: var(--blue); font-size: 13px; font-weight: 700; letter-spacing: .14em; }
+h1, h2, h3 { margin-top: 0; line-height: 1.13; letter-spacing: -.025em; }
+h1 { max-width: 650px; margin-bottom: 23px; font-size: clamp(48px, 5.1vw, 74px); font-weight: 690; }
+.lead { max-width: 590px; margin: 0 0 34px; color: var(--muted); font-size: 21px; line-height: 1.6; }
+.actions { display: flex; flex-wrap: wrap; gap: 12px; }
+.button { display: inline-flex; align-items: center; justify-content: center; min-height: 48px; padding: 0 20px; border: 1px solid transparent; border-radius: 8px; text-decoration: none; font-size: 15px; font-weight: 650; }
+.primary { color: #fff; background: var(--blue); box-shadow: 0 8px 24px rgba(40, 105, 232, .2); }
+.primary:hover { background: var(--blue-dark); }
+.secondary { border-color: var(--line); background: #fff; }
+.secondary:hover { border-color: #b8c5d6; background: #f9fbfd; }
+.requirements { margin: 20px 0 0; color: #708199; font-size: 13px; }
+.hero-visual { position: relative; padding: 26px; border-radius: 30px; background: linear-gradient(140deg, #eaf2ff, #f7f9fc); }
+.hero-visual::after { position: absolute; inset: 8% -12% auto auto; z-index: -1; width: 360px; height: 360px; border-radius: 50%; background: #d8e6ff; filter: blur(60px); content: ""; }
+.hero-visual img { width: min(100%, 520px); margin: auto; border: 1px solid #cbd5e2; border-radius: 12px; box-shadow: 0 25px 70px rgba(23, 43, 74, .18); }
+
+.demo { display: grid; grid-template-columns: .65fr 1.35fr; gap: 64px; align-items: start; max-width: 1180px; margin: 0 auto; padding: 104px 24px; border-top: 1px solid var(--line); }
+.demo h2, .section-heading h2, .principles h2 { margin-bottom: 0; font-size: clamp(34px, 4vw, 50px); font-weight: 660; }
+.demo video { width: 100%; border: 1px solid #cbd5e2; border-radius: 14px; background: var(--navy); box-shadow: 0 22px 60px rgba(23, 43, 74, .15); }
+
+.features { padding: 110px 24px; background: var(--panel); }
+.section-heading { max-width: 1180px; margin: 0 auto 72px; }
+.feature-card { display: grid; grid-template-columns: .9fr 1.1fr; gap: 72px; align-items: center; max-width: 1080px; margin: 0 auto 110px; }
+.feature-card:last-child { margin-bottom: 0; }
+.feature-card.reverse .feature-copy { order: 2; }
+.feature-card.reverse img { order: 1; }
+.feature-copy { max-width: 470px; }
+.number { display: block; margin-bottom: 22px; color: #8ca0bb; font-size: 13px; font-weight: 700; letter-spacing: .13em; }
+.feature-card h3 { margin-bottom: 8px; font-size: 42px; font-weight: 680; }
+.feature-card .question { margin: 0 0 19px; color: var(--blue); font-size: 21px; font-weight: 600; }
+.feature-card p:not(.question) { margin: 0 0 25px; color: var(--muted); }
+kbd { display: inline-block; padding: 8px 13px; border: 1px solid #cbd6e4; border-bottom-width: 2px; border-radius: 7px; background: #fff; color: #354962; font: 600 13px "Segoe UI", sans-serif; }
+.feature-card img { max-height: 630px; margin: auto; border: 1px solid #cbd5e2; border-radius: 12px; box-shadow: 0 20px 50px rgba(23, 43, 74, .13); }
+
+.principles { display: grid; grid-template-columns: .8fr 1.2fr; gap: 90px; max-width: 1180px; margin: 0 auto; padding: 116px 24px; }
+.principles ul { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; margin: 0; padding: 0; background: var(--line); border: 1px solid var(--line); list-style: none; }
+.principles li { min-height: 170px; padding: 29px; background: #fff; }
+.principles strong, .principles span { display: block; }
+.principles strong { margin-bottom: 8px; font-size: 18px; }
+.principles span { color: var(--muted); font-size: 15px; }
+
+.download { display: grid; grid-template-columns: auto 1fr auto; gap: 25px; align-items: center; max-width: 1132px; margin: 0 auto 105px; padding: 38px; border-radius: 18px; color: #fff; background: var(--navy); }
+.download img { border-radius: 18px; }
+.download h2 { margin: 0 0 4px; font-size: 27px; }
+.download p { margin: 0; color: #b9c6d8; }
+.download .button { min-width: 130px; }
+
+footer { display: flex; justify-content: space-between; max-width: 1180px; margin: 0 auto; padding: 28px 24px 40px; border-top: 1px solid var(--line); color: #74859b; font-size: 13px; }
+
+@media (max-width: 850px) {
+ nav a { display: none; }
+ .hero, .demo, .principles { grid-template-columns: 1fr; }
+ .hero { gap: 52px; min-height: 0; padding-top: 64px; }
+ .hero-visual { max-width: 590px; }
+ .demo { gap: 36px; }
+ .feature-card { grid-template-columns: 1fr; gap: 36px; }
+ .feature-card.reverse .feature-copy, .feature-card.reverse img { order: initial; }
+ .feature-copy { max-width: 650px; }
+ .principles { gap: 45px; }
+}
+
+@media (max-width: 560px) {
+ body { font-size: 16px; }
+ h1 { font-size: 45px; }
+ .topbar { padding: 0 18px; }
+ .hero, .demo, .features, .principles { padding-left: 18px; padding-right: 18px; }
+ .principles ul { grid-template-columns: 1fr; }
+ .download { grid-template-columns: auto 1fr; margin: 0 18px 70px; padding: 26px; }
+ .download img { width: 58px; height: 58px; }
+ .download .button { grid-column: 1 / -1; }
+ footer { flex-direction: column; gap: 8px; padding-left: 18px; padding-right: 18px; }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ html { scroll-behavior: auto; }
+}
diff --git a/docs/site.js b/docs/site.js
new file mode 100644
index 0000000..ac4d863
--- /dev/null
+++ b/docs/site.js
@@ -0,0 +1,24 @@
+(() => {
+ const button = document.querySelector("#language");
+ const translatable = document.querySelectorAll("[data-en][data-zh]");
+ const stored = localStorage.getItem("peek-site-language");
+ const preferred = navigator.language.toLowerCase().startsWith("zh") ? "zh" : "en";
+
+ function setLanguage(language) {
+ const next = language === "zh" ? "zh" : "en";
+ document.documentElement.lang = next === "zh" ? "zh-CN" : "en";
+ document.title = next === "zh" ? "Peek — 极小的原生 Windows 工具" : "Peek — Tiny native Windows utility";
+ translatable.forEach((element) => {
+ element.textContent = element.dataset[next];
+ });
+ button.textContent = next === "zh" ? "EN" : "中文";
+ button.dataset.language = next;
+ localStorage.setItem("peek-site-language", next);
+ }
+
+ button.addEventListener("click", () => {
+ setLanguage(button.dataset.language === "zh" ? "en" : "zh");
+ });
+
+ setLanguage(stored || preferred);
+})();
diff --git a/docs/sitemap.xml b/docs/sitemap.xml
new file mode 100644
index 0000000..f926f77
--- /dev/null
+++ b/docs/sitemap.xml
@@ -0,0 +1,8 @@
+
+
+
+ https://habadede.github.io/Peek/
+ monthly
+ 1.0
+
+
diff --git a/scripts/build-release.ps1 b/scripts/build-release.ps1
index c900881..2bdb6e1 100644
--- a/scripts/build-release.ps1
+++ b/scripts/build-release.ps1
@@ -18,7 +18,9 @@ if (-not $cmake) { throw "CMake 3.24+ was not found in PATH or Visual Studio." }
$cmakePath = if ($cmake.Source) { $cmake.Source } else { $cmake.FullName }
& $cmakePath -S $root -B $build -G "Visual Studio 17 2022" -A x64 -DCMAKE_BUILD_TYPE=Release
+if ($LASTEXITCODE -ne 0) { throw "CMake configure failed with code $LASTEXITCODE." }
& $cmakePath --build $build --config Release --parallel
+if ($LASTEXITCODE -ne 0) { throw "Release build failed with code $LASTEXITCODE." }
$exe = Join-Path $build "bin/Release/Peek.exe"
if (-not (Test-Path $exe)) {
diff --git a/scripts/ci-smoke.ps1 b/scripts/ci-smoke.ps1
new file mode 100644
index 0000000..1082046
--- /dev/null
+++ b/scripts/ci-smoke.ps1
@@ -0,0 +1,29 @@
+[CmdletBinding()]
+param([Parameter(Mandatory=$true)][string]$Exe)
+
+$ErrorActionPreference = "Stop"
+$exePath = (Resolve-Path $Exe).Path
+$report = Join-Path $env:TEMP "Peek-ci-selftest-$PID.txt"
+$peek = $null
+
+try {
+ $selfTest = Start-Process $exePath -ArgumentList '--self-test',('"' + $report + '"') -PassThru -Wait
+ if ($selfTest.ExitCode -ne 0) { throw "Peek self-test failed with code $($selfTest.ExitCode)." }
+ if (-not (Test-Path $report)) { throw "Peek did not write its self-test report." }
+
+ $contents = Get-Content $report
+ $contents
+ if ($contents -match '^FAIL') { throw "Peek self-test report contains a failure." }
+
+ $peek = Start-Process $exePath -PassThru
+ Start-Sleep -Milliseconds 1000
+ if ($peek.HasExited) { throw "Peek exited during startup with code $($peek.ExitCode)." }
+ Write-Host "Startup smoke test passed (PID $($peek.Id))."
+}
+finally {
+ if ($peek -and -not $peek.HasExited) {
+ Stop-Process -Id $peek.Id -Force -ErrorAction SilentlyContinue
+ $peek.WaitForExit(5000) | Out-Null
+ }
+ Remove-Item -LiteralPath $report -Force -ErrorAction SilentlyContinue
+}
diff --git a/scripts/record-demo.py b/scripts/record-demo.py
new file mode 100644
index 0000000..0de643f
--- /dev/null
+++ b/scripts/record-demo.py
@@ -0,0 +1,135 @@
+"""Record the real Peek window into a privacy-safe 1280x720 product demo."""
+
+from argparse import ArgumentParser
+from ctypes import Structure, byref, sizeof, windll
+from ctypes.wintypes import DWORD, HWND, LONG, RECT
+from pathlib import Path
+import sys
+from time import monotonic, sleep
+
+from PIL import Image, ImageDraw, ImageFilter, ImageFont, ImageGrab
+
+
+ROOT = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(ROOT / "build" / "video-tools"))
+import imageio_ffmpeg # noqa: E402
+
+
+DWMWA_EXTENDED_FRAME_BOUNDS = 9
+
+
+class DwmRect(Structure):
+ _fields_ = [("left", LONG), ("top", LONG), ("right", LONG), ("bottom", LONG)]
+
+
+def capture_window(hwnd: HWND) -> Image.Image:
+ visible = DwmRect()
+ result = windll.dwmapi.DwmGetWindowAttribute(
+ hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, byref(visible), DWORD(sizeof(visible)))
+ if result != 0:
+ fallback = RECT()
+ if not windll.user32.GetWindowRect(hwnd, byref(fallback)):
+ raise RuntimeError("Unable to read Peek window bounds")
+ visible = DwmRect(fallback.left, fallback.top, fallback.right, fallback.bottom)
+ return ImageGrab.grab(
+ bbox=(visible.left, visible.top, visible.right, visible.bottom),
+ all_screens=True,
+ ).convert("RGB")
+
+
+def make_frame(window: Image.Image, elapsed: float, icon: Image.Image,
+ empty_window: Image.Image) -> Image.Image:
+ canvas = Image.new("RGB", (1280, 720), (11, 23, 40))
+ draw = ImageDraw.Draw(canvas)
+ regular = ImageFont.truetype("C:/Windows/Fonts/segoeui.ttf", 24)
+ semibold = ImageFont.truetype("C:/Windows/Fonts/seguisb.ttf", 48)
+ small = ImageFont.truetype("C:/Windows/Fonts/segoeui.ttf", 18)
+ shortcut = ImageFont.truetype("C:/Windows/Fonts/seguisb.ttf", 26)
+
+ canvas.paste(icon, (58, 51), icon)
+ draw.text((145, 58), "Peek", font=semibold, fill=(245, 248, 252))
+ draw.text((59, 151), "Ghost", font=small, fill=(92, 153, 255))
+
+ if elapsed < 4.3:
+ title, detail = "A window flashes.", "It is gone before you can read it."
+ step = "01"
+ elif elapsed < 8.0:
+ title, detail = "Press one hotkey.", "Open the latest Ghost event immediately."
+ step = "02"
+ elif elapsed < 13.0:
+ title, detail = "Peek identifies it.", "Process, lifetime, path, and parent."
+ step = "03"
+ else:
+ title, detail = "One action. One answer.", "Native Win32 · Offline · Portable"
+ step = "04"
+
+ draw.text((59, 222), step, font=small, fill=(111, 145, 190))
+ draw.multiline_text((59, 267), title, font=semibold, fill=(245, 248, 252), spacing=8)
+ draw.multiline_text((59, 344), detail, font=regular, fill=(161, 178, 202), spacing=8)
+ if 4.3 <= elapsed < 13.0:
+ draw.rounded_rectangle((59, 447, 416, 513), radius=9,
+ fill=(24, 43, 69), outline=(61, 91, 130))
+ draw.text((237, 480), "Ctrl + Shift + G", font=shortcut,
+ fill=(245, 248, 252), anchor="mm")
+
+ display_window = empty_window if elapsed < 4.3 else window
+ target_height = 664
+ target_width = round(display_window.width * target_height / display_window.height)
+ rendered = display_window.resize((target_width, target_height), Image.Resampling.LANCZOS)
+ shadow = Image.new("RGBA", canvas.size, (0, 0, 0, 0))
+ shadow_draw = ImageDraw.Draw(shadow)
+ x, y = 1280 - target_width - 38, 28
+ shadow_draw.rounded_rectangle((x + 8, y + 12, x + target_width + 8, y + target_height + 12),
+ radius=18, fill=(0, 0, 0, 115))
+ shadow = shadow.filter(ImageFilter.GaussianBlur(17))
+ canvas = Image.alpha_composite(canvas.convert("RGBA"), shadow).convert("RGB")
+ canvas.paste(rendered, (x, y))
+ return canvas
+
+
+def main() -> None:
+ parser = ArgumentParser()
+ parser.add_argument("output", type=Path)
+ parser.add_argument("--title", default="Peek")
+ parser.add_argument("--duration", type=float, default=17.0)
+ parser.add_argument("--fps", type=int, default=10)
+ args = parser.parse_args()
+
+ hwnd = windll.user32.FindWindowW(None, args.title)
+ deadline = monotonic() + 30
+ while not hwnd and monotonic() < deadline:
+ sleep(.1)
+ hwnd = windll.user32.FindWindowW(None, args.title)
+ if not hwnd:
+ raise SystemExit("Peek window not found")
+
+ icon = Image.open(ROOT / "docs/images/peek-icon.png").convert("RGBA")
+ icon.thumbnail((70, 70), Image.Resampling.LANCZOS)
+ empty_window = Image.open(ROOT / "docs/images/peek-demo-ghost-hd.png").convert("RGB")
+ args.output.parent.mkdir(parents=True, exist_ok=True)
+ writer = imageio_ffmpeg.write_frames(
+ str(args.output), (1280, 720), fps=args.fps, codec="libx264",
+ pix_fmt_in="rgb24", pix_fmt_out="yuv420p", quality=5,
+ output_params=["-movflags", "+faststart", "-preset", "slow"])
+ writer.send(None)
+ started = monotonic()
+ frame_interval = 1 / args.fps
+ next_frame = started
+ try:
+ while True:
+ now = monotonic()
+ elapsed = now - started
+ if elapsed >= args.duration:
+ break
+ if now < next_frame:
+ sleep(next_frame - now)
+ window = capture_window(hwnd)
+ writer.send(make_frame(window, elapsed, icon, empty_window).tobytes())
+ next_frame += frame_interval
+ finally:
+ writer.close()
+ print(f"Recorded {args.output} ({args.duration:.1f}s, {args.fps} fps)")
+
+
+if __name__ == "__main__":
+ main()