Skip to content

Instantly share code, notes, and snippets.

@stephancasas
Created March 29, 2023 17:23
Show Gist options
  • Save stephancasas/830218f2cb576855ddeddac9a7f9ecd0 to your computer and use it in GitHub Desktop.
Save stephancasas/830218f2cb576855ddeddac9a7f9ecd0 to your computer and use it in GitHub Desktop.
Pocket Casts AppleScript Automation
#!/usr/bin/env osascript -l JavaScript
/**
* ------------------------------------------------------------------------------
* Pocket Casts Automation / Skip Forward
* ------------------------------------------------------------------------------
*
* Skip forward 45 seconds in Pocket Casts for Mac
*
* Note: the window must be open. It may be *hidden*, but it must be open.
*
* Author: Stephan Casas
* Based on FastAX: https://github.com/stephancasas/fast-ax
*
*/
const App = Application.currentApplication();
App.includeStandardAdditions = true;
ObjC.import('CoreGraphics');
ObjC.bindFunction('AXAPIEnabled', ['bool', []]);
ObjC.bindFunction('AXIsProcessTrusted', ['bool', []]);
ObjC.bindFunction('AXUIElementCreateApplication', ['id', ['unsigned int']]);
ObjC.bindFunction('AXUIElementCopyAttributeValue', [
'int',
['id', 'id', 'id *'],
]);
ObjC.bindFunction('AXUIElementCopyAttributeNames', ['int', ['id', 'id *']]);
ObjC.bindFunction('AXUIElementCopyActionNames', ['int', ['id', 'id *']]);
ObjC.bindFunction('AXUIElementPerformAction', ['int', ['id', 'id']]);
Ref.prototype.$ = function () {
return ObjC.deepUnwrap(ObjC.castRefToObject(this));
};
class AXUIElement {
__element;
__cachedChildren;
__cachedFirstChild;
constructor(element) {
this.__element = element;
this.linkAttributes();
this.linkActions();
}
linkAttributes() {
const attributes = this.attributes;
Object.keys(attributes)
.filter((key) => !key.match(/(windows|children|actions)/))
.forEach((key) => Object.assign(this, { [key]: attributes[key] }));
}
linkActions() {
const actions = this.actions;
Object.keys(actions).forEach((key) =>
Object.assign(this, { [key]: actions[key] }),
);
}
traverse(role, depth = 1) {
let child = this;
for (let i = 0; i < depth; i++) {
const newChild = child.firstChildWhereLike('role', role);
if (!newChild) {
break;
}
child = newChild;
}
return child;
}
makeFunctionForLocatorAncestry(axUiElementAncestry, withComments = true) {
const indices = axUiElementAncestry
.map((ancestor, i) => ({
index: ancestor.$children.findIndex(
(child) => child == axUiElementAncestry[i + 1],
),
role: ancestor.role ?? '<AXUnknownRole>',
description: ancestor.description ?? '<AXEmptyDescription>',
roleDescription: ancestor.roleDescription ?? '<AXEmptyRoleDescription>',
}))
.slice(0, -1);
const greatest = {
role:
[{ role: 'ROLE' }, ...indices]
.map(({ role }) => role.length)
.sort((a, b) => a > b)
.slice(-1)[0] ?? 0,
description:
[{ description: 'DESCRIPTION' }, ...indices]
.map(({ description }) => description.length)
.sort((a, b) => a > b)
.slice(-1)[0] ?? 0,
roleDescription:
[{ roleDescription: 'ROLE DESC.' }, ...indices]
.map(({ roleDescription }) => roleDescription.length)
.sort((a, b) => a > b)
.slice(-1)[0] ?? 0,
};
const pad = (word, length) =>
`${word}${
word.length < length
? `${Array(length + 1 - word.length).join(' ')}`
: ''
}`;
const makeComment = ({ i, role, description, roleDescription }) =>
!withComments
? `${i == indices.length - 1 ? ';' : ''}`
: `${i == indices.length - 1 ? '; ' : ' '}/*${
i == 0 ? '****' : i == indices.length - 1 ? '***' : '****'
} ${pad(role, greatest.role)} | ${pad(
roleDescription,
greatest.roleDescription,
)} | ${pad(
description,
greatest.description + greatest.role + greatest.roleDescription >=
44
? greatest.description
: greatest.description +
greatest.role +
greatest.roleDescription -
44,
)} ****/`;
const makeCommentHeader = () =>
`${pad('ROLE', greatest.role)} | ${pad(
'ROLE DESC.',
greatest.roleDescription,
)} | ${pad('DESCRIPTION', greatest.description)}`;
const path = indices
.map(
({ index, role, description, roleDescription }, i) =>
` .$children[${index}]${makeComment({
i,
role,
description,
roleDescription,
})}`,
)
.join('\n');
return `const myElement = (root) => {\n return root /********* ${makeCommentHeader()} ${'***'}*/\n${path}\n};`;
}
locate(using, forInstance = 1, __ancestry = [], __found = 0) {
let match = using(this);
__found = __found + (match ? 1 : 0);
if (match && __found == forInstance) {
return [...__ancestry, this];
}
let children = this.$children ?? [];
for (let i = 0; i < children.length; i++) {
const child = children[i];
const search = child.locate(
using,
forInstance,
[...__ancestry, this],
__found,
);
if (!!search) {
return search;
}
}
return null;
}
locateWhereLike(property, value, instance = 1) {
const regex = new RegExp(value, 'gi');
return this.locate(
(child) => `${child[property] ?? ''}`.match(regex),
instance,
);
}
locateWhereHasActionLike(name, instance = 1) {
return this.locate(
(child) => !!child.firstActionLike(name, null),
instance,
);
}
childrenHavingLike(property, value) {
return this.children
.map((child) => (child.locateWhereLike(property, value) ?? [null]).pop())
.filter((child) => !!child);
}
childrenHavingActionLike(name) {
return this.children
.map((child) => (child.locateWhereHasActionLike(name) ?? [null]).pop())
.filter((child) => !!child);
}
firstChildWhereHasActionLike(name) {
return this.$children.find((child) => !!child.firstActionLike(name, null));
}
firstChildWhereLike(property, value) {
const regex = new RegExp(value, 'gi');
return this.$children.find((child) =>
`${child[property] ?? ''}`.match(regex),
);
}
firstChildWhere(
property,
valueOrOperator,
value = 'com.stephancasas.undefined',
) {
let operator = '==';
if (value != 'com.stephancasas.undefined') {
operator = valueOrOperator;
} else {
value = valueOrOperator;
}
return this.$children.find((child) =>
eval(`(child, value) => (child.${property} ${operator} value)`)(
child,
value,
),
);
}
firstActionLike(name, fallback = () => {}) {
const regex = new RegExp(name, 'gi');
const action = Object.keys(this.actions).find((key) => key.match(regex));
return action ? this[action] : fallback;
}
__valueOf(key) {
const value = Ref();
$.AXUIElementCopyAttributeValue(this.__element, key, value);
return ObjC.unwrap(value[0]);
}
__performAction(action) {
return $.AXUIElementPerformAction(this.__element, action);
}
__getUninitializedChildren() {
const value = Ref();
$.AXUIElementCopyAttributeValue(this.__element, 'AXChildren', value);
return (
ObjC.unwrap($.NSArray.arrayWithArray($.CFBridgingRelease(value[0]))) ?? []
);
}
get children() {
this.__cachedChildren = this.__getUninitializedChildren().map(
(child) => new AXUIElement(child),
);
return this.__cachedChildren;
}
get $children() {
return this.__cachedChildren == undefined
? this.children
: this.__cachedChildren;
}
get firstChild() {
const children = this.__getUninitializedChildren();
this.__cachedFirstChild =
children.length == 0
? null
: new AXUIElement(this.__getUninitializedChildren()[0]);
return this.__cachedFirstChild;
}
get $firstChild() {
return this.__cachedFirstChild == undefined
? this.firstChild
: this.__cachedFirstChild;
}
static __mapActionsToCallable(instance, actionNames) {
return actionNames.reduce(
(acc, cur) =>
Object.assign(acc, {
[`${cur.replace(/^AX/, '').charAt(0).toLowerCase()}${cur
.replace(/^AX/, '')
.slice(1)}`](...args) {
return instance.__performAction(cur, ...args);
},
}),
{},
);
}
get actions() {
const names = Ref();
$.AXUIElementCopyActionNames(this.__element, names);
return AXUIElement.__mapActionsToCallable(this, ObjC.deepUnwrap(names[0]));
}
static __mapAttributesToProperties(instance, attributeNames) {
return attributeNames.reduce(
(acc, cur) =>
Object.assign(
acc,
eval(
`(instance) => {return {get ${cur
.replace(/^AX/, '')
.charAt(0)
.toLowerCase()}${cur
.replace(/^AX/, '')
.slice(1)}(){return instance.__valueOf('${cur}');}}}`,
)(instance),
),
{},
);
}
get attributes() {
const names = Ref();
$.AXUIElementCopyAttributeNames(this.__element, names);
return AXUIElement.__mapAttributesToProperties(
this,
ObjC.deepUnwrap(names[0]),
);
}
}
class AXApplication extends AXUIElement {
__cachedWindows;
__cachedFirstWindow;
constructor(nameOrPid) {
if (
`${nameOrPid}`.replace(/\d/g, '').length == `${nameOrPid}`.length &&
typeof nameOrPid != 'number'
) {
super(AXApplication.__getApplicationByName(nameOrPid));
} else {
super(AXApplication.__getApplicationByPid(parseInt(nameOrPid)));
}
if (!this.__element) {
throw new Error(`Could not find application for ${nameOrPid}`);
}
}
get windows() {
this.__cachedWindows = this.__valueOf('AXWindows').map(
(window) => new AXUIElement(window),
);
return this.__cachedWindows;
}
get $windows() {
return this.__cachedWindows == undefined
? this.windows
: this.__cachedWindows;
}
get firstWindow() {
this.__cachedFirstWindow = this.windows[0] ?? null;
return this.__cachedFirstWindow;
}
get $firstWindow() {
return this.__cachedFirstWindow == undefined
? this.firstWindow
: this.__cachedFirstWindow;
}
static __getApplicationByName(name) {
const pid =
$.CGWindowListCopyWindowInfo(
$.kCGWindowListExcludeDesktopElements,
$.kCGNullWindowID,
)
.$()
.filter(({ kCGWindowOwnerName }) => kCGWindowOwnerName == name)
.map(({ kCGWindowOwnerPID: pid }) => pid)[0] ?? null;
return !pid ? null : this.__getApplicationByPid(pid);
}
static __getApplicationByPid(pid) {
return $.AXUIElementCreateApplication(pid);
}
}
function run(_) {
const PocketCasts = new AXApplication('Pocket Casts').$firstWindow;
const axWebArea =
PocketCasts.$children[0].$children[0].$children[0].$children[0];
try {
axWebArea.$children[22].$children[3].press();
} catch (error) {
axWebArea
.firstChildWhereLike('description', 'controls')
.firstChildWhereLike('description', 'skip forwards')
.press();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment