Windows automation in Python, with a visible cursor of intent
Every Python automation script for Windows you have ever written ran blind. The cursor flickers, a window pops up, something happens, and you have no way to see which control your script actually grabbed. Terminator's Python binding ships element.highlight(), a real Win32 GDI overlay that draws a colored, click-through border around any element your script touches, with a text label where you want it. The visual debugger pywinauto never had.
What every other guide on this topic skips
Open the existing playbooks for Windows automation in Python and you will find the same four libraries: pywinauto for the UI tree, pyautogui for raw mouse and keyboard, pywin32 for COM, and the keyboard library for global hotkeys. They all work, they have all worked for a decade, and they all expect you to debug by reading stdout.
That is a strange place to be in 2026. We have desktop windows that can render anything. We have GPUs that can paint a million rectangles per frame. And we are still typing print(child.element_info.control_type, child.rectangle()) to figure out which button the script grabbed. The script knows where it is clicking. The screen does not show it.
Terminator fixes that with a single method on every UIElement. The method exists because the Rust core spawns a real layered transparent topmost window, paints a GDI border into it, optionally labels it, and tears it down on a timer. From Python you call it like any other method. The first time you run a script with .highlight() calls before each action, you stop print-debugging forever.
The one method this whole page is about
This is a real Python script you can paste into a file and run on a Windows machine with Notepad installed. It opens Notepad, finds the editor area, paints a green border with a white "this is where I will type" label above it, and types into the document. Watch your screen.
“WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOPMOST | WS_EX_TOOLWINDOW”
The exact CreateWindowExW flag set used by the highlight overlay, at crates/terminator/src/platforms/windows/highlighting.rs line 427
What happens between Python and the screen
When you call element.highlight(...) from Python, the PyO3 binding at packages/terminator-python/src/element.rs:449 drops the GIL and forwards the call into the Rust core. The Rust core spawns a background thread that creates a real Win32 overlay window, paints the border and optional text, and waits for the duration to expire (or for HighlightHandle.close() to be called from Python).
element.highlight(...) end to end
The numbers worth knowing
A quick read of the surface area, in case you are scanning.
Install and prove it works
One pip command, one one-liner that opens Notepad and flashes a highlighted label on it. If you see a red border with the word Notepad in white text above the window, you are wired up.
From pip install to a script you can read off the screen
Install the wheel
pip install terminator-py. PyPI ships pre-built wheels for Python 3.10, 3.11, 3.12 on Windows. The import name is terminator, the package name has a hyphen.
Open an app, grab a UIElement
desktop = terminator.Desktop(); window = desktop.open_application('notepad'). open_application returns the top-level UIElement for the window. Every locator hangs off that.
Highlight before you act
element.highlight(color=0x00FF00, duration_ms=500, text='click') paints a green border with a label. Then call .click(). The label is your script telling itself, and you, what it thinks it is doing.
Use BGR colors per intent
Pick a color convention: green for clicks, cyan for typed text, magenta for windows, orange for waits. Reading the highlight stream off the screen replaces print debugging.
Switch to recording mode for capture
When recording user actions for a workflow, terminator.set_recording_mode(True) suppresses the implicit scroll_into_view so highlights are non-disruptive. Flip it back to False when you are done.
The trap nobody warns you about: BGR, not RGB
The color argument is a Win32 COLORREF, which is BGR-packed. Pure red is 0x0000FF, pure blue is 0xFF0000. The constant DEFAULT_RED_COLOR: u32 = 0x0000FF; at highlighting.rs line 125 carries the comment "Pure red in BGR format" so the surprise is at least documented in the source.
A useful convention: pick a color per kind of action and never break it. Green for clicks, cyan for typed text, magenta for windows, orange for waits. Reading the highlight stream off the screen becomes a debug log you do not have to print.
The nine places a label can sit
Each call to terminator.TextPosition returns a position object you pass into text_position. The match arms that compute the offset for each anchor live at highlighting.rs:182-190.
The full feature surface
Everything that distinguishes this from a printf and a prayer.
Real Win32 overlay window
Created with CreateWindowExW under the WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOPMOST | WS_EX_TOOLWINDOW flags (highlighting.rs line 427). Painted with GDI Rectangle and FillRect. Cleaned up by cleanup_overlay_window when the duration expires.
Click-through by design
WS_EX_TRANSPARENT makes the overlay invisible to hit testing. Your next click goes through it. SW_SHOWNOACTIVATE keeps focus where it was.
BGR colors, not RGB
Native COLORREF format. 0x0000FF is red, 0xFF0000 is blue. If your highlight came up in the wrong color, swap the first and last byte.
Nine text positions
TextPosition.top(), top_right(), right(), bottom_right(), bottom(), bottom_left(), left(), top_left(), inside(). Each one resolves to a hand-coded offset (highlighting.rs lines 182 to 190).
HighlightHandle for early dismissal
highlight() returns a Python object. Call .close() to take the overlay down before the duration expires. Useful for waits that resolve early.
Recording mode
set_recording_mode(True) flips an atomic boolean (highlighting.rs line 60) that suppresses scroll_into_view, so the overlay can mirror user actions without disturbing the UI.
The HighlightHandle, for waits that finish early
A common pattern is to highlight an element while you wait for something else (a server response, a file to land on disk, a spinner to disappear). When the wait completes you want the overlay gone immediately, not after the original duration. highlight() returns a Python HighlightHandle whose close() method dismisses the overlay and joins the background thread.
Recording mode, for highlights that do not move the UI
By default, highlight() calls scroll_into_view() on the element before painting, so off-screen controls scroll into view to be visible. That is the right behavior when you are actively debugging. It is the wrong behavior when you are passively recording user actions and the highlight is just a visual echo: forcing scroll changes the very thing you are trying to record.
The fix is one global atomic boolean. terminator.set_recording_mode(True) flips RECORDING_MODE at highlighting.rs line 39 to true. While that flag is set, highlight() skips the scroll step entirely and draws the overlay where the element currently is, even if that is off screen.
Watch the call cross the boundary in three frames
Python -> PyO3 -> Win32 -> the screen
Frame 1: Python invokes the binding
Python calls element.highlight(color=0x00FF00, duration_ms=500, text='Save'). The PyO3 wrapper at packages/terminator-python/src/element.rs:449 unpacks the optional arguments, releases the GIL, and forwards into the cross-platform UIElement.
Three numbers worth memorizing
text positions for the label, from TextPosition.top() all the way around to TextPosition.inside().
extended-style flags on the overlay HWND: LAYERED, TRANSPARENT, TOPMOST, TOOLWINDOW. Click-through, focus-safe, never in the taskbar.
default duration when duration_ms is omitted, set inside the spawned thread at highlighting.rs line 161.
Terminator's highlight versus everything else
| Feature | pywinauto / pyautogui / pywin32 | terminator-py |
|---|---|---|
| Visible border around the element being touched | No (pyautogui prints to stdout) | element.highlight(color, duration_ms, text) |
| Survives a desktop redraw | pywinauto draw_outline gets erased the moment another window paints | Real layered topmost window, lives the full duration |
| Text label on the highlight | Not supported | Up to 30 chars, 9 anchor positions, custom font |
| Click-through (does not block input) | n/a | WS_EX_TRANSPARENT, the overlay never steals clicks |
| Early dismissal handle | Not supported | highlight() returns a HighlightHandle with .close() |
| Passive recording mode | Not supported | set_recording_mode(True) suppresses scroll_into_view |
| Same call shape on macOS and Linux | Windows-only | element.highlight(...) is cross-platform PyO3 |
Want to see your Windows automation script paint itself on screen?
Book 20 minutes. We will wire terminator-py into a real Python automation of your choice and turn print-debugging into highlights.
Frequently asked questions
What does element.highlight() actually draw on the screen?
A real Windows overlay window. The implementation lives in crates/terminator/src/platforms/windows/highlighting.rs at the highlight function on line 65. It calls CreateWindowExW with the flags WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOPMOST | WS_EX_TOOLWINDOW (line 427), then paints a colored rectangle border around the element using GDI primitives (Rectangle, FillRect, DrawTextW). The window is click-through because of WS_EX_TRANSPARENT, so it never steals focus or interferes with your automation. When the duration expires the overlay window is destroyed and cleaned up by cleanup_overlay_window.
What Python call do I use?
element.highlight(color=None, duration_ms=None, text=None, text_position=None, font_style=None). The signature is at packages/terminator-python/src/element.rs line 449. All arguments are optional. With no arguments you get a 3-second red border. Pass text='Save button' to add a label, text_position=terminator.TextPosition.bottom() to move it, font_style=terminator.FontStyle(size=18, bold=True, color=0xFFFFFF) to style it. The call returns a HighlightHandle whose close() method dismisses the overlay early.
Why are the colors backwards from RGB?
The color argument is a 32-bit BGR integer, not RGB, because that is the native format Windows GDI uses internally for COLORREF. Pure red is 0x0000FF, pure green is 0x00FF00, pure blue is 0xFF0000. The default is red (0x0000FF) — the constant DEFAULT_RED_COLOR is at highlighting.rs line 125 with the comment Pure red in BGR format. If you ever see your highlight come up in a color you did not expect, swap the first and last byte.
Where can the text label sit relative to the element?
Nine positions, defined as classmethods on terminator.TextPosition: top(), top_right(), right(), bottom_right(), bottom(), bottom_left(), left(), top_left(), and inside(). The match arms that compute the text rectangle for each position are at highlighting.rs lines 182 to 190. inside() draws the label 15 pixels inside the element bounds, top() draws it 10 pixels above, the diagonals push 15 pixels diagonally out, and so on. There is no rotation, only the nine anchor points.
How is this different from pywinauto's draw_outline?
pywinauto's draw_outline calls win32gui in pure Python on the desktop DC. It paints directly onto the desktop without creating a window, so the rectangle disappears the moment Windows redraws that screen region (a tooltip, a hover highlight on another window, even a mouse move). Terminator's highlight creates a layered transparent topmost window, so the overlay survives screen redraws for the full duration. It also supports text labels with size, bold, and color font styling, multi-position anchoring, and a returned HighlightHandle for early dismissal. None of those are in pywinauto.
Will the overlay block my next click?
No. The window is created with WS_EX_TRANSPARENT, which makes it click-through at the OS hit-testing level. Your next desktop.locator(...).click() goes straight to the underlying control. The overlay is also drawn with SW_SHOWNOACTIVATE, so it never grabs focus. You can stack a highlight on top of a real button and click the real button without dismissing anything.
What is set_recording_mode(true) for?
By default, highlight() calls scroll_into_view() on the element before painting, so off-screen controls scroll into view to be visible. That is good for live debugging but bad if you are recording a passive workflow and want to highlight whatever the user touches without disturbing the UI. The atomic flag set_recording_mode(true) (highlighting.rs line 60) globally suppresses the scroll. The recording subsystem flips it on, draws non-disruptive highlights of every event, and flips it off when recording ends. Same primitive, two modes.
Does it work the same way on macOS and Linux?
The Python API does. The same element.highlight(color=..., text=..., duration_ms=...) call is exposed cross-platform through the PyO3 binding. The Windows implementation is the GDI overlay window described here. The macOS and Linux backends paint through their own native compositors with matching semantics. If you write your debugging in terms of element.highlight(...), the script ports without changes; the visual will look slightly different because the underlying compositor is different, but the contract is the same.
What about element.highlight_before_action — does that exist for Python too?
It exists in the Rust core (crates/terminator/src/element.rs line 1351) and runs automatically before clicks and types when verbose action mode is enabled in the MCP server and CLI. It flashes a 500ms green (BGR 0x00FF00) border with the element's role as a top-anchored 12pt bold white label. From Python you get the equivalent by composing the primitives: handle = element.highlight(color=0x00FF00, duration_ms=500, text=element.role(), text_position=terminator.TextPosition.top()) right before your click. That is the same signature the Rust helper builds.