Guideelement.rs:714

A TV to use as a computer monitor changes more than your screen size. It changes the coordinate space your scripts see.

Every guide in the top 5 results stops at HDMI cables, input lag, and OLED burn-in. None of them tell you that the moment your TV becomes display 2, an off-by-one pixel on its left edge is enough for an AI agent to think it clicked the laptop when it actually clicked the TV. Here is the exact 95 lines of Rust in Terminator that own the routing, and what they mean for the way you set up your second display.

M
Matthew Diakonov
9 min read
4.9from 2.1k stars on GitHub
Cross-platform: Windows UIA + macOS AX
Per-element Monitor object
MIT licensed

What hardware blogs cover, and what they leave out

The top 5 search results for "tv to use as computer monitor" (RTINGS, HP, Best Buy, TCL, Lenovo) all walk through the same checklist: pick a panel with low input lag, confirm chroma 4:4:4 over HDMI, watch out for OLED image retention, and stay at least 4 ft from a 65-inch screen so individual pixels stop being visible. That advice is correct and you should follow it.

What is missing is the software half: the moment Windows or macOS enumerates your TV as a second display, the system gives it a rectangle in a virtual coordinate space. Every window manager, every accessibility tool, every screen recorder, and every desktop automation framework has to decide which physical screen owns each UI element. That decision determines whether your AI agent clicks the right button when half your apps are on the TV.

How Terminator routes a click when you have a laptop + TV

element.bounds()
xcap::Monitor::all()
scale_factor
monitor()
Laptop
TV

The exact code that decides which screen owns the click

This is the body of the function that runs every time Terminator touches a UI element on a multi-monitor setup. Read it once and you will understand why a TV is not a passive display surface, it is an active participant in coordinate routing.

crates/terminator/src/element.rs (lines 714-809)
Anchor fact

The strict less-than on the right edge, and the silent fallback when nothing matches

Two facts about this function that are not in any TV-as-monitor guide on the web. First, the range check uses <, not <=, on the right and bottom edges. A pixel that lands exactly on the right edge of your laptop belongs to the next monitor over, which on a typical layout is the TV. Second, when the element's bounds do not fall inside any monitor (common when a window remembers its position from when the TV was plugged in but the TV is now off), the function silently routes the click back to the primary monitor.

x == 2560

At a single off-by-one pixel on a 2560-wide laptop, the click is reported as on the TV instead of the laptop.

element.rs:748 — within_x = (x as i32) >= mon_x && (x as i32) < mon_x + mon_w

crates/terminator/src/lib.rs (lines 310-316)

The companion helper Monitor::contains_point(x, y) ships the same rule as a public method, so you can predict where Terminator will route an action before you fire it. Same semantics, no surprises. This is the part the hardware blogs cannot tell you because it lives in code, not a spec sheet.

Six things this 95-line function actually guarantees

Read in isolation it looks like a one-pass loop. Read in context of a desktop with a TV plugged in, it makes a series of opinionated choices about ambiguity. Here is what each one means in practice.

Upper-left corner is the only thing that counts

The check at element.rs:748 only tests one point: the element's top-left (x, y). A 600 px wide window straddling laptop and TV is owned by whichever monitor contains its upper-left pixel. The other 599 px do not vote.

Strict less-than on the right edge

(x as i32) < mon_x + mon_w. Not <=. A click at x=2560 on a laptop that is 2560 px wide does NOT belong to the laptop. It belongs to monitor_1, the TV that starts at x=2560.

Iteration order is monitor enumeration order

The loop at element.rs:732 returns the FIRST monitor whose rectangle contains the point. If monitors overlap (mirror-mode misconfiguration), the first one wins. id is just format!("monitor_{idx}"), so unplugging and replugging can renumber.

No monitor matched? Silent primary fallback

When the element is off-screen because the TV got unplugged but the window position was remembered, element.rs:812 routes the click back to the primary display without erroring. The action lands somewhere visible.

Per-monitor scale_factor on the Monitor struct

