DEV Community

Cover image for Windows won't let you own Win+Shift+S. So I hooked the keyboard in Python.
Akhouri Anmol Kumar
Akhouri Anmol Kumar

Posted on

Windows won't let you own Win+Shift+S. So I hooked the keyboard in Python.

Low-level keyboard hooks bypass shell reservations

I wanted one small thing: press Win+Shift+S and get my own screenshot tool instead of Snipping Tool.

That turned into a 7,600-line Python project, and a pile of Windows internals that I couldn't find explained in one place. Here are the four that cost me the most hair.

1. RegisterHotKey can't claim Win+Shift+S

Every tutorial says to use RegisterHotKey. It works for Ctrl+Alt+D. It silently fails for Win+Shift+S, because the shell reserves that combo.

The way around it is a low-level keyboard hook (WH_KEYBOARD_LL, id 13), which sees the key before the shell does:

HOOKPROC = ctypes.WINFUNCTYPE(ctypes.c_ssize_t, ctypes.c_int,
                              ctypes.c_size_t, ctypes.c_ssize_t)

def cb(nCode, wParam, lParam):
    if nCode == 0 and wParam in (0x100, 0x101, 0x104, 0x105):
        kb = ctypes.cast(lParam, ctypes.POINTER(KBDLLHOOKSTRUCT)).contents
        if kb.vkCode == 0x53 and win_down() and shift_down():   # S
            u.keybd_event(0xE8, 0, 0, 0)   # see below
            u.keybd_event(0xE8, 0, 2, 0)
            QTimer.singleShot(0, on_trigger)   # hop back to the Qt thread
            return 1                           # swallow the key
    return u.CallNextHookEx(None, nCode, wParam, lParam)

self._proc = HOOKPROC(cb)   # keep a reference!
hook = u.SetWindowsHookExW(13, self._proc, k.GetModuleHandleW(None), 0)
Enter fullscreen mode Exit fullscreen mode

Three gotchas worth knowing:

  • Keep a reference to the callback. If HOOKPROC(cb) gets garbage-collected, your app crashes on the next keypress. There's no traceback, it just dies.
  • Swallowing S isn't enough. If you eat the keypress and the user then releases Win, Windows opens the Start menu. The fix is to inject an unassigned virtual key (0xE8) so Windows thinks Win was used in a combo.
  • Track the key-up too. If you swallow the key-down, you have to swallow its matching key-up, or apps see an orphaned release. I pulled this into a pure function (snip_hook_decision(...)) with no ctypes in it, so it's unit-testable.

The hook only looks at the specific keys it cares about while Win+Shift are held. Nothing is logged or stored. If you ship a keyboard hook, say that in your README.

2. Keeping your own UI out of your own recording

My screen recorder has a floating control bar. Early on, every video started with the recorder recording itself, which looks amateur.

Windows 10 (2004+) has a one-line fix:

WDA_EXCLUDEFROMCAPTURE = 0x11
ctypes.windll.user32.SetWindowDisplayAffinity(int(widget.winId()),
                                              WDA_EXCLUDEFROMCAPTURE)
Enter fullscreen mode Exit fullscreen mode

The window stays visible to the user but is invisible to capture APIs. It's best-effort on older builds, so wrap it in try/except and move on.

3. System audio in Python: sounddevice can't do it

Recording the microphone is easy. Recording what the PC is playing is not. sounddevice can't do WASAPI loopback.

What worked:

Source Library
Microphone sounddevice
System audio (loopback) PyAudioWPatch
Mixing + muxing into MP4 imageio-ffmpeg (bundles an ffmpeg binary)

The part that matters: don't mix in real time. I record each source to its own WAV on disk, then mix once after the take with ffmpeg's amix. Real-time mixing means two clocks drifting against each other. Post-mixing means a dropped buffer in one stream can't corrupt the other.

I also pad or trim each track to exactly the video duration before mixing, because mic and loopback streams never end at the same millisecond.

4. OCR with zero extra installs

I wanted "select text from a screenshot" without telling users to install Tesseract.

Windows 10/11 already ships an OCR engine: Windows.Media.Ocr. You can reach it from PowerShell, so Python just shells out:

