@vox-ai-app/integrations 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +125 -0
  2. package/package.json +42 -0
  3. package/src/imessage/def.js +41 -0
  4. package/src/imessage/index.js +9 -0
  5. package/src/imessage/mac/data.js +144 -0
  6. package/src/imessage/mac/reply.js +68 -0
  7. package/src/imessage/mac/service.js +141 -0
  8. package/src/imessage/tools.js +44 -0
  9. package/src/index.js +7 -0
  10. package/src/mail/def.js +317 -0
  11. package/src/mail/index.js +165 -0
  12. package/src/mail/manage/index.js +10 -0
  13. package/src/mail/manage/mac/index.js +275 -0
  14. package/src/mail/read/index.js +1 -0
  15. package/src/mail/read/mac/accounts.js +53 -0
  16. package/src/mail/read/mac/index.js +170 -0
  17. package/src/mail/read/mac/permission.js +29 -0
  18. package/src/mail/read/mac/sync.js +98 -0
  19. package/src/mail/read/mac/transform.js +55 -0
  20. package/src/mail/send/index.js +1 -0
  21. package/src/mail/send/mac/index.js +93 -0
  22. package/src/mail/shared/index.js +6 -0
  23. package/src/mail/shared/mac/index.js +48 -0
  24. package/src/mail/tools.js +41 -0
  25. package/src/screen/capture/index.js +1 -0
  26. package/src/screen/capture/mac/index.js +109 -0
  27. package/src/screen/control/index.js +15 -0
  28. package/src/screen/control/mac/accessibility.js +25 -0
  29. package/src/screen/control/mac/apps.js +62 -0
  30. package/src/screen/control/mac/exec.js +66 -0
  31. package/src/screen/control/mac/helpers.js +5 -0
  32. package/src/screen/control/mac/index.js +10 -0
  33. package/src/screen/control/mac/keyboard.js +34 -0
  34. package/src/screen/control/mac/keycodes.js +87 -0
  35. package/src/screen/control/mac/mouse.js +59 -0
  36. package/src/screen/control/mac/python-keyboard.js +66 -0
  37. package/src/screen/control/mac/python-mouse.js +66 -0
  38. package/src/screen/control/mac/python.js +2 -0
  39. package/src/screen/control/mac/ui-scan.js +45 -0
  40. package/src/screen/def.js +304 -0
  41. package/src/screen/index.js +17 -0
  42. package/src/screen/queue.js +54 -0
  43. package/src/screen/tools.js +50 -0
  44. package/src/tools.js +6 -0