Monitor struct at lib.rs:288 carries scale_factor: f64 alongside x/y/width/height. Laptop at 1.25, TV at 1.0 is reported faithfully. Scripts that need physical pixels can multiply through.

contains_point() helper exposes the same rule

lib.rs:311 ships the strict-less-than check as a public Monitor.contains_point(x, y). Use it to predict where Terminator will route an action before you fire it. Same semantics, no surprises.

By the numbers, just for this one decision

The full routing logic is small. Small enough to read in one sitting, small enough to model in your head while debugging a misrouted click.

0Lines of Rust in element.rs:714-809
0Pixel of the element used to decide ownership
0Fallback paths to the primary monitor
0Per-monitor branches in CreateWindowExW

0 lines, one upper-left corner, two fallback paths (bounds() failure at line 720 and no-monitor-matched at line 808), and zero per-monitor special casing in the overlay window because the virtual-screen rectangle already covers everything.

The same routing in a flow you can trace by eye

One click, end to end

1

Click

AI agent calls window.click() on a UI element

2

bounds()

Get the element's (x, y) top-left corner

3

Monitor::all()

Enumerate every physical display via xcap

contains?

First mon whose rect contains (x, y) wins

5

Route

Click lands on Monitor.id (laptop or TV)

Watch the routing happen on a real desktop

Terminator ships examples/monitor_example.py, a 38-line script that walks every Window the OS reports and asks which monitor it is on. Run it once with the TV unplugged and once plugged in. The same window comes back with a different monitor.id and origin. No code change.

examples/monitor_example.py
Before vs after the TV

Hardware blogs vs software-aware setup

The two stories complement each other. The hardware story is what to buy. The software story is what to expect once it is plugged in and your scripts start firing.

FeatureHardware-only guideSoftware-aware setup
What 'TV as monitor' guides coverHDMI 2.1, 4K @ 120 Hz, chroma 4:4:4, OLED burn-inAll of the above plus how scripts route a click to the TV
Window dragged exactly to the right edge of the laptopVisually it looks like it is on both screenselement.rs:748 strict-less-than: x == 2560 reports as TV
Window remembered position from when TV was plugged in, TV now offWindow appears off-screen, user has no idea where it isget_primary_monitor_fallback() at element.rs:812 reroutes to laptop
How a click is targeted on the TVGeneric recorder records pixel coords on primary monitor onlyPer-element bounds + monitor.id, replays correctly across layouts
Picking the screen for a screenshotPrintScreen captures the primary monitor, TV side missingdesktop.capture_monitor(monitor) takes any monitor by id (lib.rs:730)
Cross-platform behaviorWindows-only utilities, manual rewrite for macOSSame monitor() trait method on Windows UIA + macOS AX adapters

From box-fresh TV to a coordinate space your scripts can trust

Five steps. The first two are exactly what RTINGS or HP would tell you. The last three are what they leave out.

1

1. Pick a TV that doesn't lie about its panel size

The hardware checklist still matters. RTINGS recommends the LG C5 OLED 42-inch for desktop use because at 4 ft viewing distance, 4K stretched over 42 inches gives a per-pixel density close to a real monitor. Avoid 65-inch panels at 2-ft distance: pixels become visible.

2

2. Confirm low input lag and chroma 4:4:4 in the TV menu

Switch the HDMI input to 'PC' or 'Game' mode. Without it, your TV will smear text. Chroma 4:4:4 is the difference between crisp anti-aliased fonts and color-fringed garbage. This is the part HP, TCL, and Lenovo all cover.

3

3. Plug it in, then read the new coordinate space

On Windows, GetSystemMetrics(SM_CXVIRTUALSCREEN) jumps to laptop_width + tv_width. On macOS, NSScreen.screens grows by one. On both, every UI element you query now reports a Monitor with a non-zero origin if it lives on the TV.

4

4. Run examples/monitor_example.py to see the routing

Walks every visible Window via desktop.locator('role:Window').all() and prints monitor.name, monitor.id, and origin. This is the easiest way to confirm a window is on the screen you think it is, before any automation touches it.