subprocess.run(
    ["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", ps1],
    capture_output=True, timeout=120,
    creationflags=0x08000000,   # CREATE_NO_WINDOW, no console flash
)
Enter fullscreen mode Exit fullscreen mode

The PowerShell script tries the user's language packs first and falls back to whatever recognizer is available. It runs in a QThread, so the UI never freezes. The result: no models to download, no 200 MB dependency, and it works offline.

Small things that made it feel finished

  • Recycle Bin, not delete. os.remove is permanent. SHFileOperationW with FOF_ALLOWUNDO sends files to the Recycle Bin, so "delete capture" is never scary.
  • Atomic settings. Write to settings.json.tmp, then os.replace(). A crash mid-write can't corrupt your config.
  • Single instance. A named mutex (CreateMutexW + check for error 183) stops two copies fighting over the same global hotkeys.
  • Autostart is one registry value under HKCU\...\Run. No installer needed.

Where all this ended up

All of the above lives in ASNIPP, a screenshot tool, screen recorder and editor that sits as a small "Dynamic Island" pill at the top of your screen. The parts I'm proudest of:

  • Screenshots with pen, highlighter, arrows, shapes, text and pixelate (for hiding sensitive info)
  • A color picker with a magnifier loupe, and a pixel ruler
  • Pin-to-screen floating captures
  • Screen recording with system audio and mic, plus live drawing that gets recorded into the video
  • A built-in video editor: trim, speed, reverse, color looks, fades, rotate and GIF export
  • OCR on any capture

It's one 7600+ lines of python code packed into .exe, PyQt6, mss for capture and OpenCV for video. If you've ever wondered how far a python desktop app can go, it's a decent case study.