@@ -0,0 +1,93 @@
1
+ import { resolveLocalPath } from '@vox-ai-app/tools'
2
+ import {
3
+ execAbortable,
4
+ esc,
5
+ EXEC_TIMEOUT,
6
+ writeTempScript,
7
+ cleanupTemp,
8
+ parseTabSeparated
9
+ } from '@vox-ai-app/tools/exec'
10
+ import { ensureAppleMailConfigured } from '../../shared/index.js'
11
+ export const sendEmailMac = async (
12
+ { to, cc, bcc, subject, body, attachments, account },
13
+ { signal } = {}
14
+ ) => {
15
+ await ensureAppleMailConfigured(signal)
16
+ const bodyEsc = esc(body).replace(/\n/g, '\\n')
17
+ const lines = ['tell application "Mail"']
18
+ if (account) {
19
+ lines.push(
20
+ ` set acct to first account whose name contains "${esc(account)}"`,
21
+ ' set addressesList to email addresses of acct',
22
+ ' set senderAddr to item 1 of addressesList',
23
+ ` set msg to make new outgoing message with properties {subject:"${esc(subject)}", content:"${bodyEsc}", visible:false, sender:senderAddr}`
24
+ )
25
+ } else {
26
+ lines.push(
27
+ ` set msg to make new outgoing message with properties {subject:"${esc(subject)}", content:"${bodyEsc}", visible:false}`
28
+ )
29
+ }
30
+ lines.push(' tell msg')
31
+ for (const addr of to) {
32
+ lines.push(` make new to recipient with properties {address:"${esc(addr)}"}`)
33
+ }
34
+ for (const addr of cc) {
35
+ lines.push(` make new cc recipient with properties {address:"${esc(addr)}"}`)
36
+ }
37
+ for (const addr of bcc) {
38
+ lines.push(` make new bcc recipient with properties {address:"${esc(addr)}"}`)
39
+ }
40
+ for (const p of attachments) {
41
+ const abs = resolveLocalPath(p)
42
+ lines.push(
43
+ ` make new attachment with properties {file name:(POSIX file "${esc(abs)}")} at after the last paragraph`
44
+ )
45
+ }
46
+ lines.push(' end tell', ' send msg', 'end tell')
47
+ const scriptFile = await writeTempScript(lines.join('\n'), 'scpt')
48
+ try {
49
+ await execAbortable(
50
+ `osascript "${scriptFile}"`,
51
+ {
52
+ timeout: EXEC_TIMEOUT
53
+ },
54
+ signal
55
+ )
56
+ return {
57
+ status: 'sent',
58
+ to,
59
+ subject
60
+ }
61
+ } finally {
62
+ await cleanupTemp(scriptFile)
63
+ }
64
+ }
65
+ export const searchContactsMac = async (query, { signal } = {}) => {
66
+ const script = [
67
+ `set Q to "${esc(query)}"`,
68
+ 'set output to ""',
69
+ 'tell application "Contacts"',
70
+ ' set matched to every person whose name contains Q',
71
+ ' repeat with p in matched',
72
+ ' set pName to name of p',
73
+ ' repeat with e in emails of p',
74
+ ' set output to output & pName & "\\t" & (value of e) & "\\n"',
75
+ ' end repeat',
76
+ ' end repeat',
77
+ 'end tell',
78
+ 'return output'
79
+ ].join('\n')
80
+ const scriptFile = await writeTempScript(script, 'scpt')
81
+ try {
82
+ const { stdout } = await execAbortable(
83
+ `osascript "${scriptFile}"`,
84
+ {
85
+ timeout: EXEC_TIMEOUT
86
+ },
87
+ signal
88
+ )
89
+ return parseTabSeparated(stdout)
90
+ } finally {
91
+ await cleanupTemp(scriptFile)
92
+ }
93
+ }
@@ -0,0 +1,6 @@
1
+ export {
2
+ openMailAutomationSettings,
3
+ getAppleMailAccounts,
4
+ ensureAppleMailConfigured,
5
+ APPLE_MAIL_SETUP_MESSAGE
6
+ } from './mac/index.js'
@@ -0,0 +1,48 @@
1
+ import { shell } from 'electron'
2
+ import { execAbortable } from '@vox-ai-app/tools/exec'
3
+ const AUTOMATION_PERMISSION_MESSAGE =
4
+ 'Vox needs permission to control Mail. Please grant it in System Settings → Privacy & Security → Automation → Vox → Mail.'
5
+ const APPLE_MAIL_SETUP_MESSAGE =
6
+ 'macOS mail actions in Vox require Apple Mail with at least one configured account. If you use Gmail or Outlook elsewhere, add the account in Apple Mail first. If Mail is already set up, check macOS Automation permissions for Vox and try again.'
7
+ const isAutomationDeniedError = (err) => {
8
+ const msg = String(err?.message || err?.stderr || '').toLowerCase()
9
+ return (
10
+ msg.includes('not allowed to send apple events') ||
11
+ msg.includes('apple event handler failed') ||
12
+ msg.includes('-1743') ||
13
+ msg.includes('access not allowed')
14
+ )
15
+ }
16
+ export const openMailAutomationSettings = () => {
17
+ shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_Automation')
18
+ }
19
+ export const getAppleMailAccounts = async (signal) => {
20
+ try {
21
+ const { stdout } = await execAbortable(
22
+ 'osascript -e \'tell application "Mail" to return name of every account\'',
23
+ {
24
+ timeout: 15_000
25
+ },
26
+ signal
27
+ )
28
+ return String(stdout)
29
+ .trim()
30
+ .split(',')
31
+ .map((name) => name.trim())
32
+ .filter(Boolean)
33
+ } catch (err) {
34
+ if (isAutomationDeniedError(err)) {
35
+ openMailAutomationSettings()
36
+ throw Object.assign(new Error(AUTOMATION_PERMISSION_MESSAGE), {
37
+ code: 'MAIL_AUTOMATION_REQUIRED'
38
+ })
39
+ }
40
+ throw new Error(APPLE_MAIL_SETUP_MESSAGE)
41
+ }
42
+ }
43
+ export const ensureAppleMailConfigured = async (signal) => {
44
+ const accounts = await getAppleMailAccounts(signal)
45
+ if (!accounts.length) throw new Error(APPLE_MAIL_SETUP_MESSAGE)
46
+ return accounts
47
+ }
48
+ export { APPLE_MAIL_SETUP_MESSAGE }
@@ -0,0 +1,41 @@
1
+ import { MAIL_TOOL_DEFINITIONS } from './def.js'
2
+ import {
3
+ sendEmail,
4
+ readEmails,
5
+ searchContacts,
6
+ getEmailBody,
7
+ replyToEmail,
8
+ forwardEmail,
9
+ markEmailRead,
10
+ flagEmail,
11
+ deleteEmail,
12
+ moveEmail,
13
+ createDraft,
14
+ saveAttachment
15
+ } from './index.js'
16
+
17
+ const DARWIN_ONLY = () => {
18
+ throw new Error('Mail tools are only available on macOS.')
19
+ }
20
+
21
+ const isDarwin = process.platform === 'darwin'
22
+
23
+ const executors = {
24
+ read_emails: (_ctx) => (isDarwin ? readEmails : DARWIN_ONLY),
25
+ search_contacts: (_ctx) => (isDarwin ? searchContacts : DARWIN_ONLY),
26
+ send_email: (_ctx) => (isDarwin ? sendEmail : DARWIN_ONLY),
27
+ get_email_body: (_ctx) => (isDarwin ? getEmailBody : DARWIN_ONLY),
28
+ reply_to_email: (_ctx) => (isDarwin ? replyToEmail : DARWIN_ONLY),
29
+ forward_email: (_ctx) => (isDarwin ? forwardEmail : DARWIN_ONLY),
30
+ mark_email_read: (_ctx) => (isDarwin ? markEmailRead : DARWIN_ONLY),
31
+ flag_email: (_ctx) => (isDarwin ? flagEmail : DARWIN_ONLY),
32
+ delete_email: (_ctx) => (isDarwin ? deleteEmail : DARWIN_ONLY),
33
+ move_email: (_ctx) => (isDarwin ? moveEmail : DARWIN_ONLY),
34
+ create_draft: (_ctx) => (isDarwin ? createDraft : DARWIN_ONLY),
35
+ save_attachment: (_ctx) => (isDarwin ? saveAttachment : DARWIN_ONLY)
36
+ }
37
+
38
+ export const MAIL_TOOLS = MAIL_TOOL_DEFINITIONS.map((def) => ({
39
+ definition: def,
40
+ execute: executors[def.name]
41
+ }))
@@ -0,0 +1 @@
1
+ export { captureFullScreen, captureRegion, waitForScreenPermission } from './mac/index.js'
@@ -0,0 +1,109 @@
1
+ import { desktopCapturer, screen, shell, systemPreferences } from 'electron'
2
+ import { exec } from 'child_process'
3
+ import fs from 'fs/promises'
4
+ import os from 'os'
5
+ import path from 'path'
6
+ const execAbortable = (command, options = {}, signal) => {
7
+ return new Promise((resolve, reject) => {
8
+ if (signal?.aborted) {
9
+ reject(new Error('Aborted'))
10
+ return
11
+ }
12
+ const child = exec(command, options, (error, stdout, stderr) => {
13
+ if (signal) signal.removeEventListener('abort', onAbort)
14
+ if (error)
15
+ reject(
16
+ Object.assign(error, {
17
+ stderr
18
+ })
19
+ )
20
+ else
21
+ resolve({
22
+ stdout,
23
+ stderr
24
+ })
25
+ })
26
+ const onAbort = () => {
27
+ try {
28
+ child.kill('SIGTERM')
29
+ } catch {
30
+ void 0
31
+ }
32
+ }
33
+ if (signal)
34
+ signal.addEventListener('abort', onAbort, {
35
+ once: true
36
+ })
37
+ })
38
+ }
39
+ export const waitForScreenPermission = async (signal) => {
40
+ const initial = systemPreferences.getMediaAccessStatus('screen')
41
+ if (initial === 'granted') return
42
+ await desktopCapturer
43
+ .getSources({
44
+ types: ['screen'],
45
+ thumbnailSize: {
46
+ width: 1,
47
+ height: 1
48
+ }
49
+ })
50
+ .catch(() => {})
51
+ await shell.openExternal(
52
+ 'x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture'
53
+ )
54
+ const deadline = Date.now() + 60_000
55
+ while (Date.now() < deadline) {
56
+ if (signal?.aborted) throw new Error('Aborted')
57
+ await new Promise((r) => setTimeout(r, 500))
58
+ if (systemPreferences.getMediaAccessStatus('screen') === 'granted') return
59
+ }
60
+ throw new Error(
61
+ 'Screen recording permission was not granted. Please allow access in System Settings → Privacy & Security → Screen Recording.'
62
+ )
63
+ }
64
+ export const captureFullScreen = async (_, { signal } = {}) => {
65
+ await waitForScreenPermission(signal)
66
+ const primaryDisplay = screen.getPrimaryDisplay()
67
+ const { width, height } = primaryDisplay.size
68
+ const sources = await desktopCapturer.getSources({
69
+ types: ['screen'],
70
+ thumbnailSize: {
71
+ width,
72
+ height
73
+ }
74
+ })
75
+ const primary = sources[0]
76
+ if (!primary) throw new Error('No screen source available for capture.')
77
+ const captureSize = primary.thumbnail.getSize()
78
+ const base64Image = primary.thumbnail.toJPEG(75).toString('base64')
79
+ return {
80
+ text: `Captured full screen (${captureSize.width}x${captureSize.height}). Coordinates in this image map directly to click_at screen coordinates.`,
81
+ imageBase64: base64Image,
82
+ mimeType: 'image/jpeg'
83
+ }
84
+ }
85
+ export const captureRegion = async ({ x, y, width, height }, { signal } = {}) => {
86
+ await waitForScreenPermission(signal)
87
+ const xi = Math.round(Number(x))
88
+ const yi = Math.round(Number(y))
89
+ const wi = Math.round(Number(width))
90
+ const hi = Math.round(Number(height))
91
+ const tmpFile = path.join(os.tmpdir(), `vox_region_${Date.now()}.jpg`)
92
+ try {
93
+ await execAbortable(
94
+ `screencapture -R ${xi},${yi},${wi},${hi} -t jpg "${tmpFile}"`,
95
+ {
96
+ timeout: 10_000
97
+ },
98
+ signal
99
+ )
100
+ const buffer = await fs.readFile(tmpFile)
101
+ return {
102
+ text: `Captured region (${wi}x${hi}) at (${xi},${yi}). Coordinates map directly to click_at screen coordinates.`,
103
+ imageBase64: buffer.toString('base64'),
104
+ mimeType: 'image/jpeg'
105
+ }
106
+ } finally {
107
+ await fs.unlink(tmpFile).catch(() => {})
108
+ }
109
+ }
@@ -0,0 +1,15 @@
1
+ export {
2
+ clickAt,
3
+ moveMouse,
4
+ typeText,
5
+ keyPress,
6
+ scroll,
7
+ drag,
8
+ getMousePosition,
9
+ getUiElements,
10
+ clipboardRead,
11
+ clipboardWrite,
12
+ focusApp,
13
+ launchApp,
14
+ listApps
15
+ } from './mac/index.js'
@@ -0,0 +1,25 @@
1
+ import { systemPreferences, shell } from 'electron'
2
+
3
+ const openAccessibilitySettings = () => {
4
+ shell.openExternal(
5
+ 'x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility'
6
+ )
7
+ }
8
+
9
+ export const ensureAccessibility = () => {
10
+ const trusted = systemPreferences.isTrustedAccessibilityClient(false)
11
+ if (!trusted) {
12
+ const prompted = systemPreferences.isTrustedAccessibilityClient(true)
13
+ if (!prompted) {
14
+ openAccessibilitySettings()
15
+ throw Object.assign(
16
+ new Error(
17
+ 'Vox needs Accessibility permission to control the screen. Opening System Settings → Privacy & Security → Accessibility — please enable it for Vox and try again.'
18
+ ),
19
+ {
20
+ code: 'ACCESSIBILITY_REQUIRED'
21
+ }
22
+ )
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,62 @@
1
+ import { enqueueScreen } from '../../queue.js'
2
+ import {
3
+ ensureAccessibility,
4
+ UI_ELEMENTS_SCRIPT,
5
+ LONG_TIMEOUT,
6
+ execAbortable,
7
+ writeTmp,
8
+ cleanTmp
9
+ } from './helpers.js'
10
+
11
+ export const getUiElements = ({ app, maxElements } = {}, { signal } = {}) =>
12
+ enqueueScreen(async () => {
13
+ ensureAccessibility()
14
+ const limit = Math.max(1, Math.min(1000, Number(maxElements) || 200))
15
+ let script = UI_ELEMENTS_SCRIPT
16
+ if (app) {
17
+ script = UI_ELEMENTS_SCRIPT.replace(
18
+ 'var proc = se.processes.whose({ frontmost: true })[0];',
19
+ `var proc = se.processes.whose({ name: "${String(app).replace(/"/g, '\\"')}" })[0];`
20
+ )
21
+ }
22
+ const file = await writeTmp(script, 'js')
23
+ try {
24
+ const { stdout } = await execAbortable(
25
+ `osascript -l JavaScript "${file}"`,
26
+ { timeout: LONG_TIMEOUT },
27
+ signal
28
+ )
29
+ const all = JSON.parse(stdout.trim())
30
+ const elements = Array.isArray(all) ? all : (all?.elements ?? [])
31
+ const total = elements.length
32
+ return { elements: elements.slice(0, limit), total, truncated: total > limit }
33
+ } catch (err) {
34
+ throw new Error(`UI element inspection failed: ${err?.message || err}`)
35
+ } finally {
36
+ await cleanTmp(file)
37
+ }
38
+ })
39
+
40
+ export const focusApp = async ({ app }, { signal } = {}) => {
41
+ await execAbortable(`open -a ${JSON.stringify(app)}`, { timeout: 10_000 }, signal)
42
+ return { action: 'focus_app', app }
43
+ }
44
+
45
+ export const launchApp = async ({ app, args = [] }, { signal } = {}) => {
46
+ const argStr =
47
+ Array.isArray(args) && args.length
48
+ ? ` --args ${args.map((a) => JSON.stringify(a)).join(' ')}`
49
+ : ''
50
+ await execAbortable(`open -a ${JSON.stringify(app)}${argStr}`, { timeout: 15_000 }, signal)
51
+ return { action: 'launch_app', app }
52
+ }
53
+
54
+ export const listApps = async (_, { signal } = {}) => {
55
+ const { stdout } = await execAbortable('ls /Applications/', { timeout: 10_000 }, signal)
56
+ const apps = stdout
57
+ .trim()
58
+ .split('\n')
59
+ .filter((a) => a.endsWith('.app'))
60
+ .map((a) => a.replace(/\.app$/, ''))
61
+ return { apps }
62
+ }
@@ -0,0 +1,66 @@
1
+ import { exec } from 'child_process'
2
+ import fs from 'fs/promises'
3
+ import os from 'os'
4
+ import path from 'path'
5
+
6
+ const EXEC_TIMEOUT = 30_000
7
+
8
+ export const execAbortable = (command, options = {}, signal) => {
9
+ return new Promise((resolve, reject) => {
10
+ if (signal?.aborted) {
11
+ reject(new Error('Aborted'))
12
+ return
13
+ }
14
+ const child = exec(command, options, (error, stdout, stderr) => {
15
+ if (signal) signal.removeEventListener('abort', onAbort)
16
+ if (error)
17
+ reject(
18
+ Object.assign(error, {
19
+ stderr
20
+ })
21
+ )
22
+ else
23
+ resolve({
24
+ stdout,
25
+ stderr
26
+ })
27
+ })
28
+ const onAbort = () => {
29
+ try {
30
+ child.kill('SIGTERM')
31
+ } catch {
32
+ void 0
33
+ }
34
+ }
35
+ if (signal)
36
+ signal.addEventListener('abort', onAbort, {
37
+ once: true
38
+ })
39
+ })
40
+ }
41
+
42
+ export const writeTmp = async (content, ext) => {
43
+ const file = path.join(
44
+ os.tmpdir(),
45
+ `vox_ctrl_${Date.now()}_${Math.random().toString(36).slice(2)}.${ext}`
46
+ )
47
+ await fs.writeFile(file, content, 'utf8')
48
+ return file
49
+ }
50
+
51
+ export const cleanTmp = (file) => fs.unlink(file).catch(() => {})
52
+
53
+ export const runPy = async (script, signal, timeout = EXEC_TIMEOUT) => {
54
+ const file = await writeTmp(script, 'py')
55
+ try {
56
+ return await execAbortable(
57
+ `python3 "${file}"`,
58
+ {
59
+ timeout
60
+ },
61
+ signal
62
+ )
63
+ } finally {
64
+ await cleanTmp(file)
65
+ }
66
+ }
@@ -0,0 +1,5 @@
1
+ export * from './exec.js'
2
+ export * from './accessibility.js'
3
+ export * from './python.js'
4
+ export * from './keycodes.js'
5
+ export * from './ui-scan.js'
@@ -0,0 +1,10 @@
1
+ import { clipboard } from 'electron'
2
+ export * from './mouse.js'
3
+ export * from './keyboard.js'
4
+ export * from './apps.js'
5
+
6
+ export const clipboardRead = () => ({ text: clipboard.readText() })
7
+ export const clipboardWrite = ({ text }) => {
8
+ clipboard.writeText(String(text || ''))
9
+ return { ok: true }
10
+ }
@@ -0,0 +1,34 @@
1
+ import { enqueueScreen } from '../../queue.js'
2
+ import {
3
+ ensureAccessibility,
4
+ CHAR_CODES,
5
+ KEY_CODES,
6
+ pyTypeText,
7
+ pyKeyCode,
8
+ pyCharKey,
9
+ runPy
10
+ } from './helpers.js'
11
+
12
+ export const typeText = ({ text }, { signal } = {}) =>
13
+ enqueueScreen(async () => {
14
+ ensureAccessibility()
15
+ if (!text) throw new Error('"text" is required.')
16
+ await runPy(pyTypeText(text), signal)
17
+ return { action: 'type', length: text.length }
18
+ })
19
+
20
+ export const keyPress = ({ key, modifiers = [] }, { signal } = {}) =>
21
+ enqueueScreen(async () => {
22
+ ensureAccessibility()
23
+ if (!key) throw new Error('"key" is required.')
24
+ const keyLower = String(key).toLowerCase().trim()
25
+ const mods = (Array.isArray(modifiers) ? modifiers : [modifiers]).filter(Boolean)
26
+ const keyCode = KEY_CODES[keyLower] ?? CHAR_CODES[keyLower]
27
+ if (keyCode !== undefined) {
28
+ await runPy(pyKeyCode(keyCode, mods), signal)
29
+ } else {
30
+ const b64 = Buffer.from(keyLower, 'utf8').toString('base64')
31
+ await runPy(pyCharKey(b64, mods), signal)
32
+ }
33
+ return { action: 'key_press', key, modifiers: mods }
34
+ })
@@ -0,0 +1,87 @@
1
+ export const KEY_CODES = {
2
+ return: 36,
3
+ enter: 36,
4
+ tab: 48,
5
+ space: 49,
6
+ delete: 51,
7
+ backspace: 51,
8
+ escape: 53,
9
+ esc: 53,
10
+ command: 55,
11
+ cmd: 55,
12
+ shift: 56,
13
+ option: 58,
14
+ alt: 58,
15
+ control: 59,
16
+ ctrl: 59,
17
+ left: 123,
18
+ right: 124,
19
+ down: 125,
20
+ up: 126,
21
+ f1: 122,
22
+ f2: 120,
23
+ f3: 99,
24
+ f4: 118,
25
+ f5: 96,
26
+ f6: 97,
27
+ f7: 98,
28
+ f8: 100,
29
+ f9: 101,
30
+ f10: 109,
31
+ f11: 103,
32
+ f12: 111,
33
+ home: 115,
34
+ end: 119,
35
+ pageup: 116,
36
+ pagedown: 121
37
+ }
38
+
39
+ export const CHAR_CODES = {
40
+ a: 0,
41
+ s: 1,
42
+ d: 2,
43
+ f: 3,
44
+ h: 4,
45
+ g: 5,
46
+ z: 6,
47
+ x: 7,
48
+ c: 8,
49
+ v: 9,
50
+ b: 11,
51
+ q: 12,
52
+ w: 13,
53
+ e: 14,
54
+ r: 15,
55
+ y: 16,
56
+ t: 17,
57
+ 1: 18,
58
+ 2: 19,
59
+ 3: 20,
60
+ 4: 21,
61
+ 6: 22,
62
+ 5: 23,
63
+ 9: 25,
64
+ 7: 26,
65
+ 8: 28,
66
+ 0: 29,
67
+ o: 31,
68
+ u: 32,
69
+ i: 34,
70
+ p: 35,
71
+ l: 37,
72
+ j: 38,
73
+ k: 40,
74
+ n: 45,
75
+ m: 46,
76
+ ',': 43,
77
+ '.': 47,
78
+ '/': 44,
79
+ ';': 41,
80
+ "'": 39,
81
+ '[': 33,
82
+ ']': 30,
83
+ '\\': 42,
84
+ '-': 27,
85
+ '=': 24,
86
+ '`': 50
87
+ }
@@ -0,0 +1,59 @@
1
+ import { enqueueScreen } from '../../queue.js'
2
+ import {
3
+ ensureAccessibility,
4
+ pyClick,
5
+ pyMove,
6
+ pyDrag,
7
+ pyScroll,
8
+ pyGetMousePos,
9
+ runPy
10
+ } from './helpers.js'
11
+
12
+ export const clickAt = ({ x, y, button = 'left', count = 1 }, { signal } = {}) =>
13
+ enqueueScreen(async () => {
14
+ ensureAccessibility()
15
+ const xInt = Math.round(Number(x))
16
+ const yInt = Math.round(Number(y))
17
+ const btn = button === 'right' ? 'right' : 'left'
18
+ const clicks = Math.max(1, Math.min(3, Number(count)))
19
+ await runPy(pyClick(xInt, yInt, btn, clicks), signal)
20
+ return { action: 'click', x: xInt, y: yInt, button: btn, count: clicks }
21
+ })
22
+
23
+ export const moveMouse = ({ x, y }, { signal } = {}) =>
24
+ enqueueScreen(async () => {
25
+ ensureAccessibility()
26
+ const xInt = Math.round(Number(x))
27
+ const yInt = Math.round(Number(y))
28
+ await runPy(pyMove(xInt, yInt), signal)
29
+ return { action: 'move', x: xInt, y: yInt }
30
+ })
31
+
32
+ export const scroll = ({ x, y, deltaX = 0, deltaY = -3 }, { signal } = {}) =>
33
+ enqueueScreen(async () => {
34
+ ensureAccessibility()
35
+ const xInt = Math.round(Number(x))
36
+ const yInt = Math.round(Number(y))
37
+ const dx = Math.round(Number(deltaX))
38
+ const dy = Math.round(Number(deltaY))
39
+ await runPy(pyScroll(xInt, yInt, dx, dy), signal)
40
+ return { action: 'scroll', x: xInt, y: yInt, deltaX: dx, deltaY: dy }
41
+ })
42
+
43
+ export const drag = ({ fromX, fromY, toX, toY }, { signal } = {}) =>
44
+ enqueueScreen(async () => {
45
+ ensureAccessibility()
46
+ const x1 = Math.round(Number(fromX))
47
+ const y1 = Math.round(Number(fromY))
48
+ const x2 = Math.round(Number(toX))
49
+ const y2 = Math.round(Number(toY))
50
+ await runPy(pyDrag(x1, y1, x2, y2), signal)
51
+ return { action: 'drag', from: { x: x1, y: y1 }, to: { x: x2, y: y2 } }
52
+ })
53
+
54
+ export const getMousePosition = (_, { signal } = {}) =>
55
+ enqueueScreen(async () => {
56
+ const { stdout } = await runPy(pyGetMousePos(), signal)
57
+ const [x, y] = stdout.trim().split(',').map(Number)
58
+ return { x: x ?? 0, y: y ?? 0 }
59
+ })