├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── cli.js ├── index.js ├── package.json ├── src ├── alert.js ├── app.js ├── calendar-event.js ├── calendar.js ├── callback-url.js ├── color.js ├── contact.js ├── contacts-container.js ├── contacts-group.js ├── data.js ├── date-formatter.js ├── date-picker.js ├── device.js ├── dictation.js ├── document-picker.js ├── draw-context.js ├── file-manager.js ├── font.js ├── image.js ├── import-module.js ├── keychain.js ├── linear-gradient.js ├── list-widget.js ├── location.js ├── mail.js ├── message.js ├── notification.js ├── pasteboard.js ├── path.js ├── photos.js ├── point.js ├── quick-look.js ├── rect.js ├── recurrence-rule.js ├── relative-date-time-formatter.js ├── reminder.js ├── request.js ├── safari.js ├── script.js ├── sf-symbol.js ├── share-sheet.js ├── size.js ├── speech.js ├── text-field.js ├── timer.js ├── ui-table-cell.js ├── ui-table-row.js ├── ui-table.js ├── url-scheme.js ├── uuid.js ├── web-view.js └── xml-parser.js └── util ├── async-syncifier.js ├── create-log-message.js ├── run-jxa.js ├── type-error.js └── xcall.app └── Contents ├── Info.plist ├── MacOS └── xcall ├── PkgInfo ├── Resources └── Base.lproj │ └── Main.storyboardc │ ├── Info.plist │ └── MainMenu.nib └── _CodeSignature └── CodeResources /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | lib/.DS_Store 4 | package-lock.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 FifiTheBulldog 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scriptable-node 2 | 3 | A Node.JS module to bring the APIs from Simon B. Støvring's iOS automation app, [Scriptable](https://scriptable.app), to other platforms. 4 | 5 | This project is in the early stages of development, and a lot of work needs to be done before it can be distributed on npm. 6 | 7 | ## Platforms to target 8 | 9 | This is aimed at: 10 | 11 | - Windows 12 | - Linux distributions 13 | - Versions of macOS below Big Sur. (For Big Sur, use the Catalyst-based [Mac beta](https://scriptable.app/mac-beta/) of Scriptable, made by Simon, or the iOS version, if your Mac has an M1 chip.) 14 | - Possibly Android, through Termux 15 | 16 | ## Goals 17 | 18 | This is a Node.JS module. After the module is completed, or at least at a good point in development, this may be used to make a full-fledged app, most likely using Electron. 19 | 20 | ## Installation 21 | 22 | Since this isn't on npm yet, you can only install this by downloading the repo directly, either with Git or from GitHub. Open up a terminal and navigate to the project folder (`scriptable-node`), then run `npm install` to install the dependencies. 23 | 24 | So far, these are the dependencies for this project: 25 | 26 | - [Brightness](https://www.npmjs.com/package/brightness) `brightness` 27 | - [Canvas](https://www.npmjs.com/package/canvas) `canvas` 28 | - [Clipboardy](https://www.npmjs.com/package/clipboardy) `clipboardy` 29 | - [Computer-Name](https://www.npmjs.com/package/computer-name)`computer-name` 30 | - [File-Uti](https://www.npmjs.com/package/file-uti) `file-uti` 31 | - [Hex-rgb](https://www.npmjs.com/package/hex-rgb) `hex-rgb` 32 | - [Image-Size](https://www.npmjs.com/package/image-size) `image-size` 33 | - [Loudness](https://www.npmjs.com/package/loudness) `loudness` 34 | - [Luxon](https://www.npmjs.com/package/luxon) `luxon` 35 | - [Mac-open-file-dialog](https://www.npmjs.com/package/macos-open-file-dialog) `macos-open-file-dialog` 36 | - [Node-Fetch](https://www.npmjs.com/package/node-fetch) `node-fetch` 37 | - [Open](https://www.npmjs.com/package/open) `open` 38 | - [OS-Locale](https://www.npmjs.com/package/os-locale) `os-locale` 39 | - [Sax](https://www.npmjs.com/package/sax) `sax` 40 | - [Say](https://www.npmjs.com/package/say) `say` 41 | - [systemInformation](https://www.npmjs.com/package/systeminformation) `systeminformation` 42 | - [Uti](https://www.npmjs.com/package/uti) `uti` 43 | - [UUID](https://www.npmjs.com/package/uuid) `uuid` 44 | 45 | These modules have a lot of their own dependencies that they will also install. 46 | 47 | ## Usage 48 | 49 | Import this module at the top of the script you want to test, retrieving the APIs you need like this: 50 | 51 | ```js 52 | const { Alert, Notification } = require('scriptable-node'); 53 | ``` 54 | 55 | Each Scriptable class or function's name in the module is identical to its name in the Scriptable app and documentation. 56 | 57 | ## A note on deprecated APIs 58 | 59 | Deprecated APIs in Scriptable will most likely not be supported by this module. That may change if they're easy enough. 60 | 61 | ## Design 62 | 63 | This module is designed to mimic the behavior of the real Scriptable app as closely as possible. Obviously, some things will work slightly differently, since this is running in a Node.JS environment in a terminal and not bridging to Swift APIs via JavaScriptCore. Also, when interacting with the file system, all file paths should use the POSIX format, even on Windows. 64 | 65 | ## System integration 66 | 67 | This is probably the toughest part. Integration will vary from system to system. 68 | 69 | - macOS: use AppleScript and/or other macOS-specific terminal commands 70 | - Windows: PowerShell seems to be a good option. 71 | - Linux: there are many different options for interacting with the OS, so decisions about which ones to use will be made later. 72 | - Android: whatever system functions Termux offers? 73 | 74 | If possible, the system default apps for mail, messaging, calendar, etc. should be used. 75 | 76 | ## Contributing 77 | 78 | This project can use all the help it can get. Pull requests to implement features are welcomed. 79 | 80 | Right now, there are a couple of things that urgently need to be finished: 81 | 82 | - `Data` - the foundation of a lot. This will be a wrapper for `Buffer`, at least for now. When this becomes an Electron app, I will either have it use `ArrayBuffer` everywhere or use `Buffer` or `Blob` depending on the environment. 83 | - [All other items on this list have been completed] 84 | 85 | After that, the process of developing system-specific bridges begins. 86 | 87 | ## API checklist 88 | 89 | APIs with a check mark next to their names are "finished." This does not mean that they are ready to use, as they may rely on unfinished APIs. Rather, it just means that no more code should be needed to get them working, and any further work on them is just debugging (or, at some point in the future, possibly adding new features). 90 | 91 | - [x] App 92 | - [ ] Alert 93 | - [ ] args 94 | - [ ] Calendar 95 | - [ ] CalendarEvent 96 | - [ ] CallbackURL 97 | - [x] Color 98 | - [ ] config 99 | - [x] console 100 | - [ ] Contact 101 | - [ ] ContactsContainer 102 | - [ ] ContactsGroup 103 | - [ ] Data 104 | - [x] DateFormatter 105 | - [ ] DatePicker 106 | - [ ] Device 107 | - [ ] Dictation 108 | - [ ] DocumentPicker 109 | - [ ] DrawContext 110 | - [ ] FileManager 111 | - [ ] Font 112 | - [x] Image 113 | - [x] importModule 114 | - [ ] Keychain 115 | - [x] LinearGradient 116 | - [ ] ListWidget 117 | - [ ] Location 118 | - [ ] Mail 119 | - [ ] Message 120 | - [x] module 121 | - [ ] Notification 122 | - [ ] Pasteboard 123 | - [ ] Path 124 | - [ ] Photos 125 | - [x] Point 126 | - [ ] QuickLook 127 | - [x] Rect 128 | - [ ] RecurrenceRule 129 | - [x] RelativeDateTimeFormatter 130 | - [ ] Reminder 131 | - [ ] Request 132 | - [x] Safari 133 | - [x] Script 134 | - [ ] SFSymbol 135 | - [ ] ShareSheet 136 | - [x] Size 137 | - [x] Speech 138 | - [x] TextField 139 | - [x] Timer 140 | - [ ] UITable 141 | - [x] UITableCell 142 | - [x] UITableRow 143 | - [x] URLScheme 144 | - [x] UUID 145 | - [ ] WebView 146 | - [x] WidgetDate 147 | - [x] WidgetImage 148 | - [x] WidgetSpacer 149 | - [x] WidgetStack 150 | - [x] WidgetText 151 | - [x] XMLParser 152 | 153 | ## Things to do once this sort of works 154 | 155 | - [ ] Integrate the actual system keychain on macOS 156 | - [ ] Share sheet--somehow? -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | // Set up args and config here 3 | const scriptPath; // This will be defined by the CLI arguments 4 | 5 | Object.assign(globalThis, require("./index.js")); 6 | 7 | console.logError = (msg) => { 8 | _scriptable_deprecation("console.logError", "1.3", "Use console.error(message) instead.") 9 | console.error(msg) 10 | }; 11 | 12 | Script.name = function () { 13 | return path.basename(scriptPath, path.extname(scriptPath)); 14 | }; 15 | 16 | function _scriptable_didRun(result, error) { 17 | if (error !== undefined) console.error(error) 18 | } 19 | 20 | let _postFunctions = []; 21 | 22 | function dispatch(fn) { 23 | _postFunctions.push(fn); 24 | } 25 | 26 | let _scriptable_run = (async ()=>{}).constructor(fs.readFileSync(scriptPath)); 27 | 28 | // This is pretty raw, straight from how the actual app does it. 29 | // I have not tested this code yet, so it will probably change. 30 | _scriptable_run().then(output => { 31 | if (output !== undefined && Script.shortcutOutput() === null) { 32 | Script.setShortcutOutput(output); 33 | } 34 | _scriptable_didRun(output, undefined); 35 | }).catch(err => { 36 | dispatch(function() { 37 | _scriptable_didRun(undefined, err); 38 | }); 39 | }).finally(() => _postFunctions.forEach(fn=>fn())); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // As APIs are completed, they will be uncommented. 4 | 5 | exports.Alert = require("./src/alert.js"); 6 | 7 | // Undocumented App class, available in Scriptable 8 | exports.App = require("./src/app.js"); 9 | 10 | //exports.Calendar = require("./src/calendar.js"); 11 | 12 | //exports.CalendarEvent = require("./src/calendar-event.js"); 13 | 14 | exports.CallbackURL = require("./src/callback-url.js"); 15 | 16 | exports.Color = require("./src/color.js"); 17 | 18 | //exports.Contact = require("./src/contact.js"); 19 | 20 | //exports.ContactsContainer = require("./src/contacts-container.js"); 21 | 22 | //exports.ContactsGroup = require("./src/contacts-group.js"); 23 | 24 | exports.Data = require("./src/data.js"); 25 | 26 | exports.DateFormatter = require("./src/date-formatter.js"); 27 | 28 | exports.DatePicker = require("./src/date-picker.js"); 29 | 30 | exports.Device = require("./src/device.js"); 31 | 32 | //exports.Dictation = require("./src/dictation.js"); 33 | 34 | exports.DocumentPicker = require("./src/document-picker.js"); 35 | 36 | exports.DrawContext = require("./src/draw-context.js"); 37 | 38 | exports.FileManager = require("./src/file-manager.js"); 39 | 40 | exports.Font = require("./src/font.js"); 41 | 42 | exports.Image = require("./src/image.js"); 43 | 44 | exports.importModule = require("./src/import-module.js"); 45 | 46 | exports.Keychain = require("./src/keychain.js"); 47 | 48 | exports.LinearGradient = require("./src/linear-gradient.js"); 49 | 50 | exports.ListWidget = require("./src/list-widget.js"); 51 | 52 | exports.Location = require("./src/location.js"); 53 | 54 | exports.Mail = require("./src/mail.js"); 55 | 56 | exports.Message = require("./src/message.js"); 57 | 58 | exports.Notification = require("./src/notification.js"); 59 | 60 | exports.Pasteboard = require("./src/pasteboard.js"); 61 | 62 | exports.Path = require("./src/path.js"); 63 | 64 | exports.Photos = require("./src/photos.js"); 65 | 66 | exports.Point = require("./src/point.js"); 67 | 68 | exports.QuickLook = require("./src/quick-look.js"); 69 | 70 | exports.Rect = require("./src/rect.js"); 71 | 72 | exports.RecurrenceRule = require("./src/recurrence-rule.js"); 73 | 74 | exports.RelativeDateTimeFormatter = require("./src/relative-date-time-formatter.js"); 75 | 76 | //exports.Reminder = require("./src/reminder.js"); 77 | 78 | exports.Request = require("./src/request.js"); 79 | 80 | exports.Safari = require("./src/safari.js"); 81 | 82 | exports.Script = require("./src/script.js"); 83 | 84 | exports.SFSymbol = require("./src/sf-symbol.js"); 85 | 86 | exports.ShareSheet = require("./src/share-sheet.js"); 87 | 88 | exports.Size = require("./src/size.js"); 89 | 90 | exports.Speech = require("./src/speech.js"); 91 | 92 | exports.TextField = require("./src/text-field.js"); 93 | 94 | exports.Timer = require("./src/timer.js"); 95 | 96 | exports.UITable = require("./src/ui-table.js"); 97 | 98 | exports.UITableCell = require("./src/ui-table-cell.js"); 99 | 100 | exports.UITableRow = require("./src/ui-table-row.js"); 101 | 102 | exports.URLScheme = require("./src/url-scheme.js"); 103 | 104 | exports.UUID = require("./src/uuid.js"); 105 | 106 | exports.WebView = require("./src/web-view.js"); 107 | 108 | exports.XMLParser = require("./src/xml-parser.js"); 109 | 110 | /* 111 | Global functions for the console 112 | 113 | _scriptable_log, _scriptable_logError, and _scriptable_logWarning 114 | are the undocumented internal functions used in the real Scriptable app 115 | that actually log things to the console. 116 | */ 117 | exports.log = exports._scriptable_log = console.log; 118 | exports.logError = exports._scriptable_logError = console.error; 119 | exports.logWarning = exports._scriptable_logWarning = console.warn; 120 | 121 | // Create and log deprecation messages 122 | exports._scriptable_deprecation = function (itemName, version, message) { 123 | console.warn(`${itemName} was deprecated in version ${version}. ${message}`); 124 | }; 125 | 126 | // Convert everything to a string, like Scriptable does for its console 127 | // This differs slightly from Scriptable's implmentation, since it actually stringifies 128 | // null and String objects rather than simply returning them. 129 | exports._scriptable_createLogMessage = require("./util/create-log-message.js"); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scriptable-node", 3 | "version": "1.0.0", 4 | "description": "Porting the APIs from Scriptable on iOS to Node.JS for all platforms.", 5 | "main": "index.js", 6 | "directories": { 7 | "lib": "lib" 8 | }, 9 | "dependencies": { 10 | "brightness": "^3.0.0", 11 | "canvas": "^2.6.1", 12 | "clipboardy": "^2.3.0", 13 | "computer-name": "^0.1.0", 14 | "file-uti": "^1.0.0", 15 | "hex-rgb": "^4.3.0", 16 | "image-size": "^0.9.3", 17 | "loudness": "^0.4.1", 18 | "luxon": "^2.0.2", 19 | "macos-open-file-dialog": "^1.0.1", 20 | "node-fetch": "^2.6.1", 21 | "open": "^7.3.0", 22 | "os-locale": "^6.0.0", 23 | "sax": "^1.2.4", 24 | "say": "^0.16.0", 25 | "systeminformation": "^5.8.7", 26 | "uti": "^6.3.12", 27 | "uuid": "^8.3.2" 28 | }, 29 | "scripts": { 30 | "test": "echo \"Error: no test specified\" && exit 1" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/FifiTheBulldog/scriptable-node.git" 35 | }, 36 | "author": "", 37 | "license": "MIT", 38 | "bugs": { 39 | "url": "https://github.com/FifiTheBulldog/scriptable-node/issues" 40 | }, 41 | "homepage": "https://github.com/FifiTheBulldog/scriptable-node#readme" 42 | } 43 | -------------------------------------------------------------------------------- /src/alert.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const readline = require("readline"); 4 | 5 | const TextField = require("./text-field.js"); 6 | 7 | async function shellPresent(alertType) { 8 | // Need to create a command line interface to simulate the fields and options of an Alert 9 | // Support for actual UI alerts may come later. 10 | } 11 | 12 | class Action { 13 | /** 14 | * 15 | * @param {string} title 16 | * @param {*} type 17 | */ 18 | constructor(title, type) { 19 | this.title = title; 20 | this.type = type; 21 | } 22 | } 23 | 24 | class Alert { 25 | #actions; 26 | #fields; 27 | #values; 28 | 29 | constructor() { 30 | this.title = null; 31 | this.message = null; 32 | this.#actions = []; 33 | this.#fields = []; 34 | this.#values = []; 35 | } 36 | 37 | /** 38 | * @param {string} title 39 | */ 40 | addAction(title) { 41 | this.#actions.push(new Action(title, "action")); 42 | } 43 | 44 | /** 45 | * @param {string} title 46 | */ 47 | addDestructiveAction(title) { 48 | this.#actions.push(new Action(title, "destructive")); 49 | } 50 | 51 | /** 52 | * @param {string} title 53 | */ 54 | addCancelAction(title) { 55 | this.#actions.push(new Action(title, "cancel"));; 56 | } 57 | 58 | /** 59 | * @param {string} text 60 | * @param {string} placeholder 61 | */ 62 | addTextField(placeholder, text) { 63 | const field = new TextField(placeholder, text, false); 64 | this.#fields.push(field); 65 | return field; 66 | } 67 | 68 | /** 69 | * @param {string} text 70 | * @param {string} placeholder 71 | */ 72 | addSecureTextField(placeholder, text) { 73 | const field = new TextField(placeholder, text, true); 74 | this.#fields.push(field); 75 | return field; 76 | } 77 | 78 | /** 79 | * @param {number} index 80 | */ 81 | textFieldValue(index) { 82 | return this.#values[index]; 83 | } 84 | 85 | async present() { 86 | return (await this.presentAlert()); 87 | } 88 | 89 | async presentAlert() { 90 | return (await shellPresent("alert")); 91 | } 92 | 93 | async presentSheet() { 94 | return (await shellPresent("sheet")); 95 | } 96 | } 97 | 98 | module.exports = Alert; -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // This is iOS-specific and does nothing for now. 4 | // It doesn't even do anything on macOS in the Scriptable for Mac beta. 5 | // Maybe implement something later? 6 | // Or not, could just leave this non-functional. 7 | // This method might need to be overwritten per platform. 8 | exports.close = function () {}; -------------------------------------------------------------------------------- /src/calendar-event.js: -------------------------------------------------------------------------------- 1 | class CalendarEvent { 2 | constructor() { 3 | this.title = "Created with Scriptable"; 4 | this.location = null; 5 | this.notes = null; 6 | this.startDate = (new Date()).setMinutes(new Date().getMinutes + 30); 7 | this.endDate = this.endDate; 8 | this.isAllDay = false; 9 | this.attendees = null; 10 | this.availability = "notSupported"; 11 | this.timeZone = null; 12 | this.calendar = null; 13 | Object.defineProperty(this, "_recurrence", { 14 | value: [], 15 | writable: true, 16 | }); 17 | } 18 | 19 | addRecurrenceRule(recurrenceRule) { 20 | this._recurrence.push(recurrenceRule); 21 | } 22 | 23 | removeAllRecurrenceRules() { 24 | this._recurrence = []; 25 | } 26 | 27 | save() { 28 | } 29 | 30 | remove() { 31 | } 32 | 33 | async presentEdit() { 34 | } 35 | 36 | static async presentCreate() { 37 | } 38 | 39 | static async tomorrow(calendars) { 40 | } 41 | 42 | static async yesterday(calendars) { 43 | } 44 | 45 | static async thisWeek(calendars) { 46 | } 47 | 48 | static async nextWeek(calendars) { 49 | } 50 | 51 | static async lastWeek(calendars) { 52 | } 53 | 54 | static async between(startDate, endDate, calendars) { 55 | } 56 | } 57 | 58 | module.exports = CalendarEvent; -------------------------------------------------------------------------------- /src/calendar.js: -------------------------------------------------------------------------------- 1 | const Color = require('./color') 2 | 3 | class Calendar { 4 | constructor() { 5 | } 6 | 7 | /** 8 | * @readonly 9 | * @type {string} 10 | */ 11 | identifier 12 | 13 | /** 14 | * @type {string} 15 | */ 16 | title 17 | 18 | /** 19 | * @readonly 20 | * @type {boolean} 21 | */ 22 | isSubscribed 23 | 24 | /** 25 | * @readonly 26 | * @type {boolean} 27 | */ 28 | allowsContentModifications 29 | 30 | /** 31 | * @type {Color} 32 | */ 33 | color 34 | 35 | /** 36 | * 37 | * @param {string} availability - Availability to check against. 38 | * @returns {boolean} - True if the calendar supports the availability, otherwise false. 39 | */ 40 | supportsAvailability(availability) { 41 | return this.supportsAvailability(availability) 42 | } 43 | 44 | save() { 45 | return this.save() 46 | } 47 | 48 | remove() { 49 | return this.remove() 50 | } 51 | 52 | /** 53 | * @returns {Promise<[Calendar]>} - Promise that provides the calendars when fulfilled. 54 | */ 55 | static forReminders() { 56 | return this.forReminders() 57 | } 58 | 59 | /** 60 | * @returns {Promise<[Calendar]>} - Promise that provides the calendars when fulfilled. 61 | */ 62 | static forEvents() { 63 | return this.forEvents() 64 | } 65 | 66 | /** 67 | * @param {string} title 68 | * @returns {Promise} 69 | */ 70 | static forRemindersByTitle() { 71 | return this.forRemindersByTitle() 72 | } 73 | 74 | /** 75 | * @param {string} title 76 | * @returns {Promise} 77 | */ 78 | static forEventsByTitle() { 79 | return this.forEventsByTitle() 80 | } 81 | 82 | /** 83 | * 84 | * @param {string} title 85 | * @returns {Promise} 86 | */ 87 | static createForReminders(ttile) { 88 | return this.createForReminders(ttile) 89 | } 90 | } 91 | 92 | module.exports = Calendar; -------------------------------------------------------------------------------- /src/callback-url.js: -------------------------------------------------------------------------------- 1 | const querystring = require('querystring'); 2 | 3 | class CallbackURL { 4 | constructor(baseURL) { 5 | Object.defineProperty(this, "_url", { 6 | value: new URL(baseURL), 7 | writable: true 8 | }); 9 | this._url.search = ''; 10 | } 11 | 12 | addParameter(name, value) { 13 | this._url.searchParams.append(name, value); 14 | } 15 | 16 | async open() { 17 | if (process.platform == "darwin") { 18 | // Open x-callback URL and return response 19 | const appPath = require('path').normalize(`${__dirname}/../lib/xcall.app/Contents/MacOS/xcall`); 20 | require('child_process').execFile(appPath, ["-url", this.getURL()], (error, stdout, stderr) => { 21 | if (error == null) { 22 | return stdout; 23 | } else { 24 | throw error; 25 | } 26 | }); 27 | } else { 28 | throw new Error(`x-callback-url not supported on platform '${process.platform}.`); 29 | } 30 | } 31 | 32 | getURL() { 33 | this._url.searchParams.set('x-source', 'Scriptable'); 34 | const paramArray = ["success", "error", "cancel"]; 35 | for (var i = 0; i < 3; i++) { 36 | this._url.searchParams.set(`x-${paramArray[i]}`, `scriptable://x-callback-url/${paramArray[i]}`); 37 | } 38 | return this._url.toString(); 39 | } 40 | } 41 | 42 | module.exports = CallbackURL; -------------------------------------------------------------------------------- /src/color.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const hexRgb = require("hex-rgb"); 4 | 5 | /** 6 | * @param {number} int 7 | */ 8 | function intToHex(int) { 9 | const hexString = int.toString(16).toUpperCase(); 10 | return (hexString.length === 1) ? hexString.repeat(2) : hexString; 11 | } 12 | 13 | class Color { 14 | #hex; 15 | #red; 16 | #green; 17 | #blue; 18 | #alpha; 19 | 20 | /** 21 | * @readonly 22 | * @param {string} hex 23 | * @param {number} alpha 24 | */ 25 | 26 | constructor(hex, alpha) { 27 | const rgb = hexRgb(hex); 28 | 29 | Object.defineProperty(this, "hex", { 30 | value: intToHex(rgb.red) + intToHex(rgb.green) + intToHex(rgb.blue), 31 | writable: false, 32 | enumerable: true 33 | }); 34 | 35 | Object.defineProperty(this, "red", { 36 | value: rgb.red / 255, 37 | writable: false, 38 | enumerable: true 39 | }); 40 | 41 | Object.defineProperty(this, "green", { 42 | value: rgb.green / 255, 43 | writable: false, 44 | enumerable: true 45 | }); 46 | 47 | Object.defineProperty(this, "blue", { 48 | value: rgb.blue / 255, 49 | writable: false, 50 | enumerable: true 51 | }); 52 | 53 | Object.defineProperty(this, "alpha", { 54 | value: (alpha === undefined) ? rgb.alpha : alpha, 55 | writable: false, 56 | enumerable: true 57 | }); 58 | } 59 | 60 | static black() { 61 | return new this("000000", 1); 62 | } 63 | 64 | static darkGray() { 65 | return new this("555555", 1); 66 | } 67 | 68 | static lightGray() { 69 | return new this("AAAAAA", 1); 70 | } 71 | 72 | static white() { 73 | return new this("FFFFFF", 1); 74 | } 75 | 76 | static gray() { 77 | return new this("808080", 1); 78 | } 79 | 80 | static red() { 81 | return new this("FF0000", 1); 82 | } 83 | 84 | static green() { 85 | return new this("00FF00", 1); 86 | } 87 | 88 | static blue() { 89 | return new this("0000FF", 1); 90 | } 91 | 92 | static cyan() { 93 | return new this("00FFFF", 1); 94 | } 95 | 96 | static yellow() { 97 | return new this("FFFF00", 1); 98 | } 99 | 100 | static magenta() { 101 | return new this("FF00FF", 1); 102 | } 103 | 104 | static orange() { 105 | return new this("FF8000", 1); 106 | } 107 | 108 | static purple() { 109 | return new this("800080", 1); 110 | } 111 | 112 | static brown() { 113 | return new this("996633", 1); 114 | } 115 | 116 | static clear() { 117 | return new this("000000", 0); 118 | } 119 | 120 | /** 121 | * @param {Color} lightColor 122 | * @param {Color} darkColor 123 | */ 124 | static dynamic(lightColor, darkColor) { 125 | return (require("./device.js").isUsingDarkAppearance()) ? darkColor : lightColor; 126 | } 127 | } 128 | 129 | module.exports = Color; -------------------------------------------------------------------------------- /src/contact.js: -------------------------------------------------------------------------------- 1 | const ContactsContainer = require('./contacts-container'); 2 | const ContactsGroup = require('./contacts-group'); 3 | 4 | class Contact { 5 | /** 6 | * @param {string} namePrefix 7 | * @param {string} givenName 8 | * @param {string} middleName 9 | * @param {string} familyName 10 | * @param {string} nickname 11 | * @param {Date} birthday 12 | * @param {[string]} emailAddresses 13 | * @param {[string]} phoneNumbers 14 | * @param {[string]} postalAddresses 15 | * @param {[string]} socialProfiles 16 | * @param {string} note 17 | * @param {[string]} urlAddresses 18 | * @param {[any]} dates 19 | * @param {string} organizationName 20 | * @param {string} departmentName 21 | * @param {string} jobTitle 22 | */ 23 | constructor(namePrefix, givenName, middleName, familyName, nickname, birthday, emailAddresses, phoneNumbers, postalAddresses, socialProfiles, note, urlAddresses, dates, organizationName, departmentName, jobTitle) { 24 | Object.defineProperty(this, "identifier", { 25 | value: "" 26 | }); 27 | this.namePrefix = namePrefix; 28 | this.givenName = givenName; 29 | this.middleName = middleName; 30 | this.familyName = familyName; 31 | this.nickname = nickname; 32 | this.birthday = birthday; 33 | this.image = null; 34 | this.emailAddresses = emailAddresses; 35 | this.phoneNumbers = phoneNumbers; 36 | this.postalAddresses = postalAddresses; 37 | this.socialProfiles = socialProfiles; 38 | this.note = note; 39 | this.urlAddresses = urlAddresses; 40 | this.dates = dates; 41 | this.organizationName = organizationName; 42 | this.departmentName = departmentName; 43 | this.jobTitle = jobTitle; 44 | 45 | let availableProperties = []; 46 | for (item of [ 47 | "NamePrefix", 48 | "GiveName", 49 | "MiddleName", 50 | "FamilyName", 51 | "Nickname", 52 | "Birthday", 53 | "EmailAddresses", 54 | "PhoneNumbers", 55 | "PostalAddresses", 56 | "SocialProfiles", 57 | "Image", 58 | "Note", 59 | "URLAddresses", 60 | "OrganizationName", 61 | "DepartmentName", 62 | "JobTitle", 63 | "Dates" 64 | ]) { 65 | availableProperties.push(`is${item}Available`); 66 | } 67 | for (item of availableProperties) { 68 | Object.defineProperty(this, item, {value: true}); 69 | } 70 | } 71 | 72 | /** 73 | * @param {[ContactsContainer]} containers 74 | * @returns {Promise<[Contact]>} - Promise that provides the contacts when fulfilled. 75 | */ 76 | static async all(containers) { 77 | return this.all(containers) 78 | } 79 | 80 | /** 81 | * @param {[ContactsGroup]} groups 82 | * @returns {Promise<[Contact]>} - Promise that provides the contacts when fulfilled. 83 | */ 84 | static async inGroups(groups) { 85 | return this.inGroups(groups) 86 | } 87 | 88 | /** 89 | * @param {Contact} contact 90 | * @param {string} containerIdentifier 91 | */ 92 | static add(contact, containerIdentifier) { 93 | return this.add(contact, containerIdentifier) 94 | } 95 | 96 | /** 97 | * @param {Contact} contact 98 | */ 99 | static update(contact) { 100 | return this.update(contact) 101 | } 102 | 103 | /** 104 | * @param {Contact} contact 105 | */ 106 | static delete(contact) { 107 | return this.delete(contact) 108 | } 109 | 110 | /** 111 | * @returns {Promise} - Promise that fulfills when the changes have been persisted. The promise carries no value. 112 | */ 113 | static async persistChanges() { 114 | } 115 | } 116 | 117 | module.exports = Contact; -------------------------------------------------------------------------------- /src/contacts-container.js: -------------------------------------------------------------------------------- 1 | class ContactsContainer { 2 | constructor() { 3 | } 4 | } 5 | 6 | module.exports = { 7 | default: async function() { 8 | }, 9 | 10 | all: async function() { 11 | }, 12 | 13 | withIdentifier: async function(identifier) { 14 | return (await this.all()).filter(container => container.identifier = identifier); 15 | } 16 | } 17 | module.exports = ContactsContainer; -------------------------------------------------------------------------------- /src/contacts-group.js: -------------------------------------------------------------------------------- 1 | class ContactsGroup { 2 | constructor() { 3 | this.name = ""; 4 | Object.defineProperty(this, "_contacts", { 5 | value: [], 6 | writable: true 7 | }); 8 | } 9 | 10 | static async all(containers) { 11 | let contacts = []; 12 | for (c of containers) { 13 | // Retrieve all contacts and append them to the contacts array 14 | } 15 | return contacts; 16 | } 17 | 18 | addMember(contact) { 19 | this._contacts.push(contact); 20 | } 21 | 22 | removeMember(contact) { 23 | this._contacts.splice(this._contacts.indexOf(contact), 1); 24 | } 25 | 26 | static add(group, containerIdentifier) { 27 | } 28 | 29 | static update(group) { 30 | } 31 | 32 | static delete(group) { 33 | } 34 | } 35 | 36 | module.exports = ContactsGroup; -------------------------------------------------------------------------------- /src/data.js: -------------------------------------------------------------------------------- 1 | /* 2 | To do: proper image format conversions for fromJPEG() and fromPNG(). 3 | Still haven't found a good synchronous method, so for now no conversion takes place. 4 | Those two methods do the same thing: return the data from the image. 5 | */ 6 | 7 | "use strict"; 8 | 9 | const fs = require("fs"); 10 | 11 | const dataKey = Symbol.for("data"); 12 | 13 | class Data { 14 | constructor(data) { 15 | Object.defineProperty(this, dataKey, { 16 | value: data, 17 | enumerable: false, 18 | writable: false 19 | }); 20 | } 21 | 22 | toRawString() { 23 | try { 24 | return this[dataKey].toString("utf-8"); 25 | } catch { 26 | return null; 27 | } 28 | } 29 | 30 | toBase64String() { 31 | return this[dataKey].toString('base64'); 32 | } 33 | 34 | getBytes() { 35 | return Array.from(this[dataKey]); 36 | } 37 | } 38 | 39 | module.exports = { 40 | fromBase64String: function (base64String) { 41 | try { 42 | return new Data(Buffer.from(base64String, "base64")); 43 | } catch { 44 | return null; 45 | } 46 | }, 47 | 48 | fromFile: function (filePath) { 49 | return new Data(fs.readFileSync(filePath)); 50 | }, 51 | 52 | // Figure out how to differentiate/convert JPEG vs PNG later 53 | fromJPEG: function (image) { 54 | return new Data(image[dataKey]); 55 | }, 56 | 57 | fromPNG: function (image) { 58 | return new Data(image[dataKey]); 59 | }, 60 | 61 | fromString: function (string) { 62 | try { 63 | return new Data(Buffer.from(string, "utf-8")); 64 | } catch { 65 | return null; 66 | } 67 | } 68 | }; 69 | // Uncomment once finished 70 | // module.exports = Data; -------------------------------------------------------------------------------- /src/date-formatter.js: -------------------------------------------------------------------------------- 1 | const Device = require("./device.js"); 2 | const format = require("date-fns/format"); 3 | const parse = require("date-fns/parse"); 4 | 5 | class DateFormatter { 6 | #dateStyle; 7 | #timeStyle; 8 | #customFormat; 9 | #useCustomFormat; 10 | #locale; 11 | 12 | constructor() { 13 | this.dateFormat = ""; 14 | this.locale = Device.locale(); 15 | this.#dateStyle = null; 16 | this.#timeStyle = null; 17 | this.#useCustomFormat = true; 18 | this.#locale = undefined; 19 | } 20 | 21 | get dateFormat() { 22 | if (this.#useCustomFormat) { 23 | return this.#customFormat; 24 | } else { 25 | if (this.#dateStyle !== "" && this.#timeStyle !== "") { 26 | const separator = (this.#dateStyle === "M/d/yy") ? ", " : " 'at' "; 27 | return this.#dateStyle + separator + this.#timeStyle; 28 | } else { 29 | return this.#dateStyle + this.#timeStyle; 30 | } 31 | } 32 | } 33 | 34 | set dateFormat(str) { 35 | this.#useCustomFormat = true; 36 | this.#customFormat = str; 37 | } 38 | 39 | set locale(localeString) { 40 | this.#locale = localeString; 41 | } 42 | 43 | #customFormatOff() { 44 | this.#useCustomFormat = false; 45 | } 46 | 47 | string(date) { 48 | if (this.#useCustomFormat) { 49 | // return the custom formatted date 50 | } else { 51 | if (this.#dateStyle === null && this.#timeStyle === null) { 52 | return ""; 53 | } 54 | const formatOptions = {}; 55 | if (this.#dateStyle) formatOptions.dateStyle = this.#dateStyle; 56 | if (this.#timeStyle) formatOptions.timeStyle = this.#timeStyle; 57 | return (new Intl.DateTimeFormat(this.#locale, formatOptions)).format(date); 58 | } 59 | } 60 | 61 | date(str) { 62 | try { 63 | return parse(date, this.dateFormat, { 64 | locale: this.locale, 65 | useAdditionalDayOfYearTokens: true, 66 | useAdditionalWeekYearTokens: true 67 | }); 68 | } catch { 69 | return null; 70 | } 71 | } 72 | 73 | useNoDateStyle() { 74 | this.#dateStyle = null; 75 | this.#customFormatOff(); 76 | } 77 | 78 | useShortDateStyle() { 79 | this.#dateStyle = "short"; 80 | this.#customFormatOff(); 81 | } 82 | 83 | useMediumDateStyle() { 84 | this.#dateStyle = "medium"; 85 | this.#customFormatOff(); 86 | } 87 | 88 | useLongDateStyle() { 89 | this.#dateStyle = "long"; 90 | this.#customFormatOff(); 91 | } 92 | 93 | useFullDateStyle() { 94 | this.#dateStyle = "full"; 95 | this.#customFormatOff(); 96 | } 97 | 98 | useNoTimeStyle() { 99 | this.#timeStyle = null; 100 | this.#customFormatOff(); 101 | } 102 | 103 | useShortTimeStyle() { 104 | this.#timeStyle = "short"; 105 | this.#customFormatOff(); 106 | } 107 | 108 | useMediumTimeStyle() { 109 | this.#timeStyle = "medium"; 110 | this.#customFormatOff(); 111 | } 112 | 113 | useLongTimeStyle() { 114 | this.#timeStyle = "long"; 115 | this.#customFormatOff(); 116 | } 117 | 118 | useFullTimeStyle() { 119 | this.#timeStyle = "full"; 120 | this.#customFormatOff(); 121 | } 122 | } 123 | 124 | module.exports = DateFormatter; -------------------------------------------------------------------------------- /src/date-picker.js: -------------------------------------------------------------------------------- 1 | class DatePicker { 2 | constructor() { 3 | this.minimumDate = null; 4 | this.maximumDate = null; 5 | this.countdownDuration = 0; 6 | this.minuteInterval = 1; 7 | this.initialDate = null; 8 | } 9 | 10 | async pickTime() { 11 | } 12 | 13 | async pickDate() { 14 | } 15 | 16 | async pickDateAndTime() { 17 | } 18 | 19 | pickCountdownDuration() { 20 | } 21 | } 22 | 23 | module.exports = DatePicker; -------------------------------------------------------------------------------- /src/device.js: -------------------------------------------------------------------------------- 1 | // Still unfinished: 2 | // - screenSize 3 | // - screenScale 4 | // - preferredLanguages 5 | 6 | "use strict"; 7 | 8 | const brightness = require("brightness"); 9 | const computerName = require("computer-name"); 10 | const { execFileSync } = require("child_process"); 11 | const { execString, getDirect } = require("../util/async-syncifier.js"); 12 | const runJXA = require("../util/run-jxa.js"); 13 | const Size = require("./size.js"); 14 | const scriptableTypeError = require("../util/type-error.js"); 15 | 16 | const IPHONE_MODEL = /iPhone\d+,\d+/; 17 | const IPAD_MODEL = /iPad\d+,\d+/; 18 | 19 | const infoStore = {}; 20 | 21 | const systemInStore = key => { 22 | if (!(key in infoStore)) { 23 | infoStore[key] = JSON.parse(getDirect("cjs", "systeminformation", key)); 24 | } 25 | return infoStore[key]; 26 | }; 27 | 28 | module.exports = { 29 | name: computerName, 30 | 31 | systemName: function () { 32 | return systemInStore("osInfo").distro; 33 | }, 34 | 35 | systemVersion: function () { 36 | return systemInStore("osInfo").release; 37 | }, 38 | 39 | model: function () { 40 | // Not a perfect replacement (normally returns "iPhone" or "iPad"), 41 | // but it'll do for now 42 | return systemInStore("chassis").model; 43 | }, 44 | 45 | isPhone: function () { 46 | return this.model().match(IPHONE_MODEL) !== null; 47 | }, 48 | 49 | isPad: function () { 50 | return this.model().match(IPAD_MODEL) !== null; 51 | }, 52 | 53 | screenSize: function () { 54 | // Pick a random set of dimensions 55 | return new Size(400, 600) 56 | }, 57 | 58 | screenResolution: function () { 59 | const displayData = systemInStore("graphics").displays[0]; 60 | return new Size(displayData.resolutionX, displayData.resolutionY); 61 | }, 62 | 63 | screenScale: function () { 64 | }, 65 | 66 | screenBrightness: function () { 67 | try { 68 | return Number(execString(`require("brightness").get().then(level => { 69 | process.stdout.write(level.toString()) 70 | })`)); 71 | } catch { 72 | return 1; 73 | } 74 | }, 75 | 76 | isInPortrait: function () { 77 | const screenSize = this.screenSize(); 78 | return screenSize.width > screenSize.height; 79 | }, 80 | 81 | isInPortraitUpsideDown: function () { 82 | // No way to detect upside down for portrait 83 | return false; 84 | }, 85 | 86 | isInLandscapeLeft: function () { 87 | // No way to detect right or left for landscape 88 | return false; 89 | }, 90 | 91 | isInLandscapeRight: function () { 92 | // No way to detect right or left for landscape 93 | return false; 94 | }, 95 | 96 | isFaceUp: function () { 97 | // No position sensors in most devices that can run Node.JS 98 | return false; 99 | }, 100 | 101 | isFaceDown: function () { 102 | // No position sensors in most devices that can run Node.JS 103 | return false; 104 | }, 105 | 106 | batteryLevel: function () { 107 | const batteryData = JSON.parse(getDirect("cjs", "systeminformation", "battery")); 108 | if (batteryData.percent !== undefined) { 109 | return batteryData.percent / 100; 110 | } 111 | return 1; 112 | }, 113 | 114 | isDischarging: function () { 115 | return !this.isCharging(); 116 | }, 117 | 118 | isCharging: function () { 119 | const batteryData = JSON.parse(getDirect("cjs", "systeminformation", "battery")); 120 | if (batteryData.isCharging !== undefined) return batteryData.isCharging; 121 | return false; 122 | }, 123 | 124 | isFullyCharged: function () { 125 | return this.batteryLevel() === 1; 126 | }, 127 | 128 | preferredLanguages: function () { 129 | // For now, assume en-US 130 | return ["en-US"]; 131 | }, 132 | 133 | locale: function () { 134 | if (!("locale" in infoStore)) { 135 | infoStore.locale = getDirect("esm", "os-locale", "osLocale") 136 | .replace("-", "_"); 137 | } 138 | return infoStore.locale; 139 | }, 140 | 141 | language: function () { 142 | return this.locale().slice(0, 2); 143 | }, 144 | 145 | isUsingDarkAppearance: function () { 146 | if (process.platform === "darwin") { 147 | return execFileSync("osascript", [ 148 | "-e", 149 | "tell app \"System Events\" to tell appearance preferences to get dark mode" 150 | ], { encoding: "utf-8" }).trim() === "true"; 151 | } 152 | // For now, assume false on non-macOS platforms 153 | return false; 154 | }, 155 | 156 | volume: function () { 157 | if (process.platform === "darwin") { 158 | const volumeSettings = JSON.parse(runJXA("JSON.stringify(app.getVolumeSettings())", true)) 159 | if (volumeSettings.outputMuted) return 0; 160 | return volumeSettings.outputVolume / 100; 161 | } 162 | return Number(getDirect("cjs", "loudness", "getVolume")) / 100; 163 | }, 164 | 165 | /** 166 | * @param {number} percentage 167 | */ 168 | setScreenBrightness: function (percentage) { 169 | if (typeof percentage !== "number") { 170 | throw scriptableTypeError("number", typeof percentage) 171 | } 172 | try { 173 | brightness.set(percentage); 174 | } catch {} 175 | } 176 | }; -------------------------------------------------------------------------------- /src/dictation.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const defaultLocale = require("./device.js").locale(); 4 | 5 | exports.start = async function (locale = defaultLocale) { 6 | // Dictate text, return transcription 7 | // Here's some placeholder text for the time being 8 | return "Dictation is still under construction. Come back later!"; 9 | }; -------------------------------------------------------------------------------- /src/document-picker.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | open: async function (types) { 3 | }, 4 | 5 | openFile: async function () { 6 | }, 7 | 8 | openFolder: async function () { 9 | }, 10 | 11 | export: async function (path) { 12 | }, 13 | 14 | exportString: async function (content, name) { 15 | }, 16 | 17 | exportImage: async function (image, name) { 18 | }, 19 | 20 | exportData: async function (data, name) { 21 | } 22 | } -------------------------------------------------------------------------------- /src/draw-context.js: -------------------------------------------------------------------------------- 1 | const { createCanvas } = require("canvas"); 2 | const Size = require("./size"); 3 | 4 | class DrawContext { 5 | #canvas; 6 | #ctx; 7 | 8 | constructor() { 9 | this.size = new Size(200, 200); 10 | this.respectScreenScale = false; 11 | this.opaque = true; 12 | this.#canvas = createCanvas(); 13 | this.#ctx = this.#canvas.getContext("2d"); 14 | } 15 | 16 | getImage() { 17 | } 18 | 19 | drawImageInRect(image, rect) { 20 | } 21 | 22 | drawImageAtPoint(image, point) { 23 | } 24 | 25 | setFillColor(color) { 26 | } 27 | 28 | setStrokeColor(color) { 29 | this.#ctx.strokeStyle = color.hex; 30 | } 31 | 32 | setLineWidth(width) { 33 | } 34 | 35 | fill(rect) { 36 | } 37 | 38 | fillRect(rect) { 39 | } 40 | 41 | fillElipse(rect) { 42 | } 43 | 44 | stroke(rect) { 45 | } 46 | 47 | strokeRect(rect) { 48 | } 49 | 50 | strokeEllipse(rect) { 51 | } 52 | 53 | addPath(path) { 54 | } 55 | 56 | strokePath() { 57 | } 58 | 59 | fillPath() { 60 | } 61 | 62 | drawText(text, pos) { 63 | } 64 | 65 | drawTextInRect(text, rect) { 66 | } 67 | 68 | setFont(font) { 69 | } 70 | 71 | setTextColor(color) { 72 | } 73 | 74 | setTextAlignedLeft() { 75 | } 76 | 77 | setTextAlignedCenter() { 78 | } 79 | 80 | setTextAlignedRight() { 81 | } 82 | } 83 | 84 | module.exports = DrawContext; -------------------------------------------------------------------------------- /src/file-manager.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | To do: 4 | 5 | - constructor 6 | - cacheDirectory 7 | - documentsDirectory 8 | - libraryDirectory 9 | - allTags 10 | - addTag 11 | - removeTag 12 | - readExtendedAttribute 13 | - writeExtendedAttribute 14 | - removeExtendedAttribute 15 | - allExtendedAttributes 16 | - getUTI (possibly use the uti module on npm for this?) 17 | - bookmarkedPath 18 | - bookmarkExists 19 | - downloadFileFromiCloud 20 | - isFileStoredIniCloud 21 | - isFileDownloaded 22 | - allFileBookmarks 23 | 24 | */ 25 | 26 | const Data = require("./data.js"); 27 | const fs = require("fs"); 28 | const Image = require("./image.js"); 29 | const os = require("os"); 30 | const path = require("path"); 31 | const dataKey = Symbol.for("data"); 32 | 33 | // Yes, yes, I know, don't use JSON as a database 34 | const BOOKMARKS_PATH = "../config/file-bookmarks.json"; 35 | 36 | class FileManager { 37 | #type; 38 | 39 | constructor(type) { 40 | this.#type = type; 41 | if (type == 'iCloud') { 42 | // Choose the iCloud directories, if available 43 | } else { 44 | // Choose the home directory 45 | } 46 | } 47 | 48 | read(filePath) { 49 | return Data.fromFile(filePath); 50 | } 51 | 52 | readString(filePath) { 53 | return fs.readFileSync(filePath, { encoding: "utf-8" }); 54 | } 55 | 56 | readImage(filePath) { 57 | return Image.fromFile(filePath); 58 | } 59 | 60 | write(filePath, content) { 61 | fs.writeFileSync(filePath, content[dataKey]); 62 | } 63 | 64 | writeString(filePath, content) { 65 | fs.writeFileSync(filePath, content); 66 | } 67 | 68 | writeImage(filePath, image) { 69 | fs.writeFileSync(filePath, image[dataKey]); 70 | } 71 | 72 | remove(filePath) { 73 | fs.unlinkSync(filePath); 74 | } 75 | 76 | move(sourceFilePath, destinationFilePath) { 77 | fs.rename(sourceFilePath, destinationFilePath); 78 | } 79 | 80 | copy(sourceFilePath, destinationFilePath) { 81 | fs.copyFileSync(sourceFilePath, destinationFilePath, fs.constants.COPYFILE_EXCL); 82 | } 83 | 84 | fileExists(filePath) { 85 | return fs.existsSync(filePath); 86 | } 87 | 88 | isDirectory(path) { 89 | return (fs.existsSync(path) && fs.lstatSync(path).isDirectory()); 90 | } 91 | 92 | createDirectory(path, intermediateDirectories) { 93 | fs.mkdirSync(path, { recursive: intermediateDirectories }); 94 | } 95 | 96 | temporaryDirectory() { 97 | if (this.#type == 'iCloud') { 98 | throw new Error('Temporary directory cannot be accessed in iCloud. Use a local FileManager instead.'); 99 | } else { 100 | return os.tmpdir(); 101 | } 102 | } 103 | 104 | cacheDirectory() { 105 | if (this.#type == 'iCloud') { 106 | throw new Error('Cache directory cannot be accessed in iCloud. Use a local FileManager instead.'); 107 | } else { 108 | // What is the cache directory? 109 | } 110 | } 111 | 112 | documentsDirectory() { 113 | } 114 | 115 | libraryDirectory() { 116 | } 117 | 118 | joinPath(lhsPath, rhsPath) { 119 | return path.join(lhsPath, rhsPath); 120 | } 121 | 122 | allTags(filePath) { 123 | } 124 | 125 | addTag(filePath, tag) { 126 | } 127 | 128 | removeTag(filePath, tag) { 129 | } 130 | 131 | readExtendedAttribute(filePath, name) { 132 | } 133 | 134 | writeExtendedAttribute(filePath, value, name) { 135 | } 136 | 137 | removeExtendedAttribute(filePath, name) { 138 | } 139 | 140 | allExtendedAttributes(filePath) { 141 | } 142 | 143 | getUTI(filePath) { 144 | // file-uti is for macOS only 145 | if (process.platform === "darwin") { 146 | return require("file-uti").sync(filePath); 147 | } 148 | 149 | // For other platforms 150 | } 151 | 152 | listContents(directoryPath) { 153 | return fs.readdirSync(directoryPath); 154 | } 155 | 156 | fileName(filePath, includeFileExtension) { 157 | if (includeFileExtension) { 158 | return path.basename(filePath, path.extname(filePath)); 159 | } else { 160 | return path.basename(filePath); 161 | } 162 | } 163 | 164 | fileExtension(filePath) { 165 | return path.extname(filePath); 166 | } 167 | 168 | bookmarkedPath(name) { 169 | try { 170 | return this.allFileBookmarks().find(b => b.name === name)._path; 171 | } catch { 172 | throw new Error(`No bookmark named "${name}" found.`); 173 | } 174 | } 175 | 176 | bookmarkExists(name) { 177 | return Boolean(this.allFileBookmarks().find(b => b.name === name)); 178 | } 179 | 180 | async downloadFileFromiCloud(filePath) { 181 | } 182 | 183 | isFileStoredIniCloud(filePath) { 184 | } 185 | 186 | isFileDownloaded(filePath) { 187 | } 188 | 189 | creationDate(filePath) { 190 | return fs.statSync(filePath).birthtime; 191 | } 192 | 193 | modificationDate(filePath) { 194 | return fs.statSync(filePath).mtime; 195 | } 196 | 197 | fileSize(filePath) { 198 | return fs.statSync(filePath).size / 1000; 199 | } 200 | 201 | allFileBookmarks() { 202 | try { 203 | return JSON.parse(fs.readFileSync(BOOKMARKS_PATH, { encoding: "utf-8" })); 204 | } catch { 205 | return {}; 206 | } 207 | } 208 | } 209 | 210 | module.exports = { 211 | local: function() { 212 | return new FileManager('local'); 213 | }, 214 | 215 | iCloud: function() { 216 | return new FileManager('iCloud'); 217 | } 218 | } -------------------------------------------------------------------------------- /src/font.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | class Font { 4 | /** 5 | * @param {string} name 6 | * @param {number} size 7 | */ 8 | constructor(name, size) { 9 | this.name = name; 10 | this.size = size; 11 | } 12 | 13 | static largeTitle() { 14 | return new this("", ); 15 | } 16 | 17 | static title1() { 18 | return new this("", ); 19 | } 20 | 21 | static title2() { 22 | return new this("", ); 23 | } 24 | 25 | static title3() { 26 | return new this("", ); 27 | } 28 | 29 | static headline() { 30 | return new this("", ); 31 | } 32 | 33 | static subheadline() { 34 | return new this("", ); 35 | } 36 | 37 | static body() { 38 | return new this("", ); 39 | } 40 | 41 | static callout() { 42 | return new this("", ); 43 | } 44 | 45 | static footnote() { 46 | return new this("", ); 47 | } 48 | 49 | static caption1() { 50 | return new this("", ); 51 | } 52 | 53 | static caption2() { 54 | return new this("", ); 55 | } 56 | 57 | /** 58 | * @param {number} size 59 | */ 60 | static systemFont(size) { 61 | return new this("", size); 62 | } 63 | 64 | /** 65 | * @param {number} size 66 | */ 67 | static ultraLightSystemFont(size) { 68 | return new this("", size); 69 | } 70 | 71 | /** 72 | * @param {number} size 73 | */ 74 | static thinSystemFont(size) { 75 | return new this("", size); 76 | } 77 | 78 | /** 79 | * @param {number} size 80 | */ 81 | static lightSystemFont(size) { 82 | return new this("", size); 83 | } 84 | 85 | /** 86 | * @param {number} size 87 | */ 88 | static regularSystemFont(size) { 89 | return new this("", size); 90 | } 91 | 92 | /** 93 | * @param {number} size 94 | */ 95 | static mediumSystemFont(size) { 96 | return new this("", size); 97 | } 98 | 99 | /** 100 | * @param {number} size 101 | */ 102 | static semiboldSystemFont(size) { 103 | return new this("", size); 104 | } 105 | 106 | /** 107 | * @param {number} size 108 | */ 109 | static boldSystemFont(size) { 110 | return new this("", size); 111 | } 112 | 113 | /** 114 | * @param {number} size 115 | */ 116 | static heavySystemFont(size) { 117 | return new this("", size); 118 | } 119 | 120 | /** 121 | * @param {number} size 122 | */ 123 | static blackSystemFont(size) { 124 | return new this("", size); 125 | } 126 | 127 | /** 128 | * @param {number} size 129 | */ 130 | static italicSystemFont(size) { 131 | return new this("", size); 132 | } 133 | 134 | /** 135 | * @param {number} size 136 | */ 137 | static ultraLightMonospacedSystemFont(size) { 138 | return new this("", size); 139 | } 140 | 141 | /** 142 | * @param {number} size 143 | */ 144 | static thinMonospacedSystemFont(size) { 145 | return new this("", size); 146 | } 147 | 148 | /** 149 | * @param {number} size 150 | */ 151 | static lightMonospacedSystemFont(size) { 152 | return new this("", size); 153 | } 154 | 155 | /** 156 | * @param {number} size 157 | */ 158 | static regularMonospacedSystemFont(size) { 159 | return new this("", size); 160 | } 161 | 162 | /** 163 | * @param {number} size 164 | */ 165 | static mediumMonospacedSystemFont(size) { 166 | return new this("", size); 167 | } 168 | 169 | /** 170 | * @param {number} size 171 | */ 172 | static semiboldMonospacedSystemFont(size) { 173 | return new this("", size); 174 | } 175 | 176 | /** 177 | * @param {number} size 178 | */ 179 | static boldMonospacedSystemFont(size) { 180 | return new this("", size); 181 | } 182 | 183 | /** 184 | * @param {number} size 185 | */ 186 | static heavyMonospacedSystemFont(size) { 187 | return new this("", size); 188 | } 189 | 190 | /** 191 | * @param {number} size 192 | */ 193 | static blackMonospacedSystemFont(size) { 194 | return new this("", size); 195 | } 196 | 197 | /** 198 | * @param {number} size 199 | */ 200 | static ultraLightRoundedSystemFont(size) { 201 | return new this("", size); 202 | } 203 | 204 | /** 205 | * @param {number} size 206 | */ 207 | static thinRoundedSystemFont(size) { 208 | return new this("", size); 209 | } 210 | 211 | /** 212 | * @param {number} size 213 | */ 214 | static lightRoundedSystemFont(size) { 215 | return new this("", size); 216 | } 217 | 218 | /** 219 | * @param {number} size 220 | */ 221 | static regularRoundedSystemFont(size) { 222 | return new this("", size); 223 | } 224 | 225 | /** 226 | * @param {number} size 227 | */ 228 | static mediumRoundedSystemFont(size) { 229 | return new this("", size); 230 | } 231 | 232 | /** 233 | * @param {number} size 234 | */ 235 | static semiboldRoundedSystemFont(size) { 236 | return new this("", size); 237 | } 238 | 239 | /** 240 | * @param {number} size 241 | */ 242 | static boldRoundedSystemFont(size) { 243 | return new this("", size); 244 | } 245 | 246 | /** 247 | * @param {number} size 248 | */ 249 | static heavyRoundedSystemFont(size) { 250 | return new this("", size); 251 | } 252 | 253 | /** 254 | * @param {number} size 255 | */ 256 | static blackRoundedSystemFont(size) { 257 | return new this("", size); 258 | } 259 | } 260 | 261 | module.exports = Font; -------------------------------------------------------------------------------- /src/image.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require("fs"); 4 | const imageSize = require("image-size"); 5 | const Size = require("./size.js"); 6 | // Uncomment when finished 7 | // const Data = require("./data.js"); 8 | 9 | const dataKey = Symbol.for("data"); 10 | 11 | class Image { 12 | constructor(data) { 13 | Object.defineProperty(this, dataKey, { 14 | value: data, 15 | enumerable: false, 16 | writable: false 17 | }); 18 | 19 | const dimensions = imageSize(data); 20 | 21 | Object.defineProperty(this, "size", { 22 | value: new Size(dimensions.width, dimensions.height), 23 | enumerable: true, 24 | writable: false 25 | }); 26 | } 27 | } 28 | 29 | module.exports = { 30 | /** 31 | * @param {string} filePath 32 | * @returns {Image} an Image or null 33 | */ 34 | fromFile: function (filePath) { 35 | try { 36 | return new Image(fs.readFileSync(filePath)); 37 | } catch { 38 | return null; 39 | } 40 | }, 41 | 42 | /** 43 | * @param {Data} data 44 | */ 45 | fromData: function (data) { 46 | try { 47 | return new Image(data[dataKey]); 48 | } catch { 49 | return null; 50 | } 51 | } 52 | }; -------------------------------------------------------------------------------- /src/import-module.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function (name) { 4 | return require(name); 5 | }; -------------------------------------------------------------------------------- /src/keychain.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // For now, use a dummy keychain system that just stores everything in a JSON 4 | // Later, try to at least get the actual keychain integrated on macOS 5 | 6 | const KEYCHAIN_PATH = "../config/keychain.json"; 7 | 8 | const { readFileSync, writeFileSync } = require("fs"); 9 | 10 | function readKeychain() { 11 | try { 12 | return JSON.parse(readFileSync(KEYCHAIN_PATH, { encoding: "utf-8" })); 13 | } catch { 14 | return {}; 15 | } 16 | } 17 | 18 | function writeKeychain(obj) { 19 | writeFileSync(KEYCHAIN_PATH, JSON.stringify(obj, null, 2)); 20 | } 21 | 22 | module.exports = { 23 | contains: function (key) { 24 | return key in readKeychain(); 25 | }, 26 | 27 | set: function (key, value) { 28 | const currentKeychain = readKeychain(); 29 | currentKeychain[key] = value; 30 | writeKeychain(currentKeychain); 31 | }, 32 | 33 | get: function (key) { 34 | const currentKeychain = readKeychain(); 35 | if (!(key in currentKeychain)) { 36 | throw new Error(`The key '${key}' does not exist in the keychain.`); 37 | } 38 | return currentKeychain[key]; 39 | }, 40 | 41 | remove: function (key) { 42 | const currentKeychain = readKeychain(); 43 | delete currentKeychain[key]; 44 | writeKeychain(currentKeychain); 45 | } 46 | } -------------------------------------------------------------------------------- /src/linear-gradient.js: -------------------------------------------------------------------------------- 1 | const Point = require('./point.js'); 2 | 3 | class LinearGradient { 4 | constructor() { 5 | this.colors = []; 6 | this.locations = []; 7 | this.startPoint = new Point(0, 0); 8 | this.endPoint = new Point(0, 1) 9 | } 10 | } 11 | 12 | module.exports = LinearGradient; -------------------------------------------------------------------------------- /src/list-widget.js: -------------------------------------------------------------------------------- 1 | const Color = require('./color.js'); 2 | const Font = require('./font.js'); 3 | const Point = require('./point.js'); 4 | const Size = require('./size.js'); 5 | 6 | 7 | // Create an element to add to a WidgetStack or ListWidget 8 | function makeElement(type, object) { 9 | return { 10 | type: type, 11 | object: object 12 | } 13 | } 14 | 15 | 16 | // Present a widget - TO DO 17 | function present(widget, width, height) { 18 | } 19 | 20 | 21 | class WidgetSpacer { 22 | constructor(length) { 23 | this.length = length; 24 | } 25 | } 26 | 27 | 28 | // Parent class for WidgetDate and WidgetText 29 | class WidgetWrite { 30 | constructor() { 31 | this.textColor = null; 32 | this.font = Font.body(); 33 | this.textOpacity = 1; 34 | this.lineLimit = 0; 35 | this.minimumScaleFactor = 1; 36 | this.shadowColor = Color.black(); 37 | this.shadowRadius = 0; 38 | this.shadowOffset = new Point(0, 0); 39 | this.url = null; 40 | Object.defineProperty(this, "_align", { 41 | value: "left", 42 | writable: true 43 | }) 44 | } 45 | 46 | leftAlignText() { 47 | this._align = "left"; 48 | } 49 | 50 | centerAlignText() { 51 | this._align = "center"; 52 | } 53 | 54 | rightAlignText() { 55 | this._align = "right"; 56 | } 57 | } 58 | 59 | 60 | class WidgetText extends WidgetWrite { 61 | constructor(text) { 62 | this.text = text; 63 | } 64 | } 65 | 66 | 67 | class WidgetDate extends WidgetWrite { 68 | constructor(date) { 69 | this.date = date; 70 | this._style = "date"; 71 | } 72 | 73 | applyTimeStyle() { 74 | this._style = "time"; 75 | } 76 | 77 | applyDateStyle() { 78 | this._style = "date"; 79 | } 80 | 81 | applyRelativeStyle() { 82 | this._style = "relative"; 83 | } 84 | 85 | applyOffsetStyle() { 86 | this._style = "offset"; 87 | } 88 | 89 | applyTimerStyle() { 90 | this._style = "timer"; 91 | } 92 | } 93 | 94 | 95 | class WidgetImage { 96 | constructor(image) { 97 | this.image = image; 98 | this.resizable = true; 99 | this.imageSize = null; 100 | this.imageOpacity = 1; 101 | this.cornerRadius = 0; 102 | this.borderWidth = 0; 103 | this.borderColor = Color.black(); 104 | this.containerRelativeShape = false; 105 | this.tintColor = null; 106 | this.url = null; 107 | this._align = "left"; 108 | this._contentMode = "fitting"; 109 | } 110 | 111 | leftAlignImage() { 112 | this._align = "left"; 113 | } 114 | 115 | centerAlignImage() { 116 | this._align = "center"; 117 | } 118 | 119 | rightAlignImage() { 120 | this._align = "right"; 121 | } 122 | 123 | applyFittingContentMode() { 124 | _contentMode = "fitting"; 125 | } 126 | 127 | applyFillingContentMode() { 128 | _contentMode = "filling"; 129 | } 130 | } 131 | 132 | 133 | // Parent class for WidgetStack and ListWidget 134 | class WidgetBase { 135 | constructor() { 136 | this.backgroundColor = null; 137 | this.backgroundImage = null; 138 | this.backgroundGradient = null; 139 | this.spacing = 0; 140 | this.url = null; 141 | this._elements = []; 142 | this._padding = null; 143 | } 144 | 145 | addText(text) { 146 | let t = new WidgetText(text); 147 | this._elements.push(makeElement("text", t)); 148 | return t; 149 | } 150 | 151 | addDate(date) { 152 | let d = new WidgetDate(date); 153 | this._elements.push(makeElement("date", d)); 154 | return d; 155 | } 156 | 157 | addImage(image) { 158 | let i = new WidgetImage(image); 159 | this._elements.push(makeElement("image", i)); 160 | return i; 161 | } 162 | 163 | addSpacer(length) { 164 | let sp = new WidgetSpacer(length); 165 | this._elements.push(makeElement("spacer", sp)); 166 | return sp; 167 | } 168 | 169 | addStack() { 170 | let st = new WidgetStack(); 171 | this._elements.push(makeElement("stack", st)); 172 | return st; 173 | } 174 | 175 | setPadding(top, leading, bottom, trailing) { 176 | this._padding = { 177 | top: top, 178 | leading: leading, 179 | bottom: bottom, 180 | trailing: trailing 181 | } 182 | } 183 | 184 | useDefaultPadding() { 185 | this._padding = null; 186 | } 187 | } 188 | 189 | 190 | class WidgetStack extends WidgetBase { 191 | constructor() { 192 | this.size = new Size(0, 0); 193 | this.cornerRadius = 0; 194 | this.borderWidth = 0; 195 | this.borderColor = Color.black(); 196 | this._align = "top"; 197 | this._layout = "horizontal"; 198 | } 199 | 200 | topAlignContent() { 201 | this._align = "top"; 202 | } 203 | 204 | centerAlignContent() { 205 | this._align = "center"; 206 | } 207 | 208 | bottomAlignContent() { 209 | this._align = "bottom"; 210 | } 211 | 212 | layoutHorizontally() { 213 | this._layout = "horizontal"; 214 | } 215 | 216 | layoutVertically() { 217 | this._layout = "vertical"; 218 | } 219 | } 220 | 221 | 222 | // ListWidget class - TO DO: choose dimensions for small, medium, and large 223 | class ListWidget extends WidgetBase { 224 | constructor() { 225 | this.refreshAfterDate = null; 226 | } 227 | 228 | async presentSmall() { 229 | await present(this, SMALLWIDTH, SMALLHEIGHT); 230 | } 231 | 232 | async presentMedium() { 233 | await present(this, MEDWIDTH, MEDHEIGHT); 234 | } 235 | 236 | async presentLarge() { 237 | await present(this, LGWIDTH, LGHEIGHT); 238 | } 239 | } 240 | 241 | module.exports = ListWidget; 242 | -------------------------------------------------------------------------------- /src/location.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let accuracy = "best"; 4 | 5 | module.exports = { 6 | current: async function () { 7 | }, 8 | 9 | setAccuracyToBest: function () { 10 | accuracy = "best"; 11 | }, 12 | 13 | setAccuracyToTenMeters: function () { 14 | accuracy = 10; 15 | }, 16 | 17 | setAccuracyToHundredMeters: function () { 18 | accuracy = 100; 19 | }, 20 | 21 | setAccuracyToKilometer: function () { 22 | accuracy = 1000; 23 | }, 24 | 25 | setAccuracyToThreeKilometers: function () { 26 | accuracy = 3000; 27 | }, 28 | 29 | reverseGeocode: async function (latitude, longitude, locale) { 30 | } 31 | } -------------------------------------------------------------------------------- /src/mail.js: -------------------------------------------------------------------------------- 1 | class Mail { 2 | constructor() { 3 | this.bccRecipients = []; 4 | this.body = ""; 5 | this.ccRecipients = []; 6 | this.isBodyHTML = false; 7 | this.preferredSendingEmailAddress = null; 8 | this.subject = ""; 9 | this.toRecipients = []; 10 | Object.defineProperty(this, "_attachments", { 11 | value: [], 12 | writable: true 13 | }); 14 | } 15 | 16 | addDataAttachment(data, mimeType, filename) { 17 | } 18 | 19 | addFileAttachment(filePath) { 20 | } 21 | 22 | addImageAttachment(image) { 23 | } 24 | 25 | async send() { 26 | } 27 | } 28 | 29 | module.exports = Mail; -------------------------------------------------------------------------------- /src/message.js: -------------------------------------------------------------------------------- 1 | class Message { 2 | constructor() { 3 | this.recipients = []; 4 | this.body = ""; 5 | } 6 | 7 | async send() { 8 | } 9 | 10 | addImageAttachment(image) { 11 | } 12 | 13 | addFileAttachment(filePath) { 14 | } 15 | 16 | addDataAttachment(data, uti, filename) { 17 | } 18 | } 19 | 20 | module.exports = Message; -------------------------------------------------------------------------------- /src/notification.js: -------------------------------------------------------------------------------- 1 | const { v4: uuidv4 } = require("uuid"); 2 | 3 | class Notification { 4 | #actions; 5 | 6 | constructor() { 7 | this.identifier = uuidv4(); 8 | this.title = ""; 9 | this.subtitle = ""; 10 | this.body = ""; 11 | this.preferredContentHeight = null; 12 | this.badge = null; 13 | this.threadIdentifier = ""; 14 | this.userInfo = {}; 15 | this.sound = null; 16 | this.openURL = null; 17 | this.#actions = []; 18 | } 19 | 20 | get actions() { 21 | return this.#actions; 22 | } 23 | 24 | async schedule() { 25 | } 26 | 27 | async remove() { 28 | } 29 | 30 | setTriggerDate(date) { 31 | } 32 | 33 | setDailyTrigger(hour, minute, repeats) { 34 | } 35 | 36 | setWeeklyTrigger(weekday, hour, minute, repeats) { 37 | } 38 | 39 | addAction(title, url, destructive) { 40 | } 41 | 42 | static async allPending() { 43 | } 44 | 45 | static async allDelivered() { 46 | } 47 | 48 | static async removeAllPending() { 49 | } 50 | 51 | static async removeAllDelivered() { 52 | } 53 | 54 | static async removePending(identifiers) { 55 | } 56 | 57 | static async removeDelivered(identifiers) { 58 | } 59 | 60 | static resetCurrent() { 61 | } 62 | } 63 | 64 | module.exports = Notification; -------------------------------------------------------------------------------- /src/pasteboard.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { writeSync, readSync } = require("clipboardy"); 4 | 5 | const copyString = (string) => writeSync(string); 6 | const pasteString = () => readSync(); 7 | const copyImage = (image) => {}; // To-do 8 | const pasteImage = () => {}; // To-do 9 | 10 | module.exports = { 11 | copy: copyString, 12 | copyImage, 13 | copyString, 14 | paste: pasteString, 15 | pasteImage, 16 | pasteString 17 | }; -------------------------------------------------------------------------------- /src/path.js: -------------------------------------------------------------------------------- 1 | class Path { 2 | constructor() { 3 | // Set private properties 4 | } 5 | 6 | move(point) { 7 | } 8 | 9 | addLine(point) { 10 | } 11 | 12 | addRect(rect) { 13 | } 14 | 15 | addEllipse(rect) { 16 | } 17 | 18 | addRoundedRect(rect, cornerWidth, cornerHeight) { 19 | } 20 | 21 | addCurve(point, control1, control2) { 22 | } 23 | 24 | addQuadCurve(point, control) { 25 | } 26 | 27 | addLines(points) { 28 | } 29 | 30 | addRects(rects) { 31 | for (const rect of rects) { 32 | this.addRect(rect); 33 | } 34 | } 35 | 36 | closeSubpath() { 37 | } 38 | } 39 | 40 | module.exports = Path; -------------------------------------------------------------------------------- /src/photos.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | fromLibrary: async function() { 3 | // Present photo picker, return selected image 4 | }, 5 | 6 | fromCamera: async function() { 7 | // Present camera, return captured image 8 | }, 9 | 10 | latestPhoto: async function() { 11 | // Get latest photo 12 | }, 13 | 14 | latestPhotos: async function(count) { 15 | // Get the most recent [count] photos 16 | }, 17 | 18 | latestScreenshot: async function() { 19 | // Get the latest photo with type "screenshot" 20 | }, 21 | 22 | latestScreenshots: async function(count) { 23 | // Get the most recent [count] screenshots 24 | }, 25 | 26 | removeLatestPhoto: function() { 27 | // Delete the latest photo, show a prompt before deleting 28 | }, 29 | 30 | removeLatestPhotos: function(count) { 31 | // Delete the latest [count] photos, show a prompt before deleting 32 | }, 33 | 34 | removeLatestScreenshot: function() { 35 | // Delete the latest screenshot, show a prompt before deleting 36 | }, 37 | 38 | removeLatestScreenshots: function(count) { 39 | // Delete the latest [count] screenshots, show a prompt before deleting 40 | }, 41 | 42 | save: function(image) { 43 | // Save image to photo library 44 | } 45 | } -------------------------------------------------------------------------------- /src/point.js: -------------------------------------------------------------------------------- 1 | const scriptableTypeError = require("../util/type-error.js"); 2 | 3 | module.exports = class Point { 4 | constructor(x, y) { 5 | let hasBadType; 6 | if (typeof x !== "number") { 7 | hasBadType = x; 8 | } else if (typeof y !== "number") { 9 | hasBadType = y; 10 | } 11 | if (hasBadType !== undefined) { 12 | scriptableTypeError("number", typeof hasBadType); 13 | } 14 | this.x = x; 15 | this.y = y; 16 | } 17 | }; -------------------------------------------------------------------------------- /src/quick-look.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | present: async (item, fullscreen) => { 3 | 4 | /* 5 | Notes: 6 | 1. The `fullscreen` parameter has no effect in scriptable-node. 7 | 2. Only strings are currently supported for Quick Look. 8 | If a string is a file path, the associated file will be previewed. 9 | Otherwise, a preview of the string will be shown. 10 | 3. Quick Look is only supported on macOS for the moment. 11 | On all other platforms, a "not supported" message is logged to the console. 12 | */ 13 | 14 | if (process.platform == "darwin") { 15 | if (typeof item == "string") { 16 | await require('child_process').execFile("qlmanage", ["-p", item]); 17 | } else { 18 | console.log("scriptable-node does not currently support Quick Look for this type."); 19 | } 20 | } else { 21 | console.log(`Quick Look is not supported on the platform '${process.platform}'.`); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/rect.js: -------------------------------------------------------------------------------- 1 | const Size = require('./size.js'); 2 | const Point = require('./point.js'); 3 | 4 | /** 5 | * @summary Structure representing a rectangle. 6 | * @description The structure has a width, height and a coordinate in a two-dimensional coordinate system. 7 | * @see https://docs.scriptable.app/rect/ 8 | */ 9 | class Rect { 10 | /** 11 | * @summary Constructs a rectangle. 12 | * @description Constructs a new rectangle placed in a two-dimensional coordinate system. 13 | * @param {number} x X coordinate. 14 | * @param {number} y Y coordinate. 15 | * @param {number} width Width of rectangle. 16 | * @param {number} height Height of rectangle. 17 | * @see https://docs.scriptable.app/rect/#-new-rect 18 | */ 19 | constructor(x, y, width, height) { 20 | /** 21 | * @summary X value. 22 | * @description The x-coordinate of the rectangle. 23 | * @type {number} 24 | * @see https://docs.scriptable.app/rect/#x 25 | */ 26 | this.x = x; 27 | 28 | /** 29 | * @summary Y value. 30 | * @description The y-coordinate of the rectangle. 31 | * @type {number} 32 | * @see https://docs.scriptable.app/rect/#y 33 | */ 34 | this.y = y; 35 | 36 | /** 37 | * @summary Width of rectangle. 38 | * @description The width of the rectangle. 39 | * @type {number} 40 | * @see https://docs.scriptable.app/rect/#width 41 | */ 42 | this.width = width; 43 | 44 | /** 45 | * @summary Height of rectangle. 46 | * @description The height of the rectangle. 47 | * @type {number} 48 | * @see https://docs.scriptable.app/rect/#height 49 | */ 50 | this.height = height; 51 | } 52 | 53 | /** 54 | * @summary Minimum X value. 55 | * @description The smallest x-coordinate in the rectangle. 56 | * @type {number} 57 | * @readonly 58 | * @see https://docs.scriptable.app/rect/#minx 59 | */ 60 | get minX() { 61 | return this.x; 62 | } 63 | 64 | /** 65 | * @summary Minimum Y value. 66 | * @description The smallest y-coordinate in the rectangle. 67 | * @type {number} 68 | * @readonly 69 | * @see https://docs.scriptable.app/rect/#miny 70 | */ 71 | get minY() { 72 | return this.y 73 | } 74 | 75 | /** 76 | * @summary Maximum X value. 77 | * @description The greatest x-coordinate in the rectangle. 78 | * @type {number} 79 | * @readonly 80 | * @see https://docs.scriptable.app/rect/#maxx 81 | */ 82 | get maxX() { 83 | return this.x + this.width; 84 | } 85 | 86 | /** 87 | * @summary Maximum Y value. 88 | * @description The greatest y-coordinate in the rectangle. 89 | * @type {number} 90 | * @readonly 91 | * @see https://docs.scriptable.app/rect/#maxy 92 | */ 93 | get maxY() { 94 | return this.y + this.height; 95 | } 96 | 97 | /** 98 | * @summary Point that specifies the rectangles origin. 99 | * @description The x- and y-coordinate that specifies the rectangles origin as a Point structure. 100 | * @type {Point} 101 | * @see https://docs.scriptable.app/rect/#origin 102 | */ 103 | get origin() { 104 | return new Point(this.x, this.y); 105 | } 106 | 107 | set origin(p) { 108 | this.x = p.x; 109 | this.y = p.y; 110 | } 111 | 112 | /** 113 | * @summary Size of the rectangle. 114 | * @description The width and height of the rectangle as a Size structure. 115 | * @type {Size} 116 | * @see https://docs.scriptable.app/rect/#size 117 | */ 118 | get size() { 119 | return new Size(this.width, this.height) 120 | } 121 | set size(s) { 122 | this.width = s.width; 123 | this.height = s.height; 124 | } 125 | } 126 | 127 | module.exports = Rect; -------------------------------------------------------------------------------- /src/recurrence-rule.js: -------------------------------------------------------------------------------- 1 | class RecurrenceRule { 2 | } 3 | 4 | module.exports = { 5 | daily: function(interval) { 6 | }, 7 | 8 | dailyEndDate: function(interval, endDate) { 9 | }, 10 | 11 | dailyOccurrenceCount: function(interval, occurrenceCount) { 12 | }, 13 | 14 | weekly: function(interval) { 15 | }, 16 | 17 | weeklyEndDate: function(interval, endDate) { 18 | }, 19 | 20 | weeklyOccurrenceCount: function(interval, occurrenceCount) { 21 | }, 22 | 23 | monthly: function(interval) { 24 | }, 25 | 26 | monthlyEndDate: function(interval, endDate) { 27 | }, 28 | 29 | monthlyOccurrenceCount: function(interval, occurrenceCount) { 30 | }, 31 | 32 | yearly: function(interval) { 33 | }, 34 | 35 | yearlyEndDate: function(interval, endDate) { 36 | }, 37 | 38 | yearlyOccurrenceCount: function(interval, occurrenceCount) { 39 | }, 40 | 41 | complexWeekly: function(interval, daysOfTheWeek, setPositions) { 42 | }, 43 | 44 | complexWeeklyEndDate: function(interval, daysOfTheWeek, setPositions, endDate) { 45 | }, 46 | 47 | complexWeeklyOccurrenceCount: function(interval, daysOfTheWeek, setPositions, occurrenceCount) { 48 | }, 49 | 50 | complexMonthly: function(interval, daysOfTheWeek, daysOfTheMonth, setPositions) { 51 | }, 52 | 53 | complexMonthlyEndDate: function(interval, daysOfTheWeek, daysOfTheMonth, setPositions, endDate) { 54 | }, 55 | 56 | complexWeeklyOccurrenceCount: function(interval, daysOfTheWeek, daysOfTheMonth, setPositions, occurrenceCount) { 57 | }, 58 | 59 | complexYearly: function(interval, daysOfTheWeek, monthsOfTheYear, weeksOfTheYear, daysOfTheYear, setPositions) { 60 | }, 61 | 62 | complexYearlyEndDate: function(interval, daysOfTheWeek, monthsOfTheYear, weeksOfTheYear, daysOfTheYear, setPositions, endDate) { 63 | }, 64 | 65 | complexYearlyOccurrenceCount: function(interval, daysOfTheWeek, monthsOfTheYear, weeksOfTheYear, daysOfTheYear, setPositions, occurrenceCount) { 66 | } 67 | } -------------------------------------------------------------------------------- /src/relative-date-time-formatter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const scriptableTypeError = require("../util/type-error.js"); 4 | 5 | const MS_PER_SECOND = 1000; 6 | const SECONDS_PER_MINUTE = 60; 7 | const MINUTES_PER_HOUR = 60; 8 | const HOURS_PER_DAY = 24; 9 | const DAYS_PER_WEEK = 7; 10 | const MONTHS_PER_YEAR = 12; 11 | 12 | const MS_PER_MINUTE = MS_PER_SECOND * SECONDS_PER_MINUTE; 13 | const MS_PER_HOUR = MS_PER_MINUTE * MINUTES_PER_HOUR; 14 | const MS_PER_DAY = MS_PER_HOUR * HOURS_PER_DAY; 15 | const MS_PER_WEEK = MS_PER_DAY * DAYS_PER_WEEK; 16 | 17 | const roundDown = num => (num < 0) ? Math.ceil(num) : Math.floor(num); 18 | 19 | function monthsDiff(then, now) { 20 | const thenIsPast = then < now; 21 | const [firstDate, secondDate] = (thenIsPast) ? [then, now] : [now, then]; 22 | let diff = secondDate.getMonth() - firstDate.getMonth(); 23 | for (const unit of ["Date", "Hours", "Minutes", "Seconds", "Milliseconds"]) { 24 | const methodName = "get" + unit; 25 | const unitDiff = secondDate[methodName]() - firstDate[methodName](); 26 | if (unitDiff !== 0) { 27 | if (unitDiff < 0) diff -= 1; 28 | break; 29 | } 30 | } 31 | diff += MONTHS_PER_YEAR * (secondDate.getFullYear() - firstDate.getFullYear()); 32 | if (thenIsPast) diff *= -1; 33 | return diff; 34 | } 35 | 36 | class RelativeDateTimeFormatter { 37 | #intlLocale; 38 | #locale; 39 | #numericOutput; 40 | 41 | constructor() { 42 | this.#numericOutput = "auto"; 43 | this.#locale = undefined; 44 | this.#intlLocale = undefined; 45 | } 46 | 47 | get locale() { 48 | return this.#locale; 49 | } 50 | 51 | set locale(localeString) { 52 | if (!(typeof localeString === "string" || localeString instanceof String)) { 53 | throw scriptableTypeError("string", typeof localeString); 54 | } 55 | if (localeString.match(/[^a-zA-z\-_]/) === null) { 56 | const cleanedLocaleString = localeString.replace("_", "-"); 57 | try { 58 | this.#locale = cleanedLocaleString; 59 | this.#intlLocale = Intl.getCanonicalLocales(localeString)[0]; 60 | this.#locale = cleanedLocaleString; 61 | } catch { 62 | this.#locale = localeString; 63 | } 64 | } else { 65 | this.#locale = localeString; 66 | this.#intlLocale = undefined; 67 | } 68 | } 69 | 70 | string(date, referenceDate) { 71 | if (!(date instanceof Date)) throw scriptableTypeError("Date", typeof date); 72 | if (!(referenceDate instanceof Date)) throw scriptableTypeError("Date", typeof referenceDate); 73 | const formatter = new Intl.RelativeTimeFormat(this.#intlLocale, { 74 | numeric: this.#numericOutput 75 | }); 76 | const diff = date - referenceDate; 77 | const diffAbs = Math.abs(diff); 78 | const formatValues = {}; 79 | if (diffAbs < MS_PER_MINUTE) { 80 | formatValues.unit = "second"; 81 | formatValues.value = roundDown(diff / MS_PER_SECOND); 82 | } else if (diffAbs < MS_PER_HOUR) { 83 | formatValues.unit = "minute"; 84 | formatValues.value = roundDown(diff / MS_PER_MINUTE); 85 | } else if (diffAbs < MS_PER_DAY) { 86 | formatValues.unit = "hour"; 87 | formatValues.value = roundDown(diff / MS_PER_HOUR); 88 | } else if (diffAbs < MS_PER_WEEK) { 89 | formatValues.unit = "day"; 90 | formatValues.value = roundDown(diff / MS_PER_DAY); 91 | } else { 92 | const monthDiff = monthsDiff(date, referenceDate); 93 | const monthDiffAbs = Math.abs(monthDiff); 94 | if (monthDiffAbs < 1) { 95 | formatValues.unit = "week"; 96 | formatValues.value = roundDown(diff / MS_PER_WEEK); 97 | } else if (monthDiffAbs < MONTHS_PER_YEAR) { 98 | formatValues.unit = "month"; 99 | formatValues.value = monthDiff; 100 | } else { 101 | formatValues.unit = "year"; 102 | formatValues.value = roundDown(monthDiff / MONTHS_PER_YEAR); 103 | } 104 | } 105 | return formatter.format(formatValues.value, formatValues.unit); 106 | } 107 | 108 | useNamedDateTimeStyle() { 109 | this.#numericOutput = "auto"; 110 | } 111 | 112 | useNumericDateTimeStyle() { 113 | this.#numericOutput = "always"; 114 | } 115 | } 116 | 117 | module.exports = RelativeDateTimeFormatter; -------------------------------------------------------------------------------- /src/reminder.js: -------------------------------------------------------------------------------- 1 | class Reminder { 2 | constructor() {} 3 | 4 | addRecurrenceRule(recurrenceRule) {} 5 | 6 | removeAllRecurrenceRules() {} 7 | 8 | save() {} 9 | 10 | remove() {} 11 | 12 | static async scheduled(calendars) {} 13 | 14 | static async all(calendars) {} 15 | 16 | static async allCompleted(calendars) {} 17 | 18 | static async allIncomplete(calendars) {} 19 | 20 | static async allDueToday(calendars) {} 21 | 22 | static async completedDueToday(calendars) {} 23 | 24 | static async incompleteDueToday(calendars) {} 25 | 26 | static async allDueTomorrow(calendars) {} 27 | 28 | static async completedDueTomorrow(calendars) {} 29 | 30 | static async incompleteDueTomorrow(calendars) {} 31 | 32 | static async allDueYesterday(calendars) {} 33 | 34 | static async completedDueYesterday(calendars) {} 35 | 36 | static async incompleteDueYesterday(calendars) {} 37 | 38 | static async allDueThisWeek(calendars) {} 39 | 40 | static async completedDueThisWeek(calendars) {} 41 | 42 | static async incompleteDueThisWeek(calendars) {} 43 | 44 | static async allDueNextWeek(calendars) {} 45 | 46 | static async completedDueNextWeek(calendars) {} 47 | 48 | static async incompleteDueNextWeek(calendars) {} 49 | 50 | static async allDueLastWeek(calendars) {} 51 | 52 | static async completedDueLastWeek(calendars) {} 53 | 54 | static async incompleteDueLastWeek(calendars) {} 55 | 56 | static async completedToday(calendars) {} 57 | 58 | static async completedThisWeek(calendars) {} 59 | 60 | static async completedLastWeek(calendars) {} 61 | 62 | static async allDueBetween(startDate, endDate, calendars) {} 63 | 64 | static async completedDueBetween(startDate, endDate, calendars) {} 65 | 66 | static async incompleteDueBetween(startDate, endDate, calendars) {} 67 | 68 | static async completedBetween(startDate, endDate, calendars) {} 69 | } 70 | 71 | module.exports = Reminder; -------------------------------------------------------------------------------- /src/request.js: -------------------------------------------------------------------------------- 1 | const Data = require('./data.js'); 2 | const fetch = require('node-fetch'); 3 | const Image = require('./image.js'); 4 | 5 | async function getBody(request) {} 6 | 7 | class Request { 8 | constructor(url) { 9 | this.url = url; 10 | this.method = 'GET'; 11 | this.headers = {}; 12 | this.timeoutInterval = 60; 13 | this.response = null; 14 | this.allowInsecureRequest = false; 15 | } 16 | 17 | async load() { 18 | const body = await getBody(this); 19 | const requestData = await body.buffer(); 20 | return new Data(requestData); 21 | } 22 | 23 | async loadString() { 24 | const body = await getBody(this); 25 | const string = await body.text(); 26 | return string; 27 | } 28 | 29 | async loadJSON() { 30 | const body = await getBody(this); 31 | const json = await body.json(); 32 | return json; 33 | } 34 | 35 | async loadImage() { 36 | const body = await getBody(this); 37 | const requestData = await body.buffer(); 38 | return new Image(requestData); 39 | } 40 | 41 | addParameterToMultipart(name, value) { 42 | } 43 | 44 | addFileDataToMultipart(data, mimeType, name, filename) { 45 | } 46 | 47 | addFileToMultipart(filePath, name, filename) { 48 | } 49 | 50 | addImageToMultipart(image, name, filename) { 51 | } 52 | } 53 | 54 | module.exports = Request; -------------------------------------------------------------------------------- /src/safari.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const openModule = require('open'); 4 | 5 | module.exports = { 6 | openInApp: async function (url, fullscreen = true) { 7 | await openModule(url, { wait: true }); 8 | }, 9 | 10 | open: function (url) { 11 | openModule(url); 12 | } 13 | }; -------------------------------------------------------------------------------- /src/script.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require("path"); 4 | let output = null; 5 | 6 | module.exports = { 7 | name: function () { 8 | if (require.main) { 9 | const fileName = require.main.filename; 10 | return path.basename(fileName, path.extname(fileName)); 11 | } 12 | return module.id; 13 | }, 14 | 15 | complete: function () { 16 | // Inform the system that script has completed 17 | // Does nothing for now 18 | }, 19 | 20 | setShortcutOutput: function (value) { 21 | output = value; 22 | }, 23 | 24 | setWidget: function (widget) { 25 | // Update the widget to display with the widget passed as input 26 | // Does nothing for now since widgets are not implemented 27 | // (Who knows, will they ever be implemented? macOS maybe?) 28 | }, 29 | 30 | shortcutOutput: function () { 31 | return output; 32 | } 33 | }; -------------------------------------------------------------------------------- /src/sf-symbol.js: -------------------------------------------------------------------------------- 1 | const Font = require("./font.js"); 2 | 3 | const weights = { 4 | ULTRA_LIGHT: "ultralight", 5 | THIN: "thin", 6 | LIGHT: "light", 7 | REGULAR: "regular", 8 | MEDIUM: "medium", 9 | SEMI_BOLD: "semibold", 10 | BOLD: "bold", 11 | HEAVY: "heavy", 12 | BLACK: "black" 13 | }; 14 | 15 | Object.freeze(weights); 16 | 17 | class SFSymbol { 18 | #name; 19 | #weight; 20 | #font; 21 | /** 22 | * @param {string} name 23 | */ 24 | constructor(name) { 25 | this.#name = name; 26 | this.#weight = weights.REGULAR; 27 | this.#font = Font.body(); 28 | } 29 | 30 | get image() { 31 | // Convert SFSymbol (a character in a font) to PNG 32 | } 33 | /** 34 | * @param {Font} font 35 | */ 36 | applyFont(font) { 37 | this.#font = font; 38 | } 39 | 40 | applyUltraLightWeight() { 41 | this.#weight = weights.ULTRA_LIGHT; 42 | } 43 | 44 | applyThinWeight() { 45 | this.#weight = weights.THIN; 46 | } 47 | 48 | applyLightWeight() { 49 | this.#weight = weights.LIGHT; 50 | } 51 | 52 | applyRegularWeight() { 53 | this.#weight = weights.REGULAR; 54 | } 55 | 56 | applyMediumWeight() { 57 | this.#weight = weights.MEDIUM; 58 | } 59 | 60 | applySemiboldWeight() { 61 | this.#weight = weights.SEMI_BOLD; 62 | } 63 | 64 | applyBoldWeight() { 65 | this.#weight = weights.BOLD; 66 | } 67 | 68 | applyHeavyWeight() { 69 | this.#weight = weights.HEAVY; 70 | } 71 | 72 | applyBlackWeight() { 73 | this.#weight = weights.BLACK; 74 | } 75 | } 76 | /** 77 | * @param {string} symbolName 78 | */ 79 | exports.named = function(symbolName) { 80 | return new SFSymbol(symbolName); 81 | } 82 | 83 | // Uncomment this once class is complete 84 | // module.exports = SFSymbol; 85 | -------------------------------------------------------------------------------- /src/share-sheet.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | present: async (activityItems) => { 5 | // Present share sheet for activityItems 6 | console.log("Share sheet is not supported yet"); 7 | console.log("Share sheet items:") 8 | console.log(activityItems) 9 | // Return an object with the completed items 10 | // For now, this will always be the result for when the share sheet is dismissed without performing any actions 11 | // Example of when an action (in this case "Copy") is selected on iOS: 12 | // { "activity_type": "com.apple.UIKit.activity.CopyToPasteboard", "completed": true } 13 | return { 14 | completed: false, 15 | activity_type: null 16 | }; 17 | } 18 | }; -------------------------------------------------------------------------------- /src/size.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | class Size { 4 | constructor(width, height) { 5 | this.width = width; 6 | this.height = height; 7 | } 8 | } 9 | 10 | module.exports = Size; -------------------------------------------------------------------------------- /src/speech.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { speak } = require('say'); 4 | 5 | exports.speak = function (text) { 6 | speak(text); 7 | }; -------------------------------------------------------------------------------- /src/text-field.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | class TextField { 4 | #align; 5 | #keyboard; 6 | 7 | constructor(placeholder = null, text = null, secure = false) { 8 | this.font = null; 9 | this.isSecure = secure; 10 | this.placeholder = placeholder; 11 | this.text = text; 12 | this.textColor = null; 13 | 14 | this.#align = "left"; 15 | this.#keyboard = "default"; 16 | } 17 | 18 | centerAlignText() { 19 | this.#align = "center"; 20 | } 21 | 22 | leftAlignText() { 23 | this.#align = "left"; 24 | } 25 | 26 | rightAlignText() { 27 | this.#align = "right"; 28 | } 29 | 30 | setDecimalPadKeyboard() { 31 | this.#keyboard = "decimalPad"; 32 | } 33 | 34 | setDefaultKeyboard() { 35 | this.#keyboard = "default"; 36 | } 37 | 38 | setEmailAddressKeyboard() { 39 | this.#keyboard = "emailAddress"; 40 | } 41 | 42 | setNumberPadKeyboard() { 43 | this.#keyboard = "numberPad"; 44 | } 45 | 46 | setNumbersAndPunctuationKeyboard() { 47 | this.#keyboard = "numbersAndPunctuation"; 48 | } 49 | 50 | setPhonePadKeyboard() { 51 | this.#keyboard = "phonePad"; 52 | } 53 | 54 | setTwitterKeyboard() { 55 | this.#keyboard = "twitter"; 56 | } 57 | 58 | setURLKeyboard() { 59 | this.#keyboard = "url"; 60 | } 61 | 62 | setWebSearchKeyboard() { 63 | this.#keyboard = "webSearch"; 64 | } 65 | } 66 | 67 | module.exports = TextField; -------------------------------------------------------------------------------- /src/timer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const TIMER_TYPES = { 4 | INTERVAL: 0, 5 | TIMEOUT: 1 6 | }; 7 | 8 | /** 9 | * @summary A timer that fires after a time interval has elapsed. 10 | * @description The timer fires after a specified time interval has elapsed. The timer can be repeating, in which case it will fire multiple times. 11 | * @see https://docs.scriptable.app/timer/ 12 | */ 13 | class Timer { 14 | #type; 15 | #timer; 16 | 17 | /** 18 | * @summary Constructs a timer. 19 | * @description Constructs a timer that fires after a specified time interval. 20 | * @see https://docs.scriptable.app/timer/#-new-timer 21 | */ 22 | constructor() { 23 | /** 24 | * @type {number} 25 | * @summary The frequency at which the timer fires, in milliseconds. 26 | * @description Be aware that the time interval is specified in setting. Defaults to 0, causing the timer to fire instantly. 27 | * @see https://docs.scriptable.app/timer/#timeinterval 28 | */ 29 | this.timeInterval = 0; 30 | 31 | /** 32 | * @type {boolean} 33 | * @summary Whether the timer should repeat. 34 | * @description A repeating timer will keep firing until it is invalidated. In contrast to non-repeating timers, repeating timers are not automatically invalidated. Defaults to false. 35 | * @see https://docs.scriptable.app/timer/#repeats 36 | */ 37 | this.repeats = false; 38 | } 39 | 40 | /** 41 | * @summary Schedules the timer. 42 | * @description Schedules the timer using its configuration. The supplied function is called when the timer fires. To stop the timer from firing, call the invalidate() function. 43 | * @param {function} callback The callback to be called when the timer fires. 44 | * @see https://docs.scriptable.app/timer/#-schedule 45 | */ 46 | schedule(callback) { 47 | if (this.#timer) { 48 | this.invalidate(); 49 | } 50 | 51 | if (this.repeats) { 52 | this.#type = TIMER_TYPES.INTERVAL; 53 | this.#timer = setInterval(callback, this.timeInterval); 54 | } else { 55 | this.#type = TIMER_TYPES.TIMEOUT; 56 | this.#timer = setTimeout(callback, this.timeInterval); 57 | } 58 | } 59 | 60 | /** 61 | * @summary Stops the timer from firing. 62 | * @description Stops the timer from firing ever again. Non-repeating timers are automatically invalidated after they have fired once. Repeating timers must be manually invalidated. 63 | * @see https://docs.scriptable.app/timer/#-invalidate 64 | */ 65 | invalidate() { 66 | switch (this.#type) { 67 | case TIMER_TYPES.INTERVAL: 68 | clearInterval(this.#timer); 69 | break; 70 | case TIMER_TYPES.TIMEOUT: 71 | clearTimeout(this.#timer); 72 | } 73 | } 74 | 75 | /** 76 | * @summary Schedules a timer. 77 | * @description This is a convenience function for creating a new timer. The created timer is instantly scheduled and will fire after the specified time interval. 78 | * @param {number} timeInterval The time interval to fire the timer at. 79 | * @param {boolean} repeats Whether the timer should repeat or not. 80 | * @param {function} callback The callback to be called when the timer fires. 81 | * @returns {Timer} The constructed timer. 82 | * @see https://docs.scriptable.app/timer/#schedule 83 | */ 84 | static schedule(timeInterval, repeats, callback) { 85 | const t = new Timer(); 86 | t.timeInterval = timeInterval; 87 | t.repeats = repeats; 88 | t.schedule(callback); 89 | return t; 90 | } 91 | } 92 | 93 | module.exports = Timer; -------------------------------------------------------------------------------- /src/ui-table-cell.js: -------------------------------------------------------------------------------- 1 | //const { UITable } = require('../index.js'); 2 | //const Request = require('./request.js'); 3 | 4 | const CELL_TYPES = { 5 | BUTTON: "button", 6 | IMAGE: "image", 7 | TEXT: "text" 8 | }; 9 | 10 | const ALIGNMENTS = { 11 | CENTER: "center", 12 | LEFT: "left", 13 | RIGHT: "right" 14 | }; 15 | 16 | class UITableCell { 17 | #type; 18 | #align; 19 | #title; 20 | #subtitle; 21 | #image; 22 | constructor(type, props) { 23 | this.dismissOnTap = false; 24 | this.onTap = null; 25 | this.subtitleColor = null; 26 | this.subtitleFont = null; 27 | this.titleColor = null; 28 | this.titleFont = null; 29 | this.widthWeight = 0; 30 | this.#align = ALIGNMENTS.LEFT; 31 | this.#type = type; 32 | switch (type) { 33 | case CELL_TYPES.BUTTON: 34 | this.#title = props.title; 35 | break; 36 | case CELL_TYPES.IMAGE: 37 | this.#image = props.image; 38 | break; 39 | case CELL_TYPES.TEXT: 40 | this.#title = props.title; 41 | this.#subtitle = props.subtitle; 42 | break; 43 | default: 44 | throw new Error(`Unknown UITableCell type '${type}'`); 45 | } 46 | } 47 | 48 | centerAligned() { 49 | this.#align = ALIGNMENTS.CENTER; 50 | } 51 | 52 | leftAligned() { 53 | this.#align = ALIGNMENTS.LEFT; 54 | } 55 | 56 | rightAligned() { 57 | this.#align = ALIGNMENTS.RIGHT; 58 | } 59 | } 60 | 61 | module.exports = { 62 | button: (title) => new UITableCell(CELL_TYPES.BUTTON, { title }), 63 | image: (image) => new UITableCell(CELL_TYPES.IMAGE, { image }), 64 | imageAtURL: function(url) { 65 | //return this.image(await new Request(url).loadImage()); 66 | }, 67 | text: (title, subtitle) => new UITableCell(CELL_TYPES.TEXT, { title, subtitle }) 68 | }; -------------------------------------------------------------------------------- /src/ui-table-row.js: -------------------------------------------------------------------------------- 1 | const UITableCell = require('./ui-table-cell.js'); 2 | 3 | class UITableRow { 4 | #cells; 5 | constructor() { 6 | this.backgroundColor = null; 7 | this.cellSpacing = 2; 8 | this.dismissOnSelect = true; 9 | this.height = 44; 10 | this.isHeader = false; 11 | this.onSelect = null; 12 | this.#cells = []; 13 | } 14 | 15 | addButton(title) { 16 | const b = UITableCell.button(title); 17 | this.#cells.push(b); 18 | return b; 19 | } 20 | 21 | addCell(cell) { 22 | this._cells.push(cell); 23 | } 24 | 25 | addImage(image) { 26 | const i = UITableCell.image(image); 27 | this.#cells.push(i); 28 | return i; 29 | } 30 | 31 | addImageAtURL(url) { 32 | const i = UITableCell.imageAtURL(url); 33 | this.#cells.push(i); 34 | return i; 35 | } 36 | 37 | addText(title, subtitle) { 38 | const t = UITableCell.text(title, subtitle); 39 | this.#cells.push(t); 40 | return t; 41 | } 42 | } 43 | 44 | module.exports = UITableRow; -------------------------------------------------------------------------------- /src/ui-table.js: -------------------------------------------------------------------------------- 1 | class UITable { 2 | #rows; 3 | constructor() { 4 | this.showSeparators = false; 5 | this.#rows = []; 6 | } 7 | 8 | addRow(row) { 9 | this.#rows.push(row); 10 | } 11 | 12 | // Need to figure out how to present rows 13 | async present(fullscreen) { 14 | } 15 | 16 | // This will come with the present() method 17 | reload() { 18 | } 19 | 20 | removeAllRows() { 21 | this.#rows = []; 22 | } 23 | 24 | removeRow(row) { 25 | this.#rows.splice(this.#rows.indexOf(row), 1); 26 | } 27 | } 28 | 29 | module.exports = UITable; -------------------------------------------------------------------------------- /src/url-scheme.js: -------------------------------------------------------------------------------- 1 | // No support for the following deprecated methods because they depend on being run 2 | // in the Scriptable app environment via a URL scheme--their functionality was moved 3 | // to args: 4 | // - allParameters() 5 | // - parameter() 6 | 7 | "use strict"; 8 | 9 | const Script = require("./script.js"); 10 | const URL_PREFIX = "scriptable:///"; 11 | const encodedName = encodeURIComponent(require("path").basename(Script.name())); 12 | 13 | const forOpeningScript = () => `${URL_PREFIX}open/${encodedName}`; 14 | const forOpeningScriptSettings = () => forOpeningScript() + "?openSettings=true"; 15 | const forRunningScript = () => `${URL_PREFIX}run/${encodedName}`; 16 | 17 | module.exports = { 18 | forOpeningScript, 19 | forOpeningScriptSettings, 20 | forRunningScript 21 | }; -------------------------------------------------------------------------------- /src/uuid.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { v4: uuidv4 } = require("uuid"); 4 | 5 | module.exports = { 6 | string: () => uuidv4().toUpperCase() 7 | }; -------------------------------------------------------------------------------- /src/web-view.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | class WebView { 4 | static async loadHTML(html, baseURL, preferredSize, fullscreen) { 5 | } 6 | 7 | static async loadFile(fileURL, preferredSize, fullscreen) { 8 | } 9 | 10 | static async loadURL(url, preferredSize, fullscreen) { 11 | } 12 | 13 | async loadURL(url) { 14 | } 15 | 16 | async loadRequest(request) { 17 | } 18 | 19 | async loadHTML(html, baseURL) { 20 | } 21 | 22 | async loadFile(fileURL) { 23 | } 24 | 25 | async evaluateJavaScript(javaScript, useCallback) { 26 | } 27 | 28 | async getHTML() { 29 | } 30 | 31 | async present(fullscreen) { 32 | } 33 | 34 | async waitForLoad() { 35 | } 36 | } 37 | 38 | module.exports = WebView; -------------------------------------------------------------------------------- /src/xml-parser.js: -------------------------------------------------------------------------------- 1 | const { parser } = require("sax"); 2 | const scriptableTypeError = require("../util/type-error.js"); 3 | 4 | class XMLParser { 5 | #parser; 6 | 7 | constructor(string) { 8 | this.#parser = parser(); 9 | const stringType = typeof string; 10 | if (stringType != "string") { 11 | throw scriptableTypeError("string", stringType) 12 | } 13 | this.string = string; 14 | } 15 | 16 | get didEndDocument() { 17 | return this.#parser.onend; 18 | } 19 | 20 | set didEndDocument(fn) { 21 | this.#parser.onend = fn; 22 | } 23 | 24 | get didStartElement() { 25 | return this.#parser.onopentag; 26 | } 27 | 28 | set didStartElement(fn) { 29 | this.#parser.onopentag = fn; 30 | } 31 | 32 | get didEndElement() { 33 | return this.#parser.onclosetag; 34 | } 35 | 36 | set didEndElement(fn) { 37 | this.#parser.onclosetag = fn; 38 | } 39 | 40 | get foundCharacters() { 41 | return this.#parser.ontext; 42 | } 43 | 44 | set foundCharacters(fn) { 45 | this.#parser.ontext = fn; 46 | } 47 | 48 | get parseErrorOccurred() { 49 | return this.#parser.onerror; 50 | } 51 | 52 | set parseErrorOccurred(fn) { 53 | this.#parser.onerror = fn; 54 | } 55 | 56 | parse() { 57 | try { 58 | this.#parser.write(this.string).close(); 59 | if (typeof this.didStartDocument === "function") { 60 | this.didStartDocument(); 61 | } 62 | return true; 63 | } catch { 64 | return false; 65 | } 66 | } 67 | } 68 | 69 | module.exports = XMLParser; -------------------------------------------------------------------------------- /util/async-syncifier.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { execFileSync } = require("child_process"); 4 | 5 | const execString = script => { 6 | return execFileSync("node", ["-e", script], { encoding: "utf-8" }); 7 | }; 8 | 9 | class WrapperScript { 10 | constructor(starter = "") { 11 | if (typeof starter === "string") { 12 | this.script = starter; 13 | } else if (starter instanceof String) { 14 | this.script = starter.toString(); 15 | } else { 16 | this.script = ""; 17 | } 18 | } 19 | 20 | addLine(str) { 21 | this.script += str + "\n"; 22 | } 23 | 24 | asyncWrap() { 25 | const asyncFunc = (async ()=>{}).constructor(this.script).toString(); 26 | this.script = "(" + asyncFunc + ")();" 27 | } 28 | } 29 | 30 | const getDirect = (type, moduleName, fnName) => { 31 | const wrapperScript = new WrapperScript("\"use strict\"\n;"); 32 | wrapperScript.addLine(`const scriptData = ${JSON.stringify({ moduleName, fnName })};`); 33 | wrapperScript.addLine(`const createLogMessage = ${require("./create-log-message.js").toString()};`); 34 | let importStatement; 35 | switch (type) { 36 | case "esm": 37 | importStatement = "await import"; 38 | break; 39 | case "cjs": 40 | default: 41 | importStatement = "require"; 42 | } 43 | wrapperScript.addLine(`const moduleToRun = ${importStatement}(scriptData.moduleName);`); 44 | wrapperScript.addLine("const functionToRun = moduleToRun[scriptData.fnName];"); 45 | wrapperScript.addLine("const executedFunction = await functionToRun();"); 46 | wrapperScript.addLine("process.stdout.write(createLogMessage(executedFunction));"); 47 | wrapperScript.asyncWrap(); 48 | return execString(wrapperScript.script); 49 | } 50 | 51 | module.exports = { execString, WrapperScript, getDirect }; -------------------------------------------------------------------------------- /util/create-log-message.js: -------------------------------------------------------------------------------- 1 | module.exports = function _scriptable_createLogMessage(obj) { 2 | if (typeof obj === "string") return obj; 3 | if (typeof obj === "undefined") return "undefined"; 4 | if (typeof obj === "object" && !(obj instanceof String)) return JSON.stringify(obj); 5 | return obj.toString(); 6 | }; -------------------------------------------------------------------------------- /util/run-jxa.js: -------------------------------------------------------------------------------- 1 | module.exports = (script, standardAdditions) => { 2 | if (standardAdditions) { 3 | script = `app = Application.currentApplication(); 4 | app.includeStandardAdditions = true; 5 | ${script}`; 6 | } 7 | return require("child_process").execFileSync("osascript", [ 8 | "-l", 9 | "JavaScript", 10 | "-e", 11 | script 12 | ], { 13 | encoding: "utf-8" 14 | }); 15 | }; -------------------------------------------------------------------------------- /util/type-error.js: -------------------------------------------------------------------------------- 1 | module.exports = (expected, got) => { 2 | return new Error(`Expected value of type ${expected} but got value of type ${got}.`) 3 | } -------------------------------------------------------------------------------- /util/xcall.app/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildMachineOSBuild 6 | 16E195 7 | CFBundleDevelopmentRegion 8 | en 9 | CFBundleExecutable 10 | xcall 11 | CFBundleIdentifier 12 | de.martin-finke.xcall 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | xcall 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSupportedPlatforms 22 | 23 | MacOSX 24 | 25 | CFBundleURLTypes 26 | 27 | 28 | CFBundleTypeRole 29 | Viewer 30 | CFBundleURLSchemes 31 | 32 | xcall066958CA 33 | 34 | 35 | 36 | CFBundleVersion 37 | 1 38 | DTCompiler 39 | com.apple.compilers.llvm.clang.1_0 40 | DTPlatformBuild 41 | 8E162 42 | DTPlatformVersion 43 | GM 44 | DTSDKBuild 45 | 16E185 46 | DTSDKName 47 | macosx10.12 48 | DTXcode 49 | 0830 50 | DTXcodeBuild 51 | 8E162 52 | LSBackgroundOnly 53 | 54 | LSMinimumSystemVersion 55 | 10.8 56 | NSHumanReadableCopyright 57 | Copyright © 2017 Martin Finke. All rights reserved. 58 | NSMainStoryboardFile 59 | Main 60 | NSPrincipalClass 61 | NSApplication 62 | 63 | 64 | -------------------------------------------------------------------------------- /util/xcall.app/Contents/MacOS/xcall: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colin273/scriptable-node/2b00f7a48b5431d1ad2f45d9513c2fb4f9039cf0/util/xcall.app/Contents/MacOS/xcall -------------------------------------------------------------------------------- /util/xcall.app/Contents/PkgInfo: -------------------------------------------------------------------------------- 1 | APPL???? -------------------------------------------------------------------------------- /util/xcall.app/Contents/Resources/Base.lproj/Main.storyboardc/Info.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colin273/scriptable-node/2b00f7a48b5431d1ad2f45d9513c2fb4f9039cf0/util/xcall.app/Contents/Resources/Base.lproj/Main.storyboardc/Info.plist -------------------------------------------------------------------------------- /util/xcall.app/Contents/Resources/Base.lproj/Main.storyboardc/MainMenu.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colin273/scriptable-node/2b00f7a48b5431d1ad2f45d9513c2fb4f9039cf0/util/xcall.app/Contents/Resources/Base.lproj/Main.storyboardc/MainMenu.nib -------------------------------------------------------------------------------- /util/xcall.app/Contents/_CodeSignature/CodeResources: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | files 6 | 7 | Resources/Base.lproj/Main.storyboardc/Info.plist 8 | 9 | M1rS+Tcas1ZdM4/iibTlduTJ22M= 10 | 11 | Resources/Base.lproj/Main.storyboardc/MainMenu.nib 12 | 13 | Iiw88bivJdVunQI4TtXBh9TsYiY= 14 | 15 | 16 | files2 17 | 18 | Resources/Base.lproj/Main.storyboardc/Info.plist 19 | 20 | hash 21 | 22 | M1rS+Tcas1ZdM4/iibTlduTJ22M= 23 | 24 | hash2 25 | 26 | 6/2HagpKuzGhxFgQU55Lc/bxgR30qm5eqHSV+p9e4/4= 27 | 28 | 29 | Resources/Base.lproj/Main.storyboardc/MainMenu.nib 30 | 31 | hash 32 | 33 | Iiw88bivJdVunQI4TtXBh9TsYiY= 34 | 35 | hash2 36 | 37 | PawTg0hStx+2CRVIIA9+03eVrCParobuC0K207J+A/0= 38 | 39 | 40 | 41 | rules 42 | 43 | ^Resources/ 44 | 45 | ^Resources/.*\.lproj/ 46 | 47 | optional 48 | 49 | weight 50 | 1000 51 | 52 | ^Resources/.*\.lproj/locversion.plist$ 53 | 54 | omit 55 | 56 | weight 57 | 1100 58 | 59 | ^Resources/Base\.lproj/ 60 | 61 | weight 62 | 1010 63 | 64 | ^version.plist$ 65 | 66 | 67 | rules2 68 | 69 | .*\.dSYM($|/) 70 | 71 | weight 72 | 11 73 | 74 | ^(.*/)?\.DS_Store$ 75 | 76 | omit 77 | 78 | weight 79 | 2000 80 | 81 | ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ 82 | 83 | nested 84 | 85 | weight 86 | 10 87 | 88 | ^.* 89 | 90 | ^Info\.plist$ 91 | 92 | omit 93 | 94 | weight 95 | 20 96 | 97 | ^PkgInfo$ 98 | 99 | omit 100 | 101 | weight 102 | 20 103 | 104 | ^Resources/ 105 | 106 | weight 107 | 20 108 | 109 | ^Resources/.*\.lproj/ 110 | 111 | optional 112 | 113 | weight 114 | 1000 115 | 116 | ^Resources/.*\.lproj/locversion.plist$ 117 | 118 | omit 119 | 120 | weight 121 | 1100 122 | 123 | ^Resources/Base\.lproj/ 124 | 125 | weight 126 | 1010 127 | 128 | ^[^/]+$ 129 | 130 | nested 131 | 132 | weight 133 | 10 134 | 135 | ^embedded\.provisionprofile$ 136 | 137 | weight 138 | 20 139 | 140 | ^version\.plist$ 141 | 142 | weight 143 | 20 144 | 145 | 146 | 147 | 148 | --------------------------------------------------------------------------------