5

5. Plan around the strict-less-than edge

If you script multi-monitor layouts, never assume an element exactly on the boundary belongs to the left monitor. Snap your windows a few pixels inside the desired display, or query monitor() directly and assert before clicking.

Why a desktop automation framework cares about your TV

Terminator is not a consumer app. It is the framework your AI coding assistant uses to control your whole OS, the way Playwright controls a browser. Every UI element it touches goes through the routing function above. The reason this page exists is that a second display is the most common way for an automation flow to silently misroute and produce results that look right in logs but land on the wrong screen.

Frequently asked questions

Is a TV as a computer monitor a problem for desktop automation tools or AI agents?

Only if the tool is hardcoded to the primary monitor. Anything that uses an accessibility-API based engine and routes through a per-element monitor() call (Terminator, native screen-readers, well-built recorders) handles a second display correctly. Pixel recorders that replay raw screen coordinates from a single-monitor recording will misfire because the virtual screen origin and dimensions changed.

Why does my window 'belong' to the TV when it looks like it is on the laptop?

Because Terminator (and most window managers) decide ownership by the upper-left corner pixel only. element.rs:748 in Terminator does (x as i32) >= mon_x && (x as i32) < mon_x + mon_w. If you dragged the window so its top-left landed at exactly x = 2560 on a 2560-wide laptop, Terminator reports the window on monitor_1 (the TV) even though most of it is visible on the laptop. The right edge of every monitor is exclusive.

I unplugged the TV and now a window is invisible. Will Terminator still find it?

Yes. element.rs:812 implements get_primary_monitor_fallback(). When the loop in monitor() does not find any monitor whose rectangle contains the element's top-left corner, the code logs a warning and returns the primary monitor instead of erroring. So a click on an off-screen window still resolves to a visible display. You will see the warning in the log, which is the signal that the window position is stale.

Does the per-monitor scale factor matter when I am scripting against a TV?

Yes. The Monitor struct at lib.rs:288 carries scale_factor: f64 alongside x, y, width, and height. A common laptop + TV combo is 1.25x scale on the laptop and 1.0x on the TV. The OS expresses element bounds in DIPs, but raw screenshot pixels and absolute mouse coords land in physical pixels. If you mix the two, multiply through scale_factor on the relevant Monitor.

What hardware specs should I prioritize for a TV used as a computer monitor?

Independently of any automation question: low input lag (under 30 ms in PC/Game mode), chroma 4:4:4 support over the HDMI version your GPU supports, and a wide viewing angle. RTINGS currently recommends the LG C5 OLED, Samsung S95F, and TCL QM7K for desktop use. Stay 4 ft or further away for 65-inch panels to avoid seeing individual pixels. OLED can image-retain when static UI sits on screen for hours, so wiggle the windows occasionally.

Will my AI agent's clicks still land on the right place after I plug the TV in?

If the agent calls a per-element click (window.click(), button.click(), element.click()) it works without code changes, because Terminator re-resolves the bounds and the owning monitor at call time. If the agent stored absolute pixel coordinates from before the TV was plugged in, those coordinates may now point to a different monitor (or, after an unplug, to nowhere). Re-resolve elements after a display configuration change, or use monitor.contains_point(x, y) to check before firing.

Can I capture a screenshot of just the TV and not the laptop?

Yes. desktop.capture_monitor(&monitor) at lib.rs:730 takes a Monitor reference and returns a ScreenshotResult for that monitor only. List monitors with desktop.list_monitors(), pick the one whose name matches your TV (e.g. 'LG TV SSCR2'), and pass it in. There is also capture_all_monitors() at lib.rs:760 which returns one screenshot per display in a single call.

Build automations that survive a second display

Terminator gives your AI coding assistant a Playwright-shaped API for the whole OS, including correct per-element monitor routing on Windows UIA and macOS AX. Open source, MIT licensed.

Star Terminator on GitHub
terminatorDesktop automation SDK
© 2026 terminator. All rights reserved.