Raise the browser the extension reported, and add a diagnostics script

focus.py raised the first browser in its list that happened to be running. On a
machine with both Chrome and Edge installed that is a coin flip, and losing it is
silent: coordinates measured from a tab in one browser, the click delivered into
a window of the other. It presents as selectors failing for no reason. A VPS with
both installed hit exactly this.

The extension now reports which browser is hosting it, and that travels with the
measurement, so the clicker raises the browser the coordinates actually came
from. Asking for a browser that is not running now fails honestly instead of
quietly raising a different one, and the verification step rejects the wrong
browser coming forward. Chromium, Opera and Vivaldi are recognised alongside
Chrome, Edge and Brave, on both platforms.

diagnose.py answers the question a remote desktop makes hard: whether the mouse
is really moving or the viewer simply is not drawing it. It moves the cursor and
reads the position back from the OS, so the answer does not depend on anything
being rendered, and it reports DPI mode, screen size, whether this is an RDP
session, and whether the browser can be raised at all. Nothing is clicked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-08-30 14:13:18 -05:00
co-authored by Claude Opus 5
parent 731fafda0e
commit 3ac9fe060f
7 changed files with 247 additions and 39 deletions
+78 -34
View File
@@ -16,20 +16,59 @@ import subprocess
import sys
import time
# Chrome ships under several bundle ids; accept whichever is installed.
MAC_BUNDLES = (
"com.google.Chrome",
"com.google.Chrome.beta",
"com.google.Chrome.dev",
"com.google.Chrome.canary",
"com.brave.Browser",
"com.microsoft.edgemac",
)
# Per browser, how to recognise it on each platform. The extension reports which
# one is hosting it, because picking by list order raises the wrong browser as
# soon as two are installed — and then clicks land in a window the coordinates
# were never measured from.
BROWSERS = {
"chrome": {
"darwin": ("com.google.Chrome", "com.google.Chrome.beta",
"com.google.Chrome.dev", "com.google.Chrome.canary",
"org.chromium.Chromium"),
"win32": ("chrome.exe",),
"titles": ("Chrome", "Chromium"),
},
"edge": {
"darwin": ("com.microsoft.edgemac",),
"win32": ("msedge.exe",),
"titles": ("Edge",),
},
"brave": {
"darwin": ("com.brave.Browser",),
"win32": ("brave.exe",),
"titles": ("Brave",),
},
"opera": {
"darwin": ("com.operasoftware.Opera",),
"win32": ("opera.exe", "launcher.exe"),
"titles": ("Opera",),
},
"vivaldi": {
"darwin": ("com.vivaldi.Vivaldi",),
"win32": ("vivaldi.exe",),
"titles": ("Vivaldi",),
},
}
# Windows browsers, matched on the owning process. A title match would also hit
# an editor with chrome.js open or a folder named Chrome; a process name cannot
# collide that way.
WINDOWS_PROCESSES = ("chrome.exe", "msedge.exe", "brave.exe")
def _ids_for(browser: str | None) -> tuple[str, ...]:
"""Identifiers to accept on this platform. Without a named browser, every
known one — the old behaviour, and still right on a single-browser box."""
key = "darwin" if sys.platform == "darwin" else "win32"
if browser and browser in BROWSERS:
return BROWSERS[browser][key]
return tuple(i for b in BROWSERS.values() for i in b[key])
def _titles_for(browser: str | None) -> tuple[str, ...]:
if browser and browser in BROWSERS:
return BROWSERS[browser]["titles"]
return tuple(t for b in BROWSERS.values() for t in b["titles"])
# Kept for callers that just want "any known browser".
MAC_BUNDLES = _ids_for(None) if sys.platform == "darwin" else BROWSERS["chrome"]["darwin"]
WINDOWS_PROCESSES = BROWSERS["chrome"]["win32"] + BROWSERS["edge"]["win32"] + BROWSERS["brave"]["win32"]
SETTLE = 0.20 # let the window manager finish raising before measuring or clicking
@@ -103,12 +142,10 @@ def _mac_workspace():
return NSWorkspace.sharedWorkspace()
def browser_ids() -> tuple[str, ...]:
"""What counts as "the browser" on this platform."""
if sys.platform == "darwin":
return MAC_BUNDLES
if sys.platform == "win32":
return WINDOWS_PROCESSES
def browser_ids(browser: str | None = None) -> tuple[str, ...]:
"""What counts as "the browser" on this platform, optionally narrowed to one."""
if sys.platform in ("darwin", "win32"):
return _ids_for(browser)
return ()
@@ -138,7 +175,7 @@ def frontmost() -> str | None:
return None
def _mac_activate() -> FocusResult:
def _mac_activate(browser: str | None = None) -> FocusResult:
ws = _mac_workspace()
if ws is None:
# pyobjc's AppKit isn't present. osascript works but may prompt for
@@ -153,7 +190,7 @@ def _mac_activate() -> FocusResult:
return FocusResult(False, f"could not activate Chrome ({exc})")
running = {a.bundleIdentifier(): a for a in ws.runningApplications()}
for bundle in MAC_BUNDLES:
for bundle in _ids_for(browser):
app = running.get(bundle)
if app is None:
continue
@@ -162,10 +199,11 @@ def _mac_activate() -> FocusResult:
app.activateWithOptions_(1 << 1)
return FocusResult(True, f"activated {bundle}")
return FocusResult(False, "no Chrome-family browser is running")
wanted = browser or "any known browser"
return FocusResult(False, f"{wanted} is not running")
def _is_browser_window(win) -> bool:
def _is_browser_window(win, browser: str | None = None) -> bool:
"""Match on the owning process where we can, title only as a fallback.
A title match alone catches an editor with chrome.js open, or a folder window
@@ -175,13 +213,13 @@ def _is_browser_window(win) -> bool:
try:
name = _win_process_name(win._hWnd)
if name:
return name in WINDOWS_PROCESSES
return name in _ids_for(browser)
except Exception:
pass # fall through to the title check
return bool(win.title) and "Chrome" in win.title
return bool(win.title) and any(t in win.title for t in _titles_for(browser))
def _other_activate() -> FocusResult:
def _other_activate(browser: str | None = None) -> FocusResult:
"""Windows (and any platform pygetwindow supports)."""
try:
import pygetwindow
@@ -189,14 +227,15 @@ def _other_activate() -> FocusResult:
return FocusResult(False, "pygetwindow unavailable — cannot raise the browser")
try:
wins = [w for w in pygetwindow.getAllWindows() if w.visible and _is_browser_window(w)]
wins = [w for w in pygetwindow.getAllWindows()
if w.visible and _is_browser_window(w, browser)]
except NotImplementedError:
# pygetwindow has no X11 backend; say so rather than looking like no
# browser is open.
return FocusResult(False, f"window management is unsupported on {sys.platform}")
if not wins:
return FocusResult(False, "no browser window found")
return FocusResult(False, f"no {browser or 'browser'} window found")
try:
win = wins[0]
@@ -211,25 +250,30 @@ def _other_activate() -> FocusResult:
return FocusResult(False, f"could not activate window ({exc})")
def activate_browser() -> FocusResult:
"""Raise the browser application above everything else."""
result = _mac_activate() if sys.platform == "darwin" else _other_activate()
def activate_browser(browser: str | None = None) -> FocusResult:
"""Raise the browser application above everything else.
`browser` is the id the extension reported ("chrome", "edge", ...). Without
it, any known browser will do — fine on a machine with one installed, wrong
on a machine with two.
"""
result = _mac_activate(browser) if sys.platform == "darwin" else _other_activate(browser)
if result.ok:
time.sleep(SETTLE)
return result
def ensure_frontmost(timeout: float = 1.5) -> FocusResult:
def ensure_frontmost(timeout: float = 1.5, browser: str | None = None) -> FocusResult:
"""Raise the browser and, where we can check, confirm it actually came forward.
Returning ok=False does not mean the click will fail — only that we could not
verify. The caller decides whether to proceed.
"""
result = activate_browser()
result = activate_browser(browser)
if not result.ok:
return result
ids = browser_ids()
ids = browser_ids(browser)
if not ids:
return FocusResult(True, result.detail + " (unverified)")