πŸ‘‰ [https://github.com/Akhouri-Systems/ASNIPP/releases/download/v1.0.0/ASNIPP_v1.zip]


If you've built something with ctypes and Win32, I'd love to hear what bit you. Drop it in the comments.

Top comments (69)

Collapse
 
star123 profile image
Sᴛᴀʀ⭐ •

❀️

Collapse
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •

thanks star for comment
what about your channel?

Collapse
 
star123 profile image
Sᴛᴀʀ⭐ •

After Diwali πŸ˜€

Thread Thread
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •

oh okay channel name StarDev?

Thread Thread
 
star123 profile image
Sᴛᴀʀ⭐ •

Idk yet. Bro please share your github link.πŸ˜€

Thread Thread
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •
Thread Thread
 
star123 profile image
Sᴛᴀʀ⭐ •

Thanks πŸ₯°

Thread Thread
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •

my personal ones is not updated wait let me update it πŸ˜†

Thread Thread
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •

yeah star now I've updated my personal ones too.

Thread Thread
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •

@star123 can you suggest some upgrades, changes needed in my personal readme to make it look better?

Thread Thread
 
star123 profile image
Sᴛᴀʀ⭐ •

Very beautiful πŸ˜€

Thread Thread
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •

what?

Thread Thread
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •

@star123 if you want to join Akhouri Systems server then you can, currently it is not active or honestly it is dead because my goal is to first collect some top individuals in the server and then start the server 24/7 like big companyy servers are... and currently it has @effessdev, puneet kumar mishra, @nyaomaru @xulingfeng
discord.gg/Hb7Mk8vh5
Akhouri Systems is ready to welcome you 🀲

Thread Thread
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •

@effessdev bro can you come online in discord?

Thread Thread
 
effessdev profile image
EffessDev •

Ok...

Thread Thread
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •

ok..

Thread Thread
 
effessdev profile image
EffessDev •

You mean youtube channel right?

Thread Thread
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •

yeah star wants to open a channel so I was suggesting a name, like I also love these channel stuff But I failed but seeing her trying I'm motivated to help her.

Thread Thread
 
star123 profile image
Sᴛᴀʀ⭐ •

❀️

Thread Thread
 
effessdev profile image
EffessDev •

I also tried a couple of times (a guitar covers channel) but recording was too
hard without proper equipment so I gave up.

Thread Thread
 
star123 profile image
Sᴛᴀʀ⭐ • • Edited

So I give up.πŸ€”

Thread Thread
 
effessdev profile image
EffessDev •

Nonono lol that doesn't mean you should give up πŸ€¦β€β™‚οΈ

Thread Thread
 
technogamerz profile image
π“π‘πž π‹πšπ³π² 𝐆𝐒𝐫π₯ •

guitar covers sounds good! :D

Thread Thread
 
effessdev profile image
EffessDev •

yeah they sound REALLY good 😌🎢

Thread Thread
 
technogamerz profile image
π“π‘πž π‹πšπ³π² 𝐆𝐒𝐫π₯ •

Yeah
But this one.......

Thread Thread
 
effessdev profile image
EffessDev •

🫩

But is that available there as well? Wow!

Thread Thread
 
technogamerz profile image
π“π‘πž π‹πšπ³π² 𝐆𝐒𝐫π₯ •

My first Dream phone (mobile)πŸ₯Ί

Thread Thread
 
effessdev profile image
EffessDev •

🀣

Thread Thread
 
effessdev profile image
EffessDev •

Did you get it?

Thread Thread
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •

what?

Thread Thread
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •

oh wow are you guitar pro??

Thread Thread
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •

nah, I've experience of over 7 channels so I can guide you πŸ˜‚

Thread Thread
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •

@effessdev bro please come online discord.

Thread Thread
 
effessdev profile image
EffessDev •

what 7 channels? πŸ€”

Thread Thread
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •

yeah once I own 7 channels, all combined maybe 1200-1500 subscribers biggest channel was 450 subs I guess

Thread Thread
 
technogamerz profile image
π“π‘πž π‹πšπ³π² 𝐆𝐒𝐫π₯ •

only had one channel with 10k subscribers, which I deleted because of JEE.πŸ˜£πŸ˜”πŸ˜­πŸ˜­

Thread Thread
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •

woah woah woah whattttt 10kkkkk

Thread Thread
 
technogamerz profile image
π“π‘πž π‹πšπ³π² 𝐆𝐒𝐫π₯ •

I deleted it because my friend Imran said so. He said if we want to crack JEE, there should be zero distractions. It was actually a vlogging channel.

Thread Thread
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •

oh, by the way how's my new profile pic looking?

Thread Thread
 
technogamerz profile image
π“π‘πž π‹πšπ³π² 𝐆𝐒𝐫π₯ • • Edited

Bahut Acchi hai.❀️

Thread Thread
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •

bro come online discord

Thread Thread
 
effessdev profile image
EffessDev •

10k lol did is soo talented 🀯

Thread Thread
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •

yeahhhh peak IITians

Thread Thread
 
effessdev profile image
EffessDev •

Ok coming...

Thread Thread
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •
Thread Thread
 
effessdev profile image
EffessDev •

From where do you find all of this lol πŸ˜†

Thread Thread
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •

πŸ˜‚ Peak Anmol stuffs.

Thread Thread
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •

bro have you watched full 🀣

Thread Thread
 
effessdev profile image
EffessDev •

Skipped a little bit

Thread Thread
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •

hmm, peak busy person πŸ˜‘
me here unemployed watched same thing 30times

Thread Thread
 
effessdev profile image
EffessDev •

Yeah now I watched it full

Thread Thread
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •

yeah now we are both unemployed I guess hehehe

Thread Thread
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •

@technogamerz do you have discord?

Thread Thread
 
technogamerz profile image
π“π‘πž π‹πšπ³π² 𝐆𝐒𝐫π₯ •

No.

Thread Thread
 
effessdev profile image
EffessDev •

Wt? Then you told us to use Discord y?

Thread Thread
 
technogamerz profile image
π“π‘πž π‹πšπ³π² 𝐆𝐒𝐫π₯ • • Edited

Me? Never use discord!🀣

Thread Thread
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •

okay then make a account please and then I would share you akhouri systems server invitation link

Collapse
 
pengeszikra profile image
Peter Vivo •

Good to remember how can we will solve this kind of promblems! ✌️
I always fight with windows keyboard settings ( forced company laptop ) until I give up the fight. My trick: I use AutoHotkey program to redefine my keyboard to
capsLock + ; = Γ©
capsLock + ' = Γ‘
... and so on
Because my main problem is the hungarian languge special character. In chat with my hungarian collegues I like to use full hungarian char set, but I always use english keyboard set and really hate to install multiple language keyboard set because windows do not saw which keyboard currently use, plus many program using english keyboard shortkeys. The whole question is so anoying.

Collapse
 
amintacccp profile image
tamina •

The 0xE8 virtual-key injection is the detail that would have taken me days to find β€” swallowing S and then watching Start open on Win release is exactly the kind of bug that only shows up in real use. Same for the callback getting garbage-collected with no traceback. And stating outright that the hook only watches the keys it cares about, with nothing logged or stored, is the right call for a keyboard hook. Really nice write-up.

Collapse
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •

Thanks 😊

Collapse
 
amintacccp profile image
tamina •

You're welcome β€” and thanks again for the write-up, the callback GC gotcha alone was worth the read. 😊

Collapse
 
sameerqaiser17 profile image
Sameer Qaiser •

The line that hit me: "If HOOKPROC(cb) gets garbage-collected, your app crashes on the next keypress. There's no traceback, it just dies."

That is genuinely the scariest sentence in Python. I'm relatively new to Python, and the silent failures are what keep me up at nightβ€”just a flash of a console and then nothing.

The fact that you built a 7,600-line Win32 API app in pure Python is wild. The WASAPI loopback and the Windows OCR workarounds are incredibly useful. I tried recording system audio in Python once, hit a wall with sounddevice, and gave up.

Quick question: what was the biggest headache when packaging this into a .exe with PyInstaller? Did you have to do a lot of custom hooking to include the ffmpeg binaries and the PowerShell OCR scripts?

Massive respect for seeing this through to the end.

Collapse
 
koda2026 profile image
Harun - solo dev •

Bro please change profile pic πŸ–ΌοΈ 🀣

Collapse
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •

why πŸ˜‚ I know I look joker but still bro I want that me and akhouri systems both visible in profile hehehe but any more cool idea?

Collapse
 
technogamerz profile image
π“π‘πž π‹πšπ³π² 𝐆𝐒𝐫π₯ • • Edited

You changed your profile image! Wow!! :D

Collapse
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •

is that profile pic looking good?

Collapse
 
enoch_hou profile image
Enoch Hou •

ζŠŠι’©ε­γ€ε›žζ”Άη«™εˆ ι™€ε’ŒεŽŸε­ε†™ε…₯θΏ™δΊ›ε°η»†θŠ‚ιƒ½ε†™ε‡Ίζ₯δΊ†οΌŒθ―»θ΅·ζ₯εΎˆθΏ‡η˜ΎοΌŒζ”Άθ—δΊ†γ€‚

Collapse
 
mp3sss31 profile image
mp3sss •

This is exactly the gap we hit from the Electron side: globalShortcut.register wraps RegisterHotKey, which silently returns false on any Win+ chord the shell has reserved β€” so Win+Shift+S is genuinely unreachable from main without a native module that drops to WH_KEYBOARD_LL. The two pitfalls you called out map directly to Electron main-process work too: keeping the HOOKPROC callback referenced at module scope is the same rule as keeping a Tray object β€” let it GC and the next keypress takes the process down with no stack. The 0xE8 trick to mark Win as 'used in a combo' is the kind of shell-internals detail the docs never mention and that every low-level-hook port eventually rediscovers.

Collapse
 
onelifevibe profile image
OneLifeVibe •

The 0xE8 virtual-key injection was the detail I would never have found on my own β€” swallowing S but leaving Win to open the Start menu on release is exactly the kind of bug that looks like the code is haunted. The HOOKPROC garbage-collection gotcha is the other one: a crash with no traceback is brutal to debug, so keeping the callback alive and making the hook decision unit-testable is a really nice touch. I also liked the discipline of recording the mic and loopback to separate WAVs and mixing with ffmpeg amix afterwards instead of fighting two drifting clocks in real time. Did the built-in Windows.Media.Ocr hold up on low-contrast screenshots, or is that where you reach for a fallback?