Skip to content

Add Screenshot Path to Clipboard mod#4708

Open
Sondre234 wants to merge 4 commits into
ramensoftware:mainfrom
Sondre234:screenshot-path-to-clipboard
Open

Add Screenshot Path to Clipboard mod#4708
Sondre234 wants to merge 4 commits into
ramensoftware:mainfrom
Sondre234:screenshot-path-to-clipboard

Conversation

@Sondre234

@Sondre234 Sondre234 commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a new mod, screenshot-path-to-clipboard, that watches %USERPROFILE%\Pictures\Screenshots and copies the full path of each new .png screenshot to the clipboard as soon as it appears (Win+PrintScreen, Snipping Tool, Xbox Game Bar, etc.).

Details

  • Runs in explorer.exe, uses ReadDirectoryChangesW on a background thread (no polling).
  • Creates the Screenshots folder if it doesn't exist.
  • No configuration/settings needed.

Changelog

N/A (new mod).

Mod authorship

This mod was created by:

    • The submitter, with AI assistance
    • The submitter, without AI assistance
    • Claude
    • ChatGPT
    • Gemini
    • Another AI (please specify):
    • Other (please specify):

Please select the options that best apply. Your selection does not affect the acceptance criteria, but it helps reviewers understand the context of the code and provide relevant feedback.

Sondre234 and others added 4 commits July 7, 2026 05:48
Watches %USERPROFILE%\Pictures\Screenshots and copies the full path
of each new .png screenshot to the clipboard as soon as it appears.
The metadata line is plain text, not a C string, so the escaped
double backslash never matched real explorer.exe paths and the mod
was never injected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@m417z

m417z commented Jul 7, 2026

Copy link
Copy Markdown
Member

Submission review

Note: This review was done by Claude, and then refined manually. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding.

Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them.


The mod is small and clean, but it's injecting into explorer.exe for work that doesn't need injection, and it has a real teardown/shutdown pitfall. Please address the following:

1. This should be a "mods as tools" mod, not injected into explorer.exe.

The mod doesn't hook anything — it only uses ReadDirectoryChangesW (folder watch), the clipboard, and a worker thread, all of which work from any process. That's the textbook profile for the mods-as-tools pattern, and this is exactly the situation the wiki describes:

  • Multiple explorer.exe processes: with "launch folder windows in a separate process" enabled (or other cases), there can be several explorer.exe instances. Each one would spin up its own MonitorThread watching the same folder, and each would race to EmptyClipboard/SetClipboardData on every new screenshot — redundant work and clipboard contention. The tool-mod launcher gives you a single dedicated process for free (the built-in single-instance mutex).
  • Shell stability: any fault in the mod (e.g. the std::terminate case below) takes down the whole Windows shell rather than a throwaway helper process.

Please convert it: change @include from explorer.exe to windhawk.exe, rename Wh_ModInit/Wh_ModUninitWhTool_ModInit/WhTool_ModUninit, and paste the launcher snippet from the wiki (verbatim). mods/explorer-folder-hover-menu.wh.cpp is a good reference — the launcher boilerplate at the bottom of that file is a straight copy of the wiki snippet.

2. Global std::thread destructor will call std::terminate() and crash the host on process exit.

g_monitorThread is a global std::thread, and it's only ever joined in Wh_ModUninit. But Wh_ModUninit is not called when the host process itself terminates (Explorer restart, sign-out, reboot) — there the OS just detaches the DLL and the CRT runs global destructors during DLL_PROCESS_DETACH. At that point the worker thread has already been killed by the OS, but the std::thread object is still joinable() (it was never joined/detached), so ~thread() calls std::terminate() and aborts the host process.

The standard fix is to give the global a trivial destructor — use a std::thread* allocated on the heap and intentionally leaked at shutdown, keeping the real join in the uninit path:

static std::thread* g_monitorThread = nullptr;
// ...
g_monitorThread = new std::thread(MonitorThread, folderPath);
// ...
// in uninit, after SetEvent:
if (g_monitorThread) {
    g_monitorThread->join();
    delete g_monitorThread;
    g_monitorThread = nullptr;
}

Converting to a tool mod (item 1) reduces the exposure because WhTool_ModUninit runs reliably before the tool process exits, but the thread-storage fix is the robust one and worth doing regardless.

3. Resolve the Screenshots folder via FOLDERID_Screenshots, not a hardcoded %USERPROFILE%\Pictures\Screenshots.

The default screenshots location is a real known folder and is frequently relocated — most commonly by OneDrive, which redirects the Pictures (and Screenshots) folder to %USERPROFILE%\OneDrive\Pictures\Screenshots, but the user can also move it manually. When that's the case, the current code watches an empty %USERPROFILE%\Pictures\Screenshots that never receives screenshots, and CreateDirectoryW silently creates a spurious empty folder there — the mod appears to do nothing. Use the shell known folder so you always get the real target:

PWSTR path = nullptr;
if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_Screenshots, 0, nullptr, &path))) {
    std::wstring folderPath = path;
    CoTaskMemFree(path);
    // ...
}

(needs #include <shlobj.h> and -lshell32 -lole32 in @compilerOptions.) This also removes the need to CreateDirectoryW — the known folder resolves to an existing location, which sidesteps leaving a persistent empty folder behind after the mod is disabled (reversibility).

Optional improvements

Minor polish — none of this affects users, so it's your call.

  • A transient watch error permanently stops monitoring. If ReadDirectoryChangesW, GetOverlappedResult, or WaitForMultipleObjects fails once, the loop breaks and the thread exits for good — the mod goes silent until it's reloaded. For a folder that could momentarily disappear (e.g. deleted/recreated), a short backoff-and-retry (reopen the directory handle) would be more resilient than giving up.

  • Error break paths leave a pending async read against the buffer. The clean stop path does CancelIo + GetOverlappedResult(bWait=TRUE) before returning, but the error paths fall straight through to CloseHandle(hDir) while an overlapped ReadDirectoryChangesW may still be in flight against the stack buffer. It's an edge case, but mirroring the cancel-and-drain sequence on those paths is tidier.

Functionality notes

Non-critical observations about the feature behavior itself.

  • README example accuracy. Xbox Game Bar saves its screenshots to Videos\Captures, not Pictures\Screenshots, so that example won't actually trigger the mod. Worth adjusting the examples in the readme to the tools that do write to the watched folder (Win+PrtScn, Snipping Tool). (I haven't verified this AI claim.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants