Skip to main content

taskbargap

Platform: Windows 10/11 PyPI CI CodeQL OpenSSF Scorecard License: MIT

Find the empty stretch of the Windows taskbar, the gap between your app buttons and the system tray, and put a top-most window in it. Correctly, across multiple monitors and per-monitor DPI scaling.

Windows leaves that strip unused, so a small always-on widget there costs you no screen space. Doing it properly turns out to be fiddly. You have to detect the gap across two generations of taskbar internals, handle per-monitor DPI and the difference between physical and logical pixels, keep the window fitted as apps open and close, and end up with a top-most tool window that owns its input without grabbing a taskbar button of its own. This library does that, and nothing else.

Windows 10/11 only. It is built on Win32 APIs, and there is no macOS or Linux build. The package imports anywhere, but calling into it off Windows raises NotWindowsError.

Install

pip install taskbargap

The only dependency is the standard library (ctypes). It writes nothing at all: no files, no registry keys, no config, no logs. It makes no network calls. See SECURITY.md for the complete list of Win32 calls it makes.

This is an alpha, and it says so on the tin. Before you rely on it, read what has and hasn't been validated, which is specific about the configurations it has never run on.

Quickstart

import taskbargap

taskbargap.enable_dpi_awareness()          # call once, before creating windows

gap = taskbargap.find_gap()                # -> Gap | None
if gap:
    print(gap.left, gap.right, gap.width, gap.scale)

# Place a window (by HWND) into the gap, right-aligned near the tray:
taskbargap.place(my_hwnd, align="right", margin=12)

To keep it fitted as the taskbar changes, when apps open and close, when Explorer restarts, when the resolution changes:

watcher = taskbargap.GapWatcher(my_hwnd, align="right")
watcher.start()          # places it now, then re-fits on change and re-asserts top-most
# ...
watcher.stop()

start() places the window immediately, on your thread, and returns. The polling happens on a daemon thread afterwards. Pass on_change=fn if you want to be told when the gap moves, including when it gets too narrow to fit, which is usually when an app wants to get out of the way.

API

Object Purpose
find_gap() -> Gap | None Detect the empty taskbar gap on the primary monitor. None if there isn't a usable one.
place(hwnd, *, align="right", margin=12, min_width=160, width=None, height=None, gap=None, nonblocking=False) -> bool Size and position an existing window inside the gap as a top-most tool window. width defaults to filling the gap, height to the taskbar's own height. False means it did nothing: no gap, min_width didn't fit, or Windows refused the move.
GapWatcher(hwnd, *, align, margin, min_width, width, height, interval=1.0, on_change=None) Background watcher that keeps the window fitted and top-most as the taskbar changes. .start() / .stop() -> bool, or use it as a context manager.
enable_dpi_awareness() -> bool Opt into per-monitor-v2 DPI awareness. Falls back gracefully on old Windows.
taskbar_items() -> list[tuple[int, int, int, int]] | None The screen rects (physical px) of every button the Windows 11 taskbar draws: Start, Search, Task View, each app, Widgets. None off Windows 11 or when the shell isn't answering. This is what find_gap() measures against; exposed so you can show why the gap is where it is.
Gap Frozen dataclass: left, right, top, bottom (physical px), scale (DPI factor), monitor (HMONITOR), measured, plus width / height.
NotWindowsError Raised by any Win32 call when you're not on Windows.

Units, and the DPI contract

Every coordinate you get back is a physical pixel in the coordinate space your process actually sees. Gap.scale is the divisor for toolkits that scale window position by DPI, such as pywebview and WinForms:

x_logical = round(gap.left / gap.scale)

Call enable_dpi_awareness() before you create any window. Without it Windows virtualises coordinates (a 3840px screen at 175% looks 2194px wide) and a window aimed at the gap lands somewhere else.

Whatever you do, scale describes the space your process addresses windows in, rather than the monitor's spec sheet. That means 1.0 for a DPI-unaware process, the system DPI for a system-aware one, and the taskbar monitor's own factor only for a per-monitor-aware one. The distinction is not academic: GetDpiForWindow will happily report the taskbar's real 175% to an unaware caller that sees a 2194px desktop, and dividing by that number puts the window a third of the screen away from where it belongs. The library stays self-consistent in every mode. It is only in per-monitor mode that you get real screen pixels and an unscaled window.

