Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 75 additions & 10 deletions aw_watcher_window/printAppStatus.jxa
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,68 @@ var url = undefined, incognito = undefined, title = undefined;
// it's not possible to get the URL from firefox
// https://stackoverflow.com/questions/17846948/does-firefox-offer-applescript-support-to-get-url-of-windows

// Handle WhatsApp first (before switch statement) due to potential hidden characters in app name
if(appName.indexOf("WhatsApp") !== -1) {
// Get the active chat name from WhatsApp's navigation bar header
try {
mainWindow = oProcess.
windows().
find(w => w.attributes.byName("AXMain").value() === true);

if (mainWindow) {
var chatName = undefined;

// The chat name appears as an AXHeading with identifier "NavigationBar_HeaderViewButton"
// In WhatsApp/Electron apps, the text is in AXDescription, not AXLabel
var allElements = mainWindow.entireContents();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 entireContents() is called on every poll — severe performance cost

mainWindow.entireContents() synchronously fetches the entire macOS Accessibility UI element tree for the WhatsApp window. For a complex Electron app like WhatsApp, this can return thousands of elements and is known to take several hundred milliseconds to multiple seconds per call.

Since printAppStatus.jxa is re-executed on every watcher poll (default: 1–2 seconds), this means the watcher will stall on every tick while WhatsApp is the frontmost app. In practice this will result in significant CPU usage and degraded watcher accuracy.

A much cheaper approach is to avoid a full tree traversal and instead use a targeted AX query:

// Cheaper: search only AXHeading elements directly
// rather than fetching every descendant via entireContents()
var headings = mainWindow.uiElements.whose({ role: "AXHeading" })();
for (var i = 0; i < headings.length; i++) {
  try {
    var identifier = headings[i].attributes.byName("AXIdentifier").value();
    if (identifier === "NavigationBar_HeaderViewButton") {
      chatName = headings[i].attributes.byName("AXDescription").value();
      break;
    }
  } catch (e) { /* continue */ }
}

Even if whose doesn't descend recursively, it avoids materializing thousands of elements in one shot and is meaningfully faster for common-case searches.

for (var i = 0; i < allElements.length; i++) {
try {
var elem = allElements[i];
var role = elem.attributes.byName("AXRole").value();

// Look for AXHeading elements (the chat name in the header)
if (role === "AXHeading") {
try {
// Check if it has the NavigationBar identifier (WhatsApp-specific)
var identifier = elem.attributes.byName("AXIdentifier").value();
if (identifier && identifier === "NavigationBar_HeaderViewButton") {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Redundant truthy check before strict equality

identifier && identifier === "NavigationBar_HeaderViewButton" — the leading identifier && guard is unnecessary because if identifier is null, undefined, or "", the strict === comparison already evaluates to false. The guard adds no safety but slightly obscures intent.

Suggested change
if (identifier && identifier === "NavigationBar_HeaderViewButton") {
if (identifier === "NavigationBar_HeaderViewButton") {

// Use AXDescription, not AXLabel (Electron app quirk)
chatName = elem.attributes.byName("AXDescription").value();
break;
}
} catch (e) {
// Continue searching
}
}
} catch (e) {
// Continue searching
}
}

// Set title to just the chat name
if (chatName && chatName.length > 0) {
title = chatName;
} else {
// Fallback to default window title if we couldn't get the chat name
title = mainWindow.attributes.byName("AXTitle").value();
}
}
} catch (e) {
// If anything fails, fall back to default title behavior
try {
mainWindow = oProcess.
windows().
find(w => w.attributes.byName("AXMain").value() === true);
if (mainWindow) {
title = mainWindow.attributes.byName("AXTitle").value();
}
} catch (e2) {
// Final fallback
title = undefined;
}
}
Comment on lines +70 to +82
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Catch block duplicates the windows().find() call unnecessarily

The outer catch (e) block repeats the same windows().find() call as the try block. If windows().find() itself threw, mainWindow is unset and the retry makes sense — but if mainWindow was found successfully and a later step threw (e.g., during entireContents() or the loop), the retry needlessly calls windows().find() a second time.

Consider a two-phase approach: get mainWindow unconditionally first, then attempt the chat-name extraction in a separate inner try/catch, falling back to AXTitle on failure. This removes the duplication and makes the control flow easier to follow.

}

switch(appName) {
case "Safari":
// incognito is not available via safari applescript
Expand All @@ -42,17 +104,20 @@ switch(appName) {
title = Application(appName).windows[0].name();
break;
default:
mainWindow = oProcess.
windows().
find(w => w.attributes.byName("AXMain").value() === true)
// Only set title if it hasn't been set already (e.g., by WhatsApp handler above)
if (title === undefined) {
mainWindow = oProcess.
windows().
find(w => w.attributes.byName("AXMain").value() === true)

// in some cases, the primary window of an application may not be found
// this occurs rarely and seems to be triggered by switching to a different application
if(mainWindow) {
title = mainWindow.
attributes.
byName("AXTitle").
value()
// in some cases, the primary window of an application may not be found
// this occurs rarely and seems to be triggered by switching to a different application
if(mainWindow) {
title = mainWindow.
attributes.
byName("AXTitle").
value()
}
}
}

Expand Down