What it deliberately does not do

  • No metrics, rendering, or UI. You bring the window, it does the placement.
  • No decision about whether to be visible. Fullscreen-hide and yielding to a crowded taskbar are app policy, and find_gap() gives you the facts to decide.
  • No cross-platform panels. Windows taskbar only.

Honest limitations

  • On Windows 11 the gap is measured from the buttons the taskbar actually draws; elsewhere it is a corroborated reading or a guess, and measured=False is not a safe edge. Windows 11 draws its taskbar in XAML, and the legacy ReBarWindow32 / MSTaskListWClass windows there report a width that can be wrong rather than missing: too narrow to hold the buttons they contain, and blind to the Widgets button, which in a left-aligned layout sits a few pixels left of the clock. The library therefore reads the taskbar's own UI Automation tree, which has one element per drawn button with its screen rectangle, and returns a span between those elements with measured=True. Where there is more than one empty span it takes the rightmost unless that is a sliver (under 40% of the widest), so a left-aligned bar gives the space between the last app button and Widgets, and a centred bar the space right of the cluster. The reading is cached for 3s unless something the cheap reads can see has changed, and costs ~20-30ms when taken.

    Without that tree (Windows 10, Server, a replacement shell, Explorer mid-restart) the legacy windows are used. Their edge is checked against the number of windows that must own a button, and when the arithmetic is impossible you get measured=False plus the more conservative of the two estimates. That is honesty, not safety. A measured=False edge is still a guess and may still overlap buttons. If covering an icon would be unacceptable for your app, treat measured=False as "don't draw here" rather than "draw here carefully".

  • Rects are sanity-checked, so detection degrades rather than lies. A taskbar child reporting a dead or off-bar rectangle, which happens while Explorer is restarting, is ignored rather than believed, and you get the heuristic with measured=False. A plausible edge that leaves no room is respected: a genuinely full taskbar returns None, because inventing a gap there would cover buttons.

  • Primary taskbar only. Secondary monitors get their own Shell_SecondaryTrayWnd bars, and v0.1 reads the primary Shell_TrayWnd. The gap you get is on whichever monitor that taskbar is on, at that monitor's DPI.

  • Horizontal taskbars only. A taskbar docked left or right, which Windows 10 allows, has no horizontal gap worth speaking of, so find_gap() returns None rather than guess.

  • The watcher needs your app to pump messages. It posts its moves rather than sending them, so a busy UI thread can never block it. The flip side is that a re-fit only lands the next time your app processes messages. Normal GUI apps do that constantly, but a wedged one won't move.

  • Auto-hide taskbars are not special-cased. You get the gap of the bar wherever it currently is, mid-slide included.

License

MIT.

Metadata

Release files for taskbargap 0.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for taskbargap 0.2.1
File Size Uploaded Provenance
taskbargap-0.2.1.tar.gz 42.6 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for taskbargap 0.2.1
File Interpreter ABI Platform
taskbargap-0.2.1-py3-none-any.whl Python 3 none any Details

Total release size: 71.6 kB

Release files / taskbargap-0.2.1.tar.gz

Download URL taskbargap-0.2.1.tar.gz
Size 42.6 kB
Tags Source
SHA-256 checksum
How to use checksums
75be9c737f1986e3d8dab8969d856a02bf8351dbe0421545b2af461f3865650c
BLAKE2b-256 checksum
How to use checksums
249af03774395a0488d3b8f672c8eb485c9a87dcb09d81c979207486ed8a9d2f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

Release files / taskbargap-0.2.1-py3-none-any.whl

Download URL taskbargap-0.2.1-py3-none-any.whl
Size 29.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a4dca2219627e3cf1d364241e2f490ee57c742d19ab5f50c452c77467ef452d2
BLAKE2b-256 checksum
How to use checksums
4131b349ffafbbba101d9ec18c1ae94c25fecd713132fbf2b77e88afc7a54d3d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

Security for taskbargap 0.2.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for taskbargap-0.2